From d8873a463d33fb4cab8c53e309fb0c642d1050f7 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 7 Jan 2025 15:22:34 +0900 Subject: [PATCH 001/215] =?UTF-8?q?=08docs:=20feature,=20bug=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. feature 템플릿 추가 2. bug 템플릿 추가 --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a40b6a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## 어떤 버그인가요? + +> 어떤 버그인지 간결하게 설명해주세요 + +## 어떤 상황에서 발생한 버그인가요? + +> (가능하면) Given-When-Then 형식으로 서술해주세요 + +## 예상 결과 + +> 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 + +## 참고할만한 자료(선택) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..eaa144a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## 어떤 기능인가요? + +> 추가하려는 기능에 대해 간결하게 설명해주세요 + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) From d3f86fada93e0374d647b75ded288fbe1cb0bef4 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 7 Jan 2025 16:33:57 +0900 Subject: [PATCH 002/215] =?UTF-8?q?feat:=20CustomException=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 1. GlobalExceptionHandler 추가 2. ApiExcepion 추가 3. ErrorCode 추가 4. ErrorResponse 추가 --- .../gamemate/global/constant/ErrorCode.java | 26 +++++ .../global/exception/ApiException.java | 11 +++ .../global/exception/ErrorResponse.java | 46 +++++++++ .../exception/GlobalExceptionHandler.java | 97 +++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 src/main/java/com/example/gamemate/global/constant/ErrorCode.java create mode 100644 src/main/java/com/example/gamemate/global/exception/ApiException.java create mode 100644 src/main/java/com/example/gamemate/global/exception/ErrorResponse.java create mode 100644 src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java new file mode 100644 index 0000000..fb0ab78 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -0,0 +1,26 @@ +package com.example.gamemate.global.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + /* 400 잘못된 입력값 */ + INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), + + /* 401 세션 없음 */ + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), + NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + + + /* 500 서버 오류 */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; + + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/gamemate/global/exception/ApiException.java b/src/main/java/com/example/gamemate/global/exception/ApiException.java new file mode 100644 index 0000000..d0085f8 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/exception/ApiException.java @@ -0,0 +1,11 @@ +package com.example.gamemate.global.exception; + +import com.example.gamemate.global.constant.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ApiException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/example/gamemate/global/exception/ErrorResponse.java b/src/main/java/com/example/gamemate/global/exception/ErrorResponse.java new file mode 100644 index 0000000..b15ba16 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/exception/ErrorResponse.java @@ -0,0 +1,46 @@ +package com.example.gamemate.global.exception; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.bind.validation.ValidationErrors; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class ErrorResponse { + private final int status; + private final String code; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List errors; + + @Getter + @Builder + public static class ValidationError{ + private final String field; + private final String message; + + public static ValidationError of(final FieldError fieldError) { + return ValidationError.builder() + .field(fieldError.getField()) + .message(fieldError.getDefaultMessage()) + .build(); + } + + public static List of(final List fieldErrors) { + return fieldErrors.stream() + .map(ValidationError::of) + .collect(Collectors.toList()); + } + } + + + +} diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4f505d7 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,97 @@ +package com.example.gamemate.global.exception; + +import com.example.gamemate.global.constant.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.Response; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.gamemate.global.constant.ErrorCode.NO_SESSION; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + @ExceptionHandler(ApiException.class) + public ResponseEntity handleCustomException(ApiException e) { + log.info("errorHandler start"); + ErrorCode errorCode = e.getErrorCode(); + return handleExceptionInternal(errorCode,errorCode.getMessage()); + } + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException ex) { + + return ResponseEntity.status(ex.getStatusCode()) + .body(ex.getReason()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { + log.warn("handleIllegalArgument", e); + ErrorCode errorCode = ErrorCode.INVALID_PARAMETER; + return handleExceptionInternal(errorCode, errorCode.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleIRuntime(RuntimeException e) { + log.warn("handleIRuntime", e); + ErrorCode errorCode = ErrorCode.NO_SESSION; + return handleExceptionInternal(errorCode, errorCode.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException e) { + log.warn("handleMethodArgumentNotValid", e); + + // 유효성 검사 에러 리스트 변환 + List errors = e.getBindingResult().getFieldErrors().stream() + .map(ErrorResponse.ValidationError::of) + .collect(Collectors.toList()); + + // ErrorResponse + ErrorResponse errorResponse = ErrorResponse.builder() + .status(ErrorCode.INVALID_PARAMETER.getStatus().value()) + .code(ErrorCode.INVALID_INPUT.name()) + .message("Validation failed") + .errors(errors) + .build(); + + return ResponseEntity.badRequest().body(errorResponse); + } + + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { + return ResponseEntity.status(errorCode.getStatus()) + .body(makeErrorResponse(errorCode)); + } + + private ErrorResponse makeErrorResponse(ErrorCode errorCode) { + return ErrorResponse.builder() + .status(errorCode.getStatus().value()) + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .build(); + } + + private ResponseEntity handleExceptionInternal(ErrorCode errorCode, String message) { + return ResponseEntity.status(errorCode.getStatus()) + .body(makeErrorResponse(errorCode, message)); + } + + private ErrorResponse makeErrorResponse(ErrorCode errorCode, String message) { + return ErrorResponse.builder() + .status(errorCode.getStatus().value()) + .code(errorCode.name()) + .message(message) + .build(); + } + +} From 1bd0771d68a65ae89f87db1a565a3f27b2310ced Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 16:34:51 +0900 Subject: [PATCH 003/215] =?UTF-8?q?feat=20:=20follow=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow 엔티티, FollowController, FollowService, FollowRepository 생성 --- .../gamemate/domain/follow/Follow.java | 29 +++++++++++++++++++ .../domain/follow/FollowController.java | 15 ++++++++++ .../domain/follow/FollowRepository.java | 8 +++++ .../gamemate/domain/follow/FollowService.java | 4 +++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/follow/Follow.java create mode 100644 src/main/java/com/example/gamemate/domain/follow/FollowController.java create mode 100644 src/main/java/com/example/gamemate/domain/follow/FollowRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/follow/FollowService.java diff --git a/src/main/java/com/example/gamemate/domain/follow/Follow.java b/src/main/java/com/example/gamemate/domain/follow/Follow.java new file mode 100644 index 0000000..b5bde89 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/Follow.java @@ -0,0 +1,29 @@ +package com.example.gamemate.domain.follow; + +import com.example.gamemate.global.BaseCreatedEntity; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Getter +public class Follow extends BaseCreatedEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "follower_id", nullable = false) + private User follower; + + @ManyToOne + @JoinColumn(name = "followee_id", nullable = false) + private User followee; + + public Follow() { + } + + public Follow(User follower, User followee) { + this.follower = follower; + this.followee = followee; + } +} diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java new file mode 100644 index 0000000..b9e209c --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -0,0 +1,15 @@ +package com.example.gamemate.domain.follow; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/follows") +public class FollowController { +// +// @PostMapping +// public +} diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java new file mode 100644 index 0000000..08d772b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java @@ -0,0 +1,8 @@ +package com.example.gamemate.domain.follow; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FollowRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java new file mode 100644 index 0000000..303e13d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -0,0 +1,4 @@ +package com.example.gamemate.domain.follow; + +public class FollowService { +} From 4d1e516bf35b6bebaaf8cec9e87e8e0b895b2b48 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 17:46:58 +0900 Subject: [PATCH 004/215] =?UTF-8?q?feat=20:=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/follow/FollowController.java | 21 +++++++++++-- .../domain/follow/FollowRepository.java | 1 + .../gamemate/domain/follow/FollowService.java | 31 +++++++++++++++++++ .../follow/dto/FollowCreateRequestDto.java | 12 +++++++ .../follow/dto/FollowCreateResponseDto.java | 12 +++++++ .../gamemate/global/constant/ErrorCode.java | 1 + 6 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index b9e209c..19fea22 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -1,7 +1,12 @@ package com.example.gamemate.domain.follow; +import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; +import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -9,7 +14,17 @@ @RequiredArgsConstructor @RequestMapping("/follows") public class FollowController { -// -// @PostMapping -// public + + private final FollowService followService; + + /** + * 팔로우 하기 + * @param dto FollowCreateRequestDto + * @return message = "팔로우 했습니다." + */ + @PostMapping + public ResponseEntity createFollow(@RequestBody FollowCreateRequestDto dto) { + FollowCreateResponseDto followCreateResponseDto = followService.createFollow(dto); + return new ResponseEntity<>(followCreateResponseDto, HttpStatus.CREATED); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java index 08d772b..b281c34 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java @@ -5,4 +5,5 @@ @Repository public interface FollowRepository extends JpaRepository { + Boolean existsByFollowerAndFollowee(User follower, User followee); } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 303e13d..654c75d 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -1,4 +1,35 @@ package com.example.gamemate.domain.follow; +import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; +import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +@RequiredArgsConstructor public class FollowService { + private final UserRepository userRepository; + private final FollowRepository followRepository; + + // 팔로우하기 + // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 + @Transactional + public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { + User follower = userRepository.findById(1L).orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다.")); + User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다.")); + + if (followRepository.existsByFollowerAndFollowee(follower, followee)) { + throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); + } + + Follow follow = new Follow(follower,followee); + followRepository.save(follow); + + return new FollowCreateResponseDto("팔로우 했습니다."); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateRequestDto.java new file mode 100644 index 0000000..677696d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateRequestDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowCreateRequestDto { + private String email; + + public FollowCreateRequestDto(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java new file mode 100644 index 0000000..88f6d70 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowCreateResponseDto { + private String message; + + public FollowCreateResponseDto(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index fb0ab78..26713f9 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -10,6 +10,7 @@ public enum ErrorCode { /* 400 잘못된 입력값 */ INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), + IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 유저입니다."), /* 401 세션 없음 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From 4e8e75861661ec5702b68398e99f9c501eddb431 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 18:15:00 +0900 Subject: [PATCH 005/215] =?UTF-8?q?feat=20:=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/follow/FollowController.java | 17 +++++++++++++---- .../gamemate/domain/follow/FollowService.java | 18 ++++++++++++++++++ .../follow/dto/FollowDeleteResponseDto.java | 12 ++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index 19fea22..8a88571 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -2,13 +2,11 @@ import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; +import com.example.gamemate.domain.follow.dto.FollowDeleteResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -27,4 +25,15 @@ public ResponseEntity createFollow(@RequestBody FollowC FollowCreateResponseDto followCreateResponseDto = followService.createFollow(dto); return new ResponseEntity<>(followCreateResponseDto, HttpStatus.CREATED); } + + /** + * 팔로우 취소 + * @param followId 취소할 팔로우 식별자 + * @return message = "팔로우를 취소했습니다." + */ + @DeleteMapping("/{followId}") + public ResponseEntity deleteFollow(@PathVariable Long followId) { + FollowDeleteResponseDto followDeleteResponseDto = followService.deleteFollow(followId); + return new ResponseEntity<>(followDeleteResponseDto,HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 654c75d..ccb29e7 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; +import com.example.gamemate.domain.follow.dto.FollowDeleteResponseDto; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; @@ -32,4 +33,21 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { return new FollowCreateResponseDto("팔로우 했습니다."); } + + // 팔로우 취소하기 + // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 + @Transactional + public FollowDeleteResponseDto deleteFollow(Long followId) { + Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new RuntimeException("팔로우를 찾을 수 없습니다.")); + + User follower = userRepository.findById(1L).orElseThrow(); + + if (findFollow.getFollower() != follower) { + throw new ApiException(ErrorCode.INVALID_INPUT); + } + + followRepository.delete(findFollow); + + return new FollowDeleteResponseDto("팔로우가 취소되었습니다."); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java new file mode 100644 index 0000000..34a10e4 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowDeleteResponseDto { + private String message; + + public FollowDeleteResponseDto(String message) { + this.message = message; + } +} From e0c1b4fac0c391bf20d65341bded6970713afd99 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 19:03:36 +0900 Subject: [PATCH 006/215] =?UTF-8?q?fix=20:=20=EB=B3=B8=EC=9D=B8=EC=9D=84?= =?UTF-8?q?=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/domain/follow/FollowService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index ccb29e7..79831da 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; +import java.util.Objects; + @Service @RequiredArgsConstructor public class FollowService { @@ -28,6 +30,10 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); } + if (Objects.equals(follower.getEmail(), dto.getEmail())) { + throw new ApiException(ErrorCode.INVALID_INPUT); + } + Follow follow = new Follow(follower,followee); followRepository.save(follow); From d2f0c7a92350933ede7bac1e26b0e26452596ba4 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 19:12:44 +0900 Subject: [PATCH 007/215] =?UTF-8?q?refactor=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=96=88=EB=8D=98=20RuntimeException=20=EC=9D=84=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20ApiException=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/gamemate/domain/follow/FollowService.java | 10 ++++------ .../example/gamemate/global/constant/ErrorCode.java | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 79831da..0b7e88f 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -7,9 +7,7 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; import java.util.Objects; @@ -23,8 +21,8 @@ public class FollowService { // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { - User follower = userRepository.findById(1L).orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다.")); - User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다.")); + User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); if (followRepository.existsByFollowerAndFollowee(follower, followee)) { throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); @@ -44,9 +42,9 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional public FollowDeleteResponseDto deleteFollow(Long followId) { - Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new RuntimeException("팔로우를 찾을 수 없습니다.")); + Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_FOLLOW)); - User follower = userRepository.findById(1L).orElseThrow(); + User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); if (findFollow.getFollower() != follower) { throw new ApiException(ErrorCode.INVALID_INPUT); diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 26713f9..8dc221b 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -16,6 +16,9 @@ public enum ErrorCode { UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + /* 404 찾을 수 없음 */ + NOT_FOUND_USER(HttpStatus.NOT_FOUND, "NOT_FOUND_USER", "유저를 찾을 수 없습니다."), + NOT_FOUND_FOLLOW(HttpStatus.NOT_FOUND,"NOT_FOUND_FOLLOW", "팔로우를 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 34faed1ef09d3709afe68ff34ba00073a49b15cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 7 Jan 2025 19:32:12 +0900 Subject: [PATCH 008/215] =?UTF-8?q?feat:=20user=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EA=B8=B0=EB=8A=A5=20=EC=B4=88=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 41 ++++++++++++ .../user/dto/PasswordUpdateRequestDto.java | 19 ++++++ .../domain/user/dto/ProfileResponseDto.java | 22 +++++++ .../user/dto/ProfileUpdateRequestDto.java | 12 ++++ .../gamemate/domain/user/entity/User.java | 65 +++++++++++++++++++ .../gamemate/domain/user/enums/Authority.java | 16 +++++ .../domain/user/enums/UserStatus.java | 16 +++++ .../user/repository/UserRepository.java | 9 +++ .../domain/user/service/UserService.java | 47 ++++++++++++++ .../gamemate/global/common/BaseEntity.java | 25 +++++++ .../gamemate/global/constant/ErrorCode.java | 4 ++ 11 files changed, 276 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/user/controller/UserController.java create mode 100644 src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/user/entity/User.java create mode 100644 src/main/java/com/example/gamemate/domain/user/enums/Authority.java create mode 100644 src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java create mode 100644 src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/user/service/UserService.java create mode 100644 src/main/java/com/example/gamemate/global/common/BaseEntity.java diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java new file mode 100644 index 0000000..ffdba68 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -0,0 +1,41 @@ +package com.example.gamemate.domain.user.controller; + +import com.example.gamemate.domain.user.dto.PasswordUpdateRequestDto; +import com.example.gamemate.domain.user.dto.ProfileResponseDto; +import com.example.gamemate.domain.user.dto.ProfileUpdateRequestDto; +import com.example.gamemate.domain.user.service.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + private final UserService userService; + + @GetMapping("/{id}") + public ResponseEntity findProfile(@PathVariable Long id) { + ProfileResponseDto responseDto = userService.findProfile(id); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @PatchMapping("/{id}") + public ResponseEntity updateProfile( + @PathVariable Long id, + @Valid @RequestBody ProfileUpdateRequestDto requestDto) { + ProfileResponseDto responseDto = userService.updateProfile(id, requestDto.getNewNickname()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @PatchMapping("/{id}/password") + public ResponseEntity updatePassword( + @PathVariable Long id, + @Valid @RequestBody PasswordUpdateRequestDto requestDto) { + + userService.updatePassword(id, requestDto.getNewPassword()); + return new ResponseEntity<>("비밀번호가 변경되었습니다.", HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java new file mode 100644 index 0000000..1cdf6c6 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class PasswordUpdateRequestDto { + + @NotBlank + private String oldPassword; + + @NotBlank +// @Size(min = 8, message = "비밀번호는 8글자 이상으로 입력해주세요.") +// @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자 최소 1글자씩 입력해주세요.") + private String newPassword; + +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java b/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java new file mode 100644 index 0000000..0e4bdcf --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.domain.user.dto; + +import com.example.gamemate.domain.user.entity.User; +import lombok.Getter; + +@Getter +public class ProfileResponseDto { + + private Long id; + private String email; + private String name; + private String nickname; + private Boolean is_premium; + + public ProfileResponseDto(User user) { + this.id = user.getId(); + this.email = user.getEmail(); + this.name = user.getName(); + this.nickname = user.getNickname(); + this.is_premium = user.getIsPremium(); + } +} diff --git a/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java new file mode 100644 index 0000000..76b6696 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.user.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class ProfileUpdateRequestDto { + + @NotBlank + private String newNickname; + +} diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java new file mode 100644 index 0000000..ba33575 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -0,0 +1,65 @@ +package com.example.gamemate.domain.user.entity; + +import com.example.gamemate.global.common.BaseEntity; +import com.example.gamemate.domain.user.enums.Authority; +import com.example.gamemate.domain.user.enums.UserStatus; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user") +@Getter +@NoArgsConstructor +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Authority auth; + + private Boolean isPremium; + + @Enumerated(EnumType.STRING) + private UserStatus userStatus; + + public User(String email, String name, String nickname, String password) { + this.email = email; + this.name = name; + this.nickname = nickname; + this.password = password; + this.auth = Authority.USER; + this.isPremium = false; + this.userStatus = UserStatus.ACTIVE; + } + + public void updatePassword(String newPassword) { + this.password = newPassword; + } + + public void updateProfile(String newNickname) { + this.nickname = newNickname; + } + + public void deleteSoftly() { + this.modifiedAt = LocalDateTime.now(); + } + +} diff --git a/src/main/java/com/example/gamemate/domain/user/enums/Authority.java b/src/main/java/com/example/gamemate/domain/user/enums/Authority.java new file mode 100644 index 0000000..cf2528c --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/enums/Authority.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.user.enums; + +import lombok.Getter; + +@Getter +public enum Authority { + + USER("user"), + ADMIN("admin"); + + private String name; + + Authority(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java b/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java new file mode 100644 index 0000000..bda43ce --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.user.enums; + +import lombok.Getter; + +@Getter +public enum UserStatus { + + ACTIVE("active"), + WITHDRAW("withdraw"); + + private String name; + + UserStatus(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..c6f04a7 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.gamemate.domain.user.repository; + +import com.example.gamemate.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +public interface UserRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java new file mode 100644 index 0000000..a7e9938 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -0,0 +1,47 @@ +package com.example.gamemate.domain.user.service; + +import com.example.gamemate.domain.user.dto.ProfileResponseDto; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.UserStatus; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.swing.*; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public ProfileResponseDto findProfile(Long id) { + User findUser = userRepository.findById(id).orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if(UserStatus.WITHDRAW.equals(findUser.getUserStatus())) { + throw new ApiException(ErrorCode.USER_WITHDRAWN); + } + return new ProfileResponseDto(findUser); + } + + public ProfileResponseDto updateProfile(Long id, String newNickname) { + User findUser = userRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + findUser.updateProfile(newNickname); + User savedUser = userRepository.save(findUser); + + return new ProfileResponseDto(savedUser); + } + + public void updatePassword(Long id, String newPassword) { + User findUser = userRepository.findById(id).orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + //Todo 비밀번호 검증 로직 Spring Security로 구현 + + findUser.updatePassword(newPassword); + User savedUser = userRepository.save(findUser); + } + +} diff --git a/src/main/java/com/example/gamemate/global/common/BaseEntity.java b/src/main/java/com/example/gamemate/global/common/BaseEntity.java new file mode 100644 index 0000000..76f3d52 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/common/BaseEntity.java @@ -0,0 +1,25 @@ +package com.example.gamemate.global.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + protected LocalDateTime modifiedAt; + +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index fb0ab78..20dd27c 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -15,6 +15,10 @@ public enum ErrorCode { UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + /* 404 찾을 수 없음 */ + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), + USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), + /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 8b70b2499179e00f912d0bd9e5bbf79b0425eabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 7 Jan 2025 19:49:11 +0900 Subject: [PATCH 009/215] =?UTF-8?q?feat:=20SwaggerConfig=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 - SpringBoot 3.4.1 - SpringDoc 2.7.0 http://localhost:8080/swagger-ui/index.html#/ --- .../gamemate/global/config/SwaggerConfig.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/com/example/gamemate/global/config/SwaggerConfig.java diff --git a/src/main/java/com/example/gamemate/global/config/SwaggerConfig.java b/src/main/java/com/example/gamemate/global/config/SwaggerConfig.java new file mode 100644 index 0000000..88ef8dc --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package com.example.gamemate.global.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + + Info info = new Info() + .title("Game Mate") + .version("v1.0.0") + .description("Game Mate REST API"); + + return new OpenAPI() + .info(info); + } +} \ No newline at end of file From ec52e016a87fb1eab7dfe2abd2b6ae053f65b557 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 19:59:32 +0900 Subject: [PATCH 010/215] =?UTF-8?q?feat=20:=20=ED=8C=94=EB=A1=9C=EC=9B=8C?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/follow/Follow.java | 4 ++-- .../domain/follow/FollowController.java | 17 +++++++++++--- .../domain/follow/FollowRepository.java | 4 ++++ .../gamemate/domain/follow/FollowService.java | 23 ++++++++++++++++--- .../follow/dto/FollowFindRequestDto.java | 12 ++++++++++ .../follow/dto/FollowFindResponseDto.java | 19 +++++++++++++++ 6 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/Follow.java b/src/main/java/com/example/gamemate/domain/follow/Follow.java index b5bde89..e33dba8 100644 --- a/src/main/java/com/example/gamemate/domain/follow/Follow.java +++ b/src/main/java/com/example/gamemate/domain/follow/Follow.java @@ -11,11 +11,11 @@ public class Follow extends BaseCreatedEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "follower_id", nullable = false) private User follower; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "followee_id", nullable = false) private User followee; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index 8a88571..4edf2e1 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -1,13 +1,13 @@ package com.example.gamemate.domain.follow; -import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; -import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; -import com.example.gamemate.domain.follow.dto.FollowDeleteResponseDto; +import com.example.gamemate.domain.follow.dto.*; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/follows") @@ -36,4 +36,15 @@ public ResponseEntity deleteFollow(@PathVariable Long f FollowDeleteResponseDto followDeleteResponseDto = followService.deleteFollow(followId); return new ResponseEntity<>(followDeleteResponseDto,HttpStatus.OK); } + + /** + * 팔로우 목록 보기 + * @param dto FollowFindRequestDto + * @return followerList + */ + @GetMapping("/follower-list") + public ResponseEntity> findFollowerList(@RequestBody FollowFindRequestDto dto) { + List followerList = followService.findFollowerList(dto); + return new ResponseEntity<>(followerList, HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java index b281c34..46d4ee9 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java @@ -3,7 +3,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface FollowRepository extends JpaRepository { Boolean existsByFollowerAndFollowee(User follower, User followee); + List findByFollowee(User followee); + List findByFollower(User follower); } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 0b7e88f..d6d97ec 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -1,14 +1,14 @@ package com.example.gamemate.domain.follow; -import com.example.gamemate.domain.follow.dto.FollowCreateRequestDto; -import com.example.gamemate.domain.follow.dto.FollowCreateResponseDto; -import com.example.gamemate.domain.follow.dto.FollowDeleteResponseDto; +import com.example.gamemate.domain.follow.dto.*; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; @Service @@ -54,4 +54,21 @@ public FollowDeleteResponseDto deleteFollow(Long followId) { return new FollowDeleteResponseDto("팔로우가 취소되었습니다."); } + + // 팔로워 목록보기 + public List findFollowerList(FollowFindRequestDto dto) { + User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + List FollowListByFollowee = followRepository.findByFollowee(followee); + + List FollowerListByFollowee = new ArrayList<>(); + + for (Follow follow : FollowListByFollowee) { + FollowerListByFollowee.add(follow.getFollower()); + } + + return FollowerListByFollowee + .stream() + .map(FollowFindResponseDto::toDto) + .toList(); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java new file mode 100644 index 0000000..5bffdb3 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowFindRequestDto { + private String email; + + public FollowFindRequestDto(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java new file mode 100644 index 0000000..151c07d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.follow.dto; + +import com.example.gamemate.domain.follow.User; +import lombok.Getter; + +@Getter +public class FollowFindResponseDto { + private Long id; + private String nickname; + + public FollowFindResponseDto(Long id, String nickname) { + this.id = id; + this.nickname = nickname; + } + + public static FollowFindResponseDto toDto(User user) { + return new FollowFindResponseDto(user.getId(), user.getNickname()); + } +} From 6693e496d95c02f1ca49c9a6d45680660d559b58 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 20:16:14 +0900 Subject: [PATCH 011/215] =?UTF-8?q?feat=20:=20=ED=8C=94=EB=A1=9C=EC=9E=89?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/follow/FollowController.java | 13 ++++++++++++- .../gamemate/domain/follow/FollowService.java | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index 4edf2e1..1d0baef 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -38,7 +38,7 @@ public ResponseEntity deleteFollow(@PathVariable Long f } /** - * 팔로우 목록 보기 + * 팔로워 목록 보기 * @param dto FollowFindRequestDto * @return followerList */ @@ -47,4 +47,15 @@ public ResponseEntity> findFollowerList(@RequestBody List followerList = followService.findFollowerList(dto); return new ResponseEntity<>(followerList, HttpStatus.OK); } + + /** + * 팔로잉 목록 보기 + * @param dto FollowFindRequestDto + * @return followeeList + */ + @GetMapping("/following-list") + public ResponseEntity> findFollowingList(@RequestBody FollowFindRequestDto dto) { + List followingList = followService.findFollowingList(dto); + return new ResponseEntity<>(followingList, HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index d6d97ec..fab95dc 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -71,4 +71,21 @@ public List findFollowerList(FollowFindRequestDto dto) { .map(FollowFindResponseDto::toDto) .toList(); } + + // 팔로잉 목록보기 + public List findFollowingList(FollowFindRequestDto dto) { + User follower = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + List FollowListByFollower = followRepository.findByFollower(follower); + + List FollowingListByFollower = new ArrayList<>(); + + for (Follow follow : FollowListByFollower) { + FollowingListByFollower.add(follow.getFollowee()); + } + + return FollowingListByFollower + .stream() + .map(FollowFindResponseDto::toDto) + .toList(); + } } From a2d4065d51a9c5440602ed03cb95335815d65732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 7 Jan 2025 20:36:45 +0900 Subject: [PATCH 012/215] =?UTF-8?q?fix:=20deleteSoftly=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gamemate/domain/user/entity/User.java | 2 +- .../java/com/example/gamemate/global/common/BaseEntity.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index ba33575..36e2a56 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -59,7 +59,7 @@ public void updateProfile(String newNickname) { } public void deleteSoftly() { - this.modifiedAt = LocalDateTime.now(); + markDeletedAt(); } } diff --git a/src/main/java/com/example/gamemate/global/common/BaseEntity.java b/src/main/java/com/example/gamemate/global/common/BaseEntity.java index 76f3d52..f1a29c6 100644 --- a/src/main/java/com/example/gamemate/global/common/BaseEntity.java +++ b/src/main/java/com/example/gamemate/global/common/BaseEntity.java @@ -20,6 +20,10 @@ public abstract class BaseEntity { private LocalDateTime createdAt; @LastModifiedDate - protected LocalDateTime modifiedAt; + private LocalDateTime modifiedAt; + + public void markDeletedAt() { + this.modifiedAt = LocalDateTime.now(); + } } From d0f901a5ae390ffe3519365583fa4cbf176f0da2 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 20:48:32 +0900 Subject: [PATCH 013/215] =?UTF-8?q?refactor=20:=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=EC=9D=98=20Get=20=EC=9A=94=EC=B2=AD=EC=8B=9C=20Reques?= =?UTF-8?q?tBody=20=EB=A5=BC=20=EB=B0=9B=EB=8D=98=20=EA=B2=83=EC=9D=84=20R?= =?UTF-8?q?equestParam=20=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restful API 설계 원칙에 맞게 수정, 사용하지 않는 FollowFindRequestDto 삭제 --- .../example/gamemate/domain/follow/Follow.java | 2 +- .../gamemate/domain/follow/FollowController.java | 8 ++++---- .../gamemate/domain/follow/FollowService.java | 16 ++++++++-------- .../domain/follow/dto/FollowFindRequestDto.java | 12 ------------ .../gamemate/global/constant/ErrorCode.java | 4 ++-- 5 files changed, 15 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/Follow.java b/src/main/java/com/example/gamemate/domain/follow/Follow.java index e33dba8..7546fbf 100644 --- a/src/main/java/com/example/gamemate/domain/follow/Follow.java +++ b/src/main/java/com/example/gamemate/domain/follow/Follow.java @@ -1,6 +1,6 @@ package com.example.gamemate.domain.follow; -import com.example.gamemate.global.BaseCreatedEntity; +import com.example.gamemate.global.common.BaseCreatedEntity; import jakarta.persistence.*; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index 1d0baef..0b94bb7 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -43,8 +43,8 @@ public ResponseEntity deleteFollow(@PathVariable Long f * @return followerList */ @GetMapping("/follower-list") - public ResponseEntity> findFollowerList(@RequestBody FollowFindRequestDto dto) { - List followerList = followService.findFollowerList(dto); + public ResponseEntity> findFollowerList(@RequestParam String email) { + List followerList = followService.findFollowerList(email); return new ResponseEntity<>(followerList, HttpStatus.OK); } @@ -54,8 +54,8 @@ public ResponseEntity> findFollowerList(@RequestBody * @return followeeList */ @GetMapping("/following-list") - public ResponseEntity> findFollowingList(@RequestBody FollowFindRequestDto dto) { - List followingList = followService.findFollowingList(dto); + public ResponseEntity> findFollowingList(@RequestParam String email) { + List followingList = followService.findFollowingList(email); return new ResponseEntity<>(followingList, HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index fab95dc..6e00f5c 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -21,8 +21,8 @@ public class FollowService { // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { - User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); - User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followRepository.existsByFollowerAndFollowee(follower, followee)) { throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); @@ -42,9 +42,9 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional public FollowDeleteResponseDto deleteFollow(Long followId) { - Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_FOLLOW)); + Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); - User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (findFollow.getFollower() != follower) { throw new ApiException(ErrorCode.INVALID_INPUT); @@ -56,8 +56,8 @@ public FollowDeleteResponseDto deleteFollow(Long followId) { } // 팔로워 목록보기 - public List findFollowerList(FollowFindRequestDto dto) { - User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + public List findFollowerList(String email) { + User followee = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); List FollowListByFollowee = followRepository.findByFollowee(followee); List FollowerListByFollowee = new ArrayList<>(); @@ -73,8 +73,8 @@ public List findFollowerList(FollowFindRequestDto dto) { } // 팔로잉 목록보기 - public List findFollowingList(FollowFindRequestDto dto) { - User follower = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_USER)); + public List findFollowingList(String email) { + User follower = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); List FollowListByFollower = followRepository.findByFollower(follower); List FollowingListByFollower = new ArrayList<>(); diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java deleted file mode 100644 index 5bffdb3..0000000 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindRequestDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.gamemate.domain.follow.dto; - -import lombok.Getter; - -@Getter -public class FollowFindRequestDto { - private String email; - - public FollowFindRequestDto(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 8dc221b..fc59d06 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -17,8 +17,8 @@ public enum ErrorCode { NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), /* 404 찾을 수 없음 */ - NOT_FOUND_USER(HttpStatus.NOT_FOUND, "NOT_FOUND_USER", "유저를 찾을 수 없습니다."), - NOT_FOUND_FOLLOW(HttpStatus.NOT_FOUND,"NOT_FOUND_FOLLOW", "팔로우를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), + FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND,"FOLLOW_NOT_FOUND", "팔로우를 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From d4326e9cbd55e1c183145f17fab3a8dd57ff2950 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 7 Jan 2025 20:48:40 +0900 Subject: [PATCH 014/215] =?UTF-8?q?feat=20:=20BaseCreatedEntity=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 생성일만 추가해주는 BaseCreatedEntity 추가 --- .../global/common/BaseCreatedEntity.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/example/gamemate/global/common/BaseCreatedEntity.java diff --git a/src/main/java/com/example/gamemate/global/common/BaseCreatedEntity.java b/src/main/java/com/example/gamemate/global/common/BaseCreatedEntity.java new file mode 100644 index 0000000..0b9201e --- /dev/null +++ b/src/main/java/com/example/gamemate/global/common/BaseCreatedEntity.java @@ -0,0 +1,21 @@ +package com.example.gamemate.global.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseCreatedEntity { + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + +} From b0bf55bc5adf10cba33c86d35cac1e980cf3827d Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:40:50 +0900 Subject: [PATCH 015/215] =?UTF-8?q?init:=20=EC=BB=A4=EB=B0=8B=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 1. 커밋 테스트 --- .../example/gamemate/game/entity/Game.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/example/gamemate/game/entity/Game.java diff --git a/src/main/java/com/example/gamemate/game/entity/Game.java b/src/main/java/com/example/gamemate/game/entity/Game.java new file mode 100644 index 0000000..ba3add8 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/entity/Game.java @@ -0,0 +1,43 @@ +package com.example.gamemate.game.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Table(name = "game") +@AllArgsConstructor +public class Game { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title", nullable = false, length = 255 ,unique = true) + private String title; + + @Column(name = "genre", nullable = false, length = 10) + private String genre; + + @Column(name = "description", nullable = false, length = 255) + private String description; + + @Column(name = "platform", nullable = false, length = 255) + private String platform; + + @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) + private List gameImages = new ArrayList<>(); + + public Game(String title, String genre, String description, String platform) { + this.title = title; + this.genre = genre; + this.description = description; + this.platform = platform; + } + +} From ecabd86d18209345bfac0431e94fcd5de28ab19b Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:18:50 +0900 Subject: [PATCH 016/215] =?UTF-8?q?init:=20add=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=B3=B8=20CRUD=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 1. 게임 등록 기능(기본) 2. 게임 조회(다건/단건) 기능(기본) 3. 게임 수정 기능(기본) 4. 게임 삭제 기능(기본) --- .../game/controller/GameController.java | 80 +++++++++++++++++++ .../game/dto/GameCreateRequestDto.java | 19 +++++ .../game/dto/GameCreateResponseDto.java | 22 +++++ .../game/dto/GameFindAllResponseDto.java | 20 +++++ .../game/dto/GameFindByResponseDto.java | 22 +++++ .../game/dto/GameUpdateRequestDto.java | 19 +++++ .../game/dto/GameUpdateResponseDto.java | 22 +++++ .../example/gamemate/game/entity/Game.java | 15 +++- .../game/repository/GameRepository.java | 21 +++++ .../gamemate/game/service/GameService.java | 69 ++++++++++++++++ 10 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/gamemate/game/controller/GameController.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/repository/GameRepository.java create mode 100644 src/main/java/com/example/gamemate/game/service/GameService.java diff --git a/src/main/java/com/example/gamemate/game/controller/GameController.java b/src/main/java/com/example/gamemate/game/controller/GameController.java new file mode 100644 index 0000000..f3d4ec1 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/controller/GameController.java @@ -0,0 +1,80 @@ +package com.example.gamemate.game.controller; + +import com.example.gamemate.game.dto.*; +import com.example.gamemate.game.service.GameService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/games") +public class GameController { + private final GameService gameService; + + @Autowired + public GameController(GameService gameService) { + this.gameService = gameService; + } + + /** + * 게임생성 + * + * @param gameCreateRequestDto + * @return + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createGame(@RequestBody GameCreateRequestDto gameCreateRequestDto) { + + GameCreateResponseDto responseDto = gameService.createGame(gameCreateRequestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } + + /** + * 게임 전체 조회 + * + * @param page + * @param szie + * @return + */ + @GetMapping + public ResponseEntity> findAllGame(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int szie) { + + Page games = gameService.findAllGame(page, szie); + return ResponseEntity.ok(games); + } + + /** + * 게임 단건 조회 + * + * @param id + * @return + */ + @GetMapping("/{id}") + public ResponseEntity findGameById(@PathVariable Long id) { + + GameFindByResponseDto gameById = gameService.findGameById(id); + return ResponseEntity.ok(gameById); + } + + /** + * 게임 정보 수정 + * @param id + * @param requestDto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateGame(@PathVariable Long id, @RequestBody GameUpdateRequestDto requestDto){ + + GameUpdateResponseDto responseDto = gameService.updateGame(id,requestDto); + return ResponseEntity.ok(responseDto); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteGame(@PathVariable Long id){ + gameService.deleteGame(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java b/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java new file mode 100644 index 0000000..050effd --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.game.dto; + +import lombok.Getter; + +@Getter +public class GameCreateRequestDto { + private String title; + private String genre; + private String platform; + private String description; + + + public GameCreateRequestDto(String title, String genre, String platform , String description) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java new file mode 100644 index 0000000..bec4451 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import lombok.Getter; + +@Getter +public class GameCreateResponseDto { + private Long id; + private String title; + private String genre; + private String platform; + private String description; + + public GameCreateResponseDto(Game game) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + this.description = game.getDescription(); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java new file mode 100644 index 0000000..1a5a988 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import lombok.Getter; + +@Getter +public class GameFindAllResponseDto { + private final Long id; + private final String title; + private final String genre; + private final String platform; + + public GameFindAllResponseDto(Game game) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java new file mode 100644 index 0000000..403ab48 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import lombok.Getter; + +@Getter +public class GameFindByResponseDto { + private final Long id; + private final String title; + private final String genre; + private final String platform; + private final String description; + + public GameFindByResponseDto(Game game) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + this.description = game.getDescription(); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java b/src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java new file mode 100644 index 0000000..671dca6 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.game.dto; + +import lombok.Getter; + +@Getter +public class GameUpdateRequestDto { + private String title; + private String genre; + private String platform; + private String description; + + + public GameUpdateRequestDto(String title, String genre, String platform , String description) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java new file mode 100644 index 0000000..23f81e4 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import lombok.Getter; + +@Getter +public class GameUpdateResponseDto { + private Long id; + private String title; + private String genre; + private String platform; + private String description; + + public GameUpdateResponseDto(Game game) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + this.description = game.getDescription(); + } +} diff --git a/src/main/java/com/example/gamemate/game/entity/Game.java b/src/main/java/com/example/gamemate/game/entity/Game.java index ba3add8..006bf21 100644 --- a/src/main/java/com/example/gamemate/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/game/entity/Game.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import java.time.LocalDateTime; @@ -13,12 +14,13 @@ @Getter @Table(name = "game") @AllArgsConstructor +@NoArgsConstructor public class Game { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "title", nullable = false, length = 255 ,unique = true) + @Column(name = "title", nullable = false, length = 255 ,unique = false) private String title; @Column(name = "genre", nullable = false, length = 10) @@ -33,11 +35,20 @@ public class Game { @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) private List gameImages = new ArrayList<>(); - public Game(String title, String genre, String description, String platform) { + public Game(String title, String genre, String platform, String description) { this.title = title; this.genre = genre; + this.platform = platform; this.description = description; + } + + public void updateGame(String title, String genre, String platform, String description){ + this.title = title; + this.genre = genre; this.platform = platform; + this.description = description; } + + } diff --git a/src/main/java/com/example/gamemate/game/repository/GameRepository.java b/src/main/java/com/example/gamemate/game/repository/GameRepository.java new file mode 100644 index 0000000..05edfb4 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/repository/GameRepository.java @@ -0,0 +1,21 @@ +package com.example.gamemate.game.repository; + +import com.example.gamemate.game.entity.Game; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface GameRepository extends JpaRepository { + // 게임 이름으로 게임 찾기 + List findGameByTitle(String title); + + // 플랫폼별 게임 찾기 + List findGamesByPlatform(String platform); + + // 장르로 게임 찾기 + List findGameByGenre(String genre); + + // 아이디로 게임 찾기 + OptionalfindGameById(Long id); +} diff --git a/src/main/java/com/example/gamemate/game/service/GameService.java b/src/main/java/com/example/gamemate/game/service/GameService.java new file mode 100644 index 0000000..979ac39 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/service/GameService.java @@ -0,0 +1,69 @@ +package com.example.gamemate.game.service; + +import com.example.gamemate.game.dto.*; +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.game.repository.GameRepository; +import org.springframework.beans.factory.annotation.Autowired; +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 javax.ws.rs.NotFoundException; + + +@Service +public class GameService { + private final GameRepository gameRepository; + + @Autowired + public GameService(GameRepository gameRepository) { + + this.gameRepository = gameRepository; + } + + public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto) { + + Game game = new Game( + gameCreateRequestDto.getTitle(), + gameCreateRequestDto.getGenre(), + gameCreateRequestDto.getPlatform(), + gameCreateRequestDto.getDescription() + ); + Game savedGame = gameRepository.save(game); + return new GameCreateResponseDto(savedGame); + + } + + public Page findAllGame(int page, int size){ + + Pageable pageable = PageRequest.of(page,size); + return gameRepository.findAll(pageable).map(GameFindAllResponseDto::new); + } + + @Transactional + public GameFindByResponseDto findGameById(Long id){ + Game game = gameRepository.findGameById(id).orElseThrow(()->new NotFoundException("게임이 존재하지 않습니다.")); + return new GameFindByResponseDto(game); + } + + @Transactional + public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto){ + Game game = gameRepository.findGameById(id).orElseThrow(()-> new NotFoundException("게임이 존해 하지 않습니다.")); + + game.updateGame( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + Game updateGame = gameRepository.save(game); + return new GameUpdateResponseDto(updateGame); + } + + public void deleteGame(Long id){ + Game game = gameRepository.findGameById(id).orElseThrow(()-> new NotFoundException("게임을 찾을 없습니다.")); + gameRepository.delete(game); + } +} From cb13abdc891c694f5e25915660ffd20844d696af Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 8 Jan 2025 14:39:05 +0900 Subject: [PATCH 017/215] =?UTF-8?q?fix:=20BaseEntity=20@EnableJpaAuditing?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. @EnableJpaAuditing 설정 --- src/main/java/com/example/gamemate/GameMateApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/gamemate/GameMateApplication.java b/src/main/java/com/example/gamemate/GameMateApplication.java index 80b6a71..43d76c7 100644 --- a/src/main/java/com/example/gamemate/GameMateApplication.java +++ b/src/main/java/com/example/gamemate/GameMateApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class GameMateApplication { public static void main(String[] args) { From 49bc65afbc0ac46af92822f74fe5e6080e00285f Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 8 Jan 2025 15:53:25 +0900 Subject: [PATCH 018/215] =?UTF-8?q?fix=20:=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 영속성 전이 및 고아 객체 삭제 제거 2. 객체 기준으로 follow 엔티티에서 follower 를 맵핑 받은 List 는 그 User 가 팔로우한 User 목록의 정보를 담고있으니 followingList 로 변경 3. 객체 기준으로 follow 엔티티에서 followee 를 맵핑 받은 List 는 그 User 를 팔로우한 User 목록의 정보를 담고있으니 followerList 로 변경 --- build.gradle | 2 +- .../com/example/gamemate/domain/user/entity/User.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c2b577f..f6660dd 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' +// implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index 36e2a56..a9a425f 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.user.entity; +import com.example.gamemate.domain.follow.Follow; import com.example.gamemate.global.common.BaseEntity; import com.example.gamemate.domain.user.enums.Authority; import com.example.gamemate.domain.user.enums.UserStatus; @@ -8,6 +9,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; @Entity @Table(name = "user") @@ -40,6 +42,12 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private UserStatus userStatus; + @OneToMany(mappedBy = "follower") + private List followingList; + + @OneToMany(mappedBy = "followee") + private List followerList; + public User(String email, String name, String nickname, String password) { this.email = email; this.name = name; From 5f00d94ba69c890a0f5cb6c244c9bfb90888c70c Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 8 Jan 2025 15:56:26 +0900 Subject: [PATCH 019/215] =?UTF-8?q?refactor=20:=20=EB=A8=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=80=20User=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20Follow=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=8A=B8?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gamemate/domain/follow/Follow.java | 1 + .../example/gamemate/domain/follow/FollowController.java | 8 ++++---- .../example/gamemate/domain/follow/FollowRepository.java | 1 + .../com/example/gamemate/domain/follow/FollowService.java | 4 +++- .../gamemate/domain/follow/dto/FollowFindResponseDto.java | 2 +- .../gamemate/domain/user/repository/UserRepository.java | 5 +++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/Follow.java b/src/main/java/com/example/gamemate/domain/follow/Follow.java index 7546fbf..dcf7ae3 100644 --- a/src/main/java/com/example/gamemate/domain/follow/Follow.java +++ b/src/main/java/com/example/gamemate/domain/follow/Follow.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.follow; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseCreatedEntity; import jakarta.persistence.*; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index 0b94bb7..c99fc8b 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -38,8 +38,8 @@ public ResponseEntity deleteFollow(@PathVariable Long f } /** - * 팔로워 목록 보기 - * @param dto FollowFindRequestDto + * 팔로우 목록 보기 + * @param email 팔로우 목록을 보고 싶은 유저 email * @return followerList */ @GetMapping("/follower-list") @@ -50,8 +50,8 @@ public ResponseEntity> findFollowerList(@RequestPara /** * 팔로잉 목록 보기 - * @param dto FollowFindRequestDto - * @return followeeList + * @param email 팔로잉 목록을 보고 싶은 유저 email + * @return followingList */ @GetMapping("/following-list") public ResponseEntity> findFollowingList(@RequestParam String email) { diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java index 46d4ee9..c0973f4 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.follow; +import com.example.gamemate.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 6e00f5c..ca0ffa1 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -1,6 +1,8 @@ package com.example.gamemate.domain.follow; import com.example.gamemate.domain.follow.dto.*; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; @@ -32,7 +34,7 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { throw new ApiException(ErrorCode.INVALID_INPUT); } - Follow follow = new Follow(follower,followee); + Follow follow = new Follow(follower, followee); followRepository.save(follow); return new FollowCreateResponseDto("팔로우 했습니다."); diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java index 151c07d..6013f2e 100644 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowFindResponseDto.java @@ -1,6 +1,6 @@ package com.example.gamemate.domain.follow.dto; -import com.example.gamemate.domain.follow.User; +import com.example.gamemate.domain.user.entity.User; import lombok.Getter; @Getter diff --git a/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java index c6f04a7..d07278c 100644 --- a/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java @@ -2,8 +2,9 @@ import com.example.gamemate.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -public interface UserRepository extends JpaRepository { +import java.util.Optional; +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } From 6b89a4c0307af32ab35faaed8ac994c9b7c56249 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 8 Jan 2025 16:55:48 +0900 Subject: [PATCH 020/215] =?UTF-8?q?feat=20:=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/follow/FollowController.java | 12 ++++++++++++ .../gamemate/domain/follow/FollowService.java | 14 +++++++++++++- .../domain/follow/dto/FollowStatusResponseDto.java | 12 ++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index c99fc8b..a139955 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -37,6 +37,18 @@ public ResponseEntity deleteFollow(@PathVariable Long f return new ResponseEntity<>(followDeleteResponseDto,HttpStatus.OK); } + /** + * 팔로우 상태 확인 (follower 가 followee 를 팔로우 했는지 확인) + * @param followerEmail + * @param followeeEmail + * @return message = "팔로우 중 입니다." or "아직 팔로우 하지 않았습니다." + */ + @GetMapping("/status") + public ResponseEntity findFollow(@RequestParam String followerEmail, @RequestParam String followeeEmail) { + FollowStatusResponseDto followStatusResponseDto = followService.findFollow(followerEmail, followeeEmail); + return new ResponseEntity<>(followStatusResponseDto, HttpStatus.OK); + } + /** * 팔로우 목록 보기 * @param email 팔로우 목록을 보고 싶은 유저 email diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index ca0ffa1..ad1347d 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -45,7 +46,6 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { @Transactional public FollowDeleteResponseDto deleteFollow(Long followId) { Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); - User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (findFollow.getFollower() != follower) { @@ -57,6 +57,18 @@ public FollowDeleteResponseDto deleteFollow(Long followId) { return new FollowDeleteResponseDto("팔로우가 취소되었습니다."); } + // 팔로우 상태 확인 + public FollowStatusResponseDto findFollow(String followerEmail, String followeeEmail) { + User follower = userRepository.findByEmail(followerEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User followee = userRepository.findByEmail(followeeEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if (!followRepository.existsByFollowerAndFollowee(follower, followee)) { + return new FollowStatusResponseDto("아직 팔로우 하지 않았습니다."); + } + + return new FollowStatusResponseDto("팔로우 중 입니다."); + } + // 팔로워 목록보기 public List findFollowerList(String email) { User followee = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java new file mode 100644 index 0000000..cdaf4b1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowStatusResponseDto { + private String message; + + public FollowStatusResponseDto(String message) { + this.message = message; + } +} From fdc4eb92c8331dc8ad75bc0a8c4f4b0a0cf60973 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 8 Jan 2025 16:59:49 +0900 Subject: [PATCH 021/215] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=82=B4=EC=9A=A9=EC=9D=98=20Dto=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FollowDeleteResponseDto, FollowCreateResponseDto, FollowStatusResponseDto 를 FollowResponseDto 로 통합 --- .../domain/follow/FollowController.java | 18 +++++++++--------- .../gamemate/domain/follow/FollowService.java | 15 +++++++-------- .../follow/dto/FollowDeleteResponseDto.java | 12 ------------ ...ResponseDto.java => FollowResponseDto.java} | 4 ++-- .../follow/dto/FollowStatusResponseDto.java | 12 ------------ 5 files changed, 18 insertions(+), 43 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java rename src/main/java/com/example/gamemate/domain/follow/dto/{FollowCreateResponseDto.java => FollowResponseDto.java} (61%) delete mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/FollowController.java index a139955..23896a4 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowController.java @@ -21,9 +21,9 @@ public class FollowController { * @return message = "팔로우 했습니다." */ @PostMapping - public ResponseEntity createFollow(@RequestBody FollowCreateRequestDto dto) { - FollowCreateResponseDto followCreateResponseDto = followService.createFollow(dto); - return new ResponseEntity<>(followCreateResponseDto, HttpStatus.CREATED); + public ResponseEntity createFollow(@RequestBody FollowCreateRequestDto dto) { + FollowResponseDto followResponseDto = followService.createFollow(dto); + return new ResponseEntity<>(followResponseDto, HttpStatus.CREATED); } /** @@ -32,9 +32,9 @@ public ResponseEntity createFollow(@RequestBody FollowC * @return message = "팔로우를 취소했습니다." */ @DeleteMapping("/{followId}") - public ResponseEntity deleteFollow(@PathVariable Long followId) { - FollowDeleteResponseDto followDeleteResponseDto = followService.deleteFollow(followId); - return new ResponseEntity<>(followDeleteResponseDto,HttpStatus.OK); + public ResponseEntity deleteFollow(@PathVariable Long followId) { + FollowResponseDto followResponseDto = followService.deleteFollow(followId); + return new ResponseEntity<>(followResponseDto,HttpStatus.OK); } /** @@ -44,9 +44,9 @@ public ResponseEntity deleteFollow(@PathVariable Long f * @return message = "팔로우 중 입니다." or "아직 팔로우 하지 않았습니다." */ @GetMapping("/status") - public ResponseEntity findFollow(@RequestParam String followerEmail, @RequestParam String followeeEmail) { - FollowStatusResponseDto followStatusResponseDto = followService.findFollow(followerEmail, followeeEmail); - return new ResponseEntity<>(followStatusResponseDto, HttpStatus.OK); + public ResponseEntity findFollow(@RequestParam String followerEmail, @RequestParam String followeeEmail) { + FollowResponseDto followResponseDto = followService.findFollow(followerEmail, followeeEmail); + return new ResponseEntity<>(followResponseDto, HttpStatus.OK); } /** diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index ad1347d..2f689a2 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -23,7 +22,7 @@ public class FollowService { // 팔로우하기 // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { + public FollowResponseDto createFollow(FollowCreateRequestDto dto) { User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -38,13 +37,13 @@ public FollowCreateResponseDto createFollow(FollowCreateRequestDto dto) { Follow follow = new Follow(follower, followee); followRepository.save(follow); - return new FollowCreateResponseDto("팔로우 했습니다."); + return new FollowResponseDto("팔로우 했습니다."); } // 팔로우 취소하기 // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public FollowDeleteResponseDto deleteFollow(Long followId) { + public FollowResponseDto deleteFollow(Long followId) { Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -54,19 +53,19 @@ public FollowDeleteResponseDto deleteFollow(Long followId) { followRepository.delete(findFollow); - return new FollowDeleteResponseDto("팔로우가 취소되었습니다."); + return new FollowResponseDto("팔로우가 취소되었습니다."); } // 팔로우 상태 확인 - public FollowStatusResponseDto findFollow(String followerEmail, String followeeEmail) { + public FollowResponseDto findFollow(String followerEmail, String followeeEmail) { User follower = userRepository.findByEmail(followerEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(followeeEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (!followRepository.existsByFollowerAndFollowee(follower, followee)) { - return new FollowStatusResponseDto("아직 팔로우 하지 않았습니다."); + return new FollowResponseDto("아직 팔로우 하지 않았습니다."); } - return new FollowStatusResponseDto("팔로우 중 입니다."); + return new FollowResponseDto("팔로우 중 입니다."); } // 팔로워 목록보기 diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java deleted file mode 100644 index 34a10e4..0000000 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowDeleteResponseDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.gamemate.domain.follow.dto; - -import lombok.Getter; - -@Getter -public class FollowDeleteResponseDto { - private String message; - - public FollowDeleteResponseDto(String message) { - this.message = message; - } -} diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java similarity index 61% rename from src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java rename to src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java index 88f6d70..d16e800 100644 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java @@ -3,10 +3,10 @@ import lombok.Getter; @Getter -public class FollowCreateResponseDto { +public class FollowResponseDto { private String message; - public FollowCreateResponseDto(String message) { + public FollowResponseDto(String message) { this.message = message; } } diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java deleted file mode 100644 index cdaf4b1..0000000 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowStatusResponseDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.gamemate.domain.follow.dto; - -import lombok.Getter; - -@Getter -public class FollowStatusResponseDto { - private String message; - - public FollowStatusResponseDto(String message) { - this.message = message; - } -} From 81c4f608fb923f77983375f194734b8b820d68c0 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:17:04 +0900 Subject: [PATCH 022/215] =?UTF-8?q?feat:=20add=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=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 1. 게임 검색 기능 --- .../game/controller/GameController.java | 23 +++++++++++++-- .../game/dto/GameFindByResponseDto.java | 6 ++++ .../game/dto/GameSearchResponseDto.java | 26 +++++++++++++++++ .../example/gamemate/game/entity/Game.java | 5 ++-- .../game/repository/GameRepository.java | 28 +++++++++++++------ .../gamemate/game/service/GameService.java | 28 +++++++++++++------ 6 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java diff --git a/src/main/java/com/example/gamemate/game/controller/GameController.java b/src/main/java/com/example/gamemate/game/controller/GameController.java index f3d4ec1..64dbcc6 100644 --- a/src/main/java/com/example/gamemate/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/game/controller/GameController.java @@ -2,6 +2,7 @@ import com.example.gamemate.game.dto.*; import com.example.gamemate.game.service.GameService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; @@ -10,6 +11,7 @@ @RestController @RequestMapping("/games") +@Slf4j public class GameController { private final GameService gameService; @@ -61,20 +63,35 @@ public ResponseEntity findGameById(@PathVariable Long id) /** * 게임 정보 수정 + * * @param id * @param requestDto * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGame(@PathVariable Long id, @RequestBody GameUpdateRequestDto requestDto){ + public ResponseEntity updateGame(@PathVariable Long id, @RequestBody GameUpdateRequestDto requestDto) { - GameUpdateResponseDto responseDto = gameService.updateGame(id,requestDto); + GameUpdateResponseDto responseDto = gameService.updateGame(id, requestDto); return ResponseEntity.ok(responseDto); } @DeleteMapping("/{id}") - public ResponseEntity deleteGame(@PathVariable Long id){ + public ResponseEntity deleteGame(@PathVariable Long id) { gameService.deleteGame(id); return ResponseEntity.ok().build(); } + + @GetMapping("/search") + public ResponseEntity> searchGame(@RequestParam String keyword, + @RequestParam(required = false) String genre, + @RequestParam(required = false) String platform, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + // 파라미터 값 확인을 위한 로깅 + log.info("Search parameters - keyword: {}, genre: {}, platform: {}, page: {}, size: {}", + keyword, platform, genre, page, size); + Page games = gameService.searchGame(keyword, genre, platform, page, size); + return ResponseEntity.ok(games); + + } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java index 403ab48..3ec3a66 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java @@ -3,6 +3,8 @@ import com.example.gamemate.game.entity.Game; import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class GameFindByResponseDto { private final Long id; @@ -10,6 +12,8 @@ public class GameFindByResponseDto { private final String genre; private final String platform; private final String description; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; public GameFindByResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -18,5 +22,7 @@ public GameFindByResponseDto(Game game) { this.genre = game.getGenre(); this.platform = game.getPlatform(); this.description = game.getDescription(); + this.createdAt = game.getCreatedAt(); + this.modifiedAt = game.getModifiedAt(); } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java new file mode 100644 index 0000000..9bfb826 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java @@ -0,0 +1,26 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class GameSearchResponseDto { + private final Long id; + private final String title; + private final String genre; + private final String platform; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; + + public GameSearchResponseDto(Game game) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + this.createdAt = game.getCreatedAt(); + this.modifiedAt = game.getModifiedAt(); + } +} diff --git a/src/main/java/com/example/gamemate/game/entity/Game.java b/src/main/java/com/example/gamemate/game/entity/Game.java index 006bf21..08bf720 100644 --- a/src/main/java/com/example/gamemate/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/game/entity/Game.java @@ -1,12 +1,11 @@ package com.example.gamemate.game.entity; +import com.example.gamemate.base.BaseEntity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -15,7 +14,7 @@ @Table(name = "game") @AllArgsConstructor @NoArgsConstructor -public class Game { +public class Game extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/gamemate/game/repository/GameRepository.java b/src/main/java/com/example/gamemate/game/repository/GameRepository.java index 05edfb4..130409f 100644 --- a/src/main/java/com/example/gamemate/game/repository/GameRepository.java +++ b/src/main/java/com/example/gamemate/game/repository/GameRepository.java @@ -1,21 +1,31 @@ package com.example.gamemate.game.repository; import com.example.gamemate.game.entity.Game; +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 java.util.List; import java.util.Optional; public interface GameRepository extends JpaRepository { - // 게임 이름으로 게임 찾기 - List findGameByTitle(String title); - - // 플랫폼별 게임 찾기 - List findGamesByPlatform(String platform); - - // 장르로 게임 찾기 - List findGameByGenre(String genre); // 아이디로 게임 찾기 - OptionalfindGameById(Long id); + Optional findGameById(Long id); + + // 게임 검색 + @Query("SELECT g FROM Game g WHERE " + + "(:keyword IS NULL OR " + + "LOWER(g.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(g.description) LIKE LOWER(CONCAT('%', :keyword, '%'))) " + + "AND (:genre IS NULL OR :genre = '' OR LOWER(g.genre) LIKE LOWER(CONCAT('%', :genre, '%'))) " + + "AND (:platform IS NULL OR :platform = '' OR LOWER(g.platform) LIKE LOWER(CONCAT('%', :platform, '%')))") + Page searchGames( + @Param("keyword") String keyword, + @Param("genre") String genre, + @Param("platform") String platform, + Pageable pageable + ); } diff --git a/src/main/java/com/example/gamemate/game/service/GameService.java b/src/main/java/com/example/gamemate/game/service/GameService.java index 979ac39..62a92cf 100644 --- a/src/main/java/com/example/gamemate/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/game/service/GameService.java @@ -3,10 +3,12 @@ import com.example.gamemate.game.dto.*; import com.example.gamemate.game.entity.Game; import com.example.gamemate.game.repository.GameRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +16,7 @@ @Service +@Slf4j public class GameService { private final GameRepository gameRepository; @@ -36,21 +39,21 @@ public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDt } - public Page findAllGame(int page, int size){ + public Page findAllGame(int page, int size) { - Pageable pageable = PageRequest.of(page,size); + Pageable pageable = PageRequest.of(page, size); return gameRepository.findAll(pageable).map(GameFindAllResponseDto::new); } @Transactional - public GameFindByResponseDto findGameById(Long id){ - Game game = gameRepository.findGameById(id).orElseThrow(()->new NotFoundException("게임이 존재하지 않습니다.")); + public GameFindByResponseDto findGameById(Long id) { + Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); return new GameFindByResponseDto(game); } @Transactional - public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto){ - Game game = gameRepository.findGameById(id).orElseThrow(()-> new NotFoundException("게임이 존해 하지 않습니다.")); + public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto) { + Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); game.updateGame( requestDto.getTitle(), @@ -62,8 +65,17 @@ public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto return new GameUpdateResponseDto(updateGame); } - public void deleteGame(Long id){ - Game game = gameRepository.findGameById(id).orElseThrow(()-> new NotFoundException("게임을 찾을 없습니다.")); + public void deleteGame(Long id) { + Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); gameRepository.delete(game); } + + public Page searchGame(String keyword, String genre, String platform, int page, int size) { + + log.info("Searching games with parameters - keyword: {}, genre: {}, platform: {}", + keyword, genre, platform); + Pageable pageable = PageRequest.of(page, size); + Page games = gameRepository.searchGames(keyword, genre, platform, pageable); + return games.map(GameSearchResponseDto::new); + } } From 52d69a2166c60b993f1190972d20b108fd9e658c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 8 Jan 2025 17:35:43 +0900 Subject: [PATCH 023/215] =?UTF-8?q?fix:=20dto=20=ED=95=84=EB=93=9C=20final?= =?UTF-8?q?=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/dto/PasswordUpdateRequestDto.java | 10 ++++++---- .../gamemate/domain/user/dto/ProfileResponseDto.java | 10 +++++----- .../domain/user/dto/ProfileUpdateRequestDto.java | 6 ++++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java index 1cdf6c6..9468e34 100644 --- a/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/user/dto/PasswordUpdateRequestDto.java @@ -4,16 +4,18 @@ import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter +@RequiredArgsConstructor public class PasswordUpdateRequestDto { - @NotBlank - private String oldPassword; + @NotBlank(message = "기존 비밀번호를 입력해주세요.") + private final String oldPassword; - @NotBlank + @NotBlank(message = "새로운 비밀번호를 입력해주세요.") // @Size(min = 8, message = "비밀번호는 8글자 이상으로 입력해주세요.") // @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자 최소 1글자씩 입력해주세요.") - private String newPassword; + private final String newPassword; } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java b/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java index 0e4bdcf..9d87ac0 100644 --- a/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/user/dto/ProfileResponseDto.java @@ -6,11 +6,11 @@ @Getter public class ProfileResponseDto { - private Long id; - private String email; - private String name; - private String nickname; - private Boolean is_premium; + private final Long id; + private final String email; + private final String name; + private final String nickname; + private final Boolean is_premium; public ProfileResponseDto(User user) { this.id = user.getId(); diff --git a/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java index 76b6696..612a1f7 100644 --- a/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/user/dto/ProfileUpdateRequestDto.java @@ -2,11 +2,13 @@ import jakarta.validation.constraints.NotBlank; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter +@RequiredArgsConstructor public class ProfileUpdateRequestDto { - @NotBlank - private String newNickname; + @NotBlank(message = "새로운 닉네임을 입력해주세요.") + private final String newNickname; } From 47ed65cba496b6ed10b153a8b248b3e9c7e9829a Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 8 Jan 2025 18:27:13 +0900 Subject: [PATCH 024/215] =?UTF-8?q?fix=20:=20=ED=83=88=ED=87=B4=ED=95=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?follow=20=EA=B8=B0=EB=8A=A5=20=EB=B2=84=EA=B7=B8=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 1. 탈퇴한 사용자 팔로우 2. 탈퇴한 사용자를 팔로우했는지 여부 확인 3. 탈퇴한 사용자의 팔로잉/팔로워 목록 확인 이 가능했던 버그 수정 --- .../gamemate/domain/follow/FollowService.java | 30 ++++++++++++++++--- .../gamemate/global/constant/ErrorCode.java | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/FollowService.java index 2f689a2..7f60ba6 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/FollowService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.follow.dto.*; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; @@ -26,6 +27,10 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + if (followee.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + } + if (followRepository.existsByFollowerAndFollowee(follower, followee)) { throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); } @@ -57,10 +62,15 @@ public FollowResponseDto deleteFollow(Long followId) { } // 팔로우 상태 확인 + // todo : 로그인한 유저(follower) 기준으로 상대 유저(followee)가 팔로우 되어 있는지 확인이 필요한 것이므로, 로그인 구현시 코드 수정해야함. public FollowResponseDto findFollow(String followerEmail, String followeeEmail) { User follower = userRepository.findByEmail(followerEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(followeeEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + if (followee.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + } + if (!followRepository.existsByFollowerAndFollowee(follower, followee)) { return new FollowResponseDto("아직 팔로우 하지 않았습니다."); } @@ -71,12 +81,18 @@ public FollowResponseDto findFollow(String followerEmail, String followeeEmail) // 팔로워 목록보기 public List findFollowerList(String email) { User followee = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - List FollowListByFollowee = followRepository.findByFollowee(followee); + if (followee.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + } + + List FollowListByFollowee = followRepository.findByFollowee(followee); List FollowerListByFollowee = new ArrayList<>(); for (Follow follow : FollowListByFollowee) { - FollowerListByFollowee.add(follow.getFollower()); + if (follow.getFollower().getUserStatus() != UserStatus.WITHDRAW) { + FollowerListByFollowee.add(follow.getFollower()); + } } return FollowerListByFollowee @@ -88,12 +104,18 @@ public List findFollowerList(String email) { // 팔로잉 목록보기 public List findFollowingList(String email) { User follower = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - List FollowListByFollower = followRepository.findByFollower(follower); + if (follower.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + } + + List FollowListByFollower = followRepository.findByFollower(follower); List FollowingListByFollower = new ArrayList<>(); for (Follow follow : FollowListByFollower) { - FollowingListByFollower.add(follow.getFollowee()); + if (follow.getFollowee().getUserStatus() != UserStatus.WITHDRAW) { + FollowingListByFollower.add(follow.getFollowee()); + } } return FollowingListByFollower diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 58b5478..1c1cdf1 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -11,6 +11,7 @@ public enum ErrorCode { INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 유저입니다."), + IS_WITHDRAW_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 유저입니다."), /* 401 세션 없음 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From eca80787a74c297800f2c554be0aa70241bd1c10 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:49:37 +0900 Subject: [PATCH 025/215] =?UTF-8?q?feat:=20add=20=EB=A6=AC=EB=B7=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 1. 리뷰 생성 기능(기본) 2. 리뷰 수정 기능(기본) 3. 리뷰 삭제 기능(기본) --- .../review/controller/ReviewController.java | 68 +++++++++++++++++++ .../review/dto/ReviewCreateRequestDto.java | 24 +++++++ .../review/dto/ReviewCreateResponseDto.java | 25 +++++++ .../dto/ReviewFindByAllResponseDto.java | 25 +++++++ .../review/dto/ReviewUpdateRequestDto.java | 20 ++++++ .../review/dto/ReviewUpdateResponseDto.java | 25 +++++++ .../gamemate/review/entity/Review.java | 49 +++++++++++++ .../review/repository/ReviewRepository.java | 11 +++ .../review/service/ReviewService.java | 62 +++++++++++++++++ 9 files changed, 309 insertions(+) create mode 100644 src/main/java/com/example/gamemate/review/controller/ReviewController.java create mode 100644 src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java create mode 100644 src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/review/entity/Review.java create mode 100644 src/main/java/com/example/gamemate/review/repository/ReviewRepository.java create mode 100644 src/main/java/com/example/gamemate/review/service/ReviewService.java diff --git a/src/main/java/com/example/gamemate/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/review/controller/ReviewController.java new file mode 100644 index 0000000..3d1cd81 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/controller/ReviewController.java @@ -0,0 +1,68 @@ +package com.example.gamemate.review.controller; + +import com.example.gamemate.review.dto.ReviewCreateRequestDto; +import com.example.gamemate.review.dto.ReviewCreateResponseDto; +import com.example.gamemate.review.dto.ReviewUpdateRequestDto; +import com.example.gamemate.review.dto.ReviewUpdateResponseDto; +import com.example.gamemate.review.entity.Review; +import com.example.gamemate.review.service.ReviewService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/games/{gameId}/reviews") +public class ReviewController { + + private final ReviewService reviewService; + + @Autowired + public ReviewController(ReviewService reviewService) { + this.reviewService = reviewService; + } + + /** + * 리뷰등록 + * + * @param gameId + * @param requestDto + * @return + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createReview(@PathVariable Long gameId, @RequestBody ReviewCreateRequestDto requestDto) { + + ReviewCreateResponseDto responseDto = reviewService.createReview(gameId, requestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } + + /** + * 리뷰 수정 + * + * @param gameId + * @param id + * @param requestDto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateReview(@PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto) { + + ReviewUpdateResponseDto responseDto = reviewService.updateReview(gameId, id, requestDto); + return ResponseEntity.ok(responseDto); + } + + /** + * 리뷰 삭제 + * + * @param gameId + * @param id + * @return + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteReview(@PathVariable Long gameId, @PathVariable Long id) { + + reviewService.deleteReview(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java b/src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java new file mode 100644 index 0000000..93bebc5 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java @@ -0,0 +1,24 @@ +package com.example.gamemate.review.dto; + +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.review.entity.Review; +import com.example.gamemate.user.entity.User; + +import lombok.Getter; + +@Getter +public class ReviewCreateRequestDto { + + private String content; + private Integer star; + private Long gameId; // Game 엔티티 대신 gameId만 전달 + private Long userId; + + public ReviewCreateRequestDto(String content, Integer star, Long gameId, Long userId) { + this.content = content; + this.star = star; + this.gameId = gameId; + this.userId = userId; + + } +} diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java b/src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java new file mode 100644 index 0000000..a5f0fc3 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java @@ -0,0 +1,25 @@ +package com.example.gamemate.review.dto; + +import com.example.gamemate.review.entity.Review; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReviewCreateResponseDto { + private Long id; + private String content; + private Integer star; + private Long gameId; + private Long userId; + private LocalDateTime createdAt; + + public ReviewCreateResponseDto(Review review) { + this.id = review.getId(); + this.content = review.getContent(); + this.star = review.getStar(); + this.gameId = review.getGame().getId(); + this.userId = review.getUserId(); + this.createdAt = review.getCreatedAt(); + } +} diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java new file mode 100644 index 0000000..842f146 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java @@ -0,0 +1,25 @@ +package com.example.gamemate.review.dto; + +import com.example.gamemate.review.entity.Review; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReviewFindByAllResponseDto { + private Long id; + private String content; + private Integer star; + private Long gameId; + private Long userId; + private LocalDateTime createdAt; + + public ReviewFindByAllResponseDto(Review review) { + this.id = review.getId(); + this.content = review.getContent(); + this.star = review.getStar(); + this.gameId = review.getGame().getId(); + this.userId = review.getUserId(); + this.createdAt = review.getCreatedAt(); + } +} diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java b/src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java new file mode 100644 index 0000000..e529649 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.review.dto; + +import lombok.Getter; + +@Getter +public class ReviewUpdateRequestDto { + + private String content; + private Integer star; + private Long gameId; // Game 엔티티 대신 gameId만 전달 + private Long userId; + + public ReviewUpdateRequestDto(String content, Integer star, Long gameId, Long userId) { + this.content = content; + this.star = star; + this.gameId = gameId; + this.userId = userId; + + } +} diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java b/src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java new file mode 100644 index 0000000..ceab845 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java @@ -0,0 +1,25 @@ +package com.example.gamemate.review.dto; + +import com.example.gamemate.review.entity.Review; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReviewUpdateResponseDto { + private Long id; + private String content; + private Integer star; + private Long gameId; + private Long userId; + private LocalDateTime modifiedAt; + + public ReviewUpdateResponseDto(Review review) { + this.id = review.getId(); + this.content = review.getContent(); + this.star = review.getStar(); + this.gameId = review.getGame().getId(); + this.userId = review.getUserId(); + this.modifiedAt = review.getModifiedAt(); + } +} diff --git a/src/main/java/com/example/gamemate/review/entity/Review.java b/src/main/java/com/example/gamemate/review/entity/Review.java new file mode 100644 index 0000000..794bfae --- /dev/null +++ b/src/main/java/com/example/gamemate/review/entity/Review.java @@ -0,0 +1,49 @@ +package com.example.gamemate.review.entity; + +import com.example.gamemate.base.BaseEntity; +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "review") +public class Review extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "content", nullable = false) + private String content; + + @Column(name = "star", nullable = false) + private Integer star; + + // @ManyToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "user_id") +// private User user; + @Column(name = "user_id", nullable = false) + private Long userId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "game_id") + private Game game; + + public Review(String content, Integer star, Game game, Long userId) { + this.content = content; + this.star = star; + this.game = game; + this.userId = userId; + } + + public void updateReview(String content, Integer star){ + this.content = content; + this.star =star; + + } + + +} diff --git a/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java b/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java new file mode 100644 index 0000000..7b75422 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java @@ -0,0 +1,11 @@ +package com.example.gamemate.review.repository; + +import com.example.gamemate.like.entity.ReviewLike; +import com.example.gamemate.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ReviewRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/gamemate/review/service/ReviewService.java b/src/main/java/com/example/gamemate/review/service/ReviewService.java new file mode 100644 index 0000000..c83a2b4 --- /dev/null +++ b/src/main/java/com/example/gamemate/review/service/ReviewService.java @@ -0,0 +1,62 @@ +package com.example.gamemate.review.service; + +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.game.repository.GameRepository; +import com.example.gamemate.review.dto.ReviewCreateRequestDto; +import com.example.gamemate.review.dto.ReviewCreateResponseDto; +import com.example.gamemate.review.dto.ReviewUpdateRequestDto; +import com.example.gamemate.review.dto.ReviewUpdateResponseDto; +import com.example.gamemate.review.entity.Review; +import com.example.gamemate.review.repository.ReviewRepository; +import org.springframework.stereotype.Service; + +import javax.ws.rs.NotFoundException; + +@Service +public class ReviewService { + private final ReviewRepository reviewRepository; + private final GameRepository gameRepository; + + public ReviewService(ReviewRepository reviewRepository, + GameRepository gameRepository) { + this.reviewRepository = reviewRepository; + this.gameRepository = gameRepository; + } + + public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto) { + + Game game = gameRepository.findById(gameId) + .orElseThrow(() -> new NotFoundException("Game not found")); + + Review review = new Review( + requestDto.getContent(), + requestDto.getStar(), + game, + requestDto.getUserId() + ); + Review saveReview = reviewRepository.save(review); + return new ReviewCreateResponseDto(saveReview); + } + + public ReviewUpdateResponseDto updateReview(Long gameId, Long id, ReviewUpdateRequestDto requestDto) { + + Review review = reviewRepository.findById(id) + .orElseThrow(() -> new NotFoundException("리뷰가 존재하지 않습니다.")); + + review.updateReview( + requestDto.getContent(), + requestDto.getStar() + ); + + Review updateReview = reviewRepository.save(review); + return new ReviewUpdateResponseDto(updateReview); + } + + public void deleteReview(Long id) { + + Review review = reviewRepository.findById(id) + .orElseThrow(() -> new NotFoundException("리뷰가 존재하지 않습니다.")); + + reviewRepository.delete(review); + } +} From 139dc964c8c6dbb34ff75e5d972922f42a757a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 9 Jan 2025 15:46:48 +0900 Subject: [PATCH 026/215] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=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 --- build.gradle | 7 ++- .../auth/controller/AuthController.java | 29 +++++++++++++ .../domain/auth/dto/SignupRequestDto.java | 27 ++++++++++++ .../domain/auth/dto/SignupResponseDto.java | 20 +++++++++ .../domain/auth/service/AuthService.java | 43 +++++++++++++++++++ .../gamemate/domain/user/entity/User.java | 8 ++-- .../user/enums/{Authority.java => Role.java} | 4 +- .../user/repository/UserRepository.java | 4 ++ .../global/config/SecurityConfig.java | 32 ++++++++++++++ 9 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/SignupResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/AuthService.java rename src/main/java/com/example/gamemate/domain/user/enums/{Authority.java => Role.java} (77%) create mode 100644 src/main/java/com/example/gamemate/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index c2b577f..36ea43d 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + implementation 'com.querydsl:querydsl-jpa:5.0.1:jakarta' implementation 'at.favre.lib:bcrypt:0.10.2' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' @@ -39,6 +43,7 @@ dependencies { developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.springframework.security:spring-security-test' // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..a07b9ea --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -0,0 +1,29 @@ +package com.example.gamemate.domain.auth.controller; + +import com.example.gamemate.domain.auth.dto.SignupRequestDto; +import com.example.gamemate.domain.auth.dto.SignupResponseDto; +import com.example.gamemate.domain.auth.service.AuthService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/signup") + public ResponseEntity signup(@Valid @RequestBody SignupRequestDto requestDto) { + SignupResponseDto responseDto = authService.signup(requestDto); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java new file mode 100644 index 0000000..0205ff2 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java @@ -0,0 +1,27 @@ +package com.example.gamemate.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class SignupRequestDto { + + @NotBlank(message = "이메일을 입력해주세요.") + @Email(message = "이메일 형식을 확인해주세요.") + private final String email; + + @NotBlank(message = "이름을 입력해주세요.") + private final String name; + + @NotBlank(message = "닉네임을 입력해주세요.") + private final String nickname; + + @NotBlank(message = "비밀번호를 입력해주세요.") +// @Size(min = 8, message = "비밀번호는 8글자 이상으로 입력해주세요.") +// @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자 최소 1글자씩 입력해주세요.") + private final String password; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/SignupResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/SignupResponseDto.java new file mode 100644 index 0000000..fe95d7b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/SignupResponseDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.auth.dto; + +import com.example.gamemate.domain.user.entity.User; +import lombok.Getter; + +@Getter +public class SignupResponseDto { + + private final Long id; + private final String email; + private final String name; + private final String nickname; + + public SignupResponseDto(User user) { + this.id = user.getId(); + this.email = user.getEmail(); + this.name = user.getName(); + this.nickname = user.getNickname(); + } +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java new file mode 100644 index 0000000..b43c4c1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -0,0 +1,43 @@ +package com.example.gamemate.domain.auth.service; + +import com.example.gamemate.domain.auth.dto.SignupRequestDto; +import com.example.gamemate.domain.auth.dto.SignupResponseDto; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.UserStatus; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public SignupResponseDto signup(SignupRequestDto requestDto) { + Optional findUser = userRepository.findByEmail(requestDto.getEmail()); + if(findUser.isPresent()) { + if(findUser.get().getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.WITHDRAWN_USER); + } + throw new ApiException(ErrorCode.DUPLICATE_EMAIL); + } + + String rawPassword = requestDto.getPassword(); + String encodedPassword = passwordEncoder.encode(rawPassword); + + User user = new User(requestDto.getEmail(), requestDto.getName(), requestDto.getNickname(), encodedPassword); + User savedUser = userRepository.save(user); + + return new SignupResponseDto(savedUser); + } + + + +} diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index 36e2a56..ccbf109 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -1,14 +1,12 @@ package com.example.gamemate.domain.user.entity; import com.example.gamemate.global.common.BaseEntity; -import com.example.gamemate.domain.user.enums.Authority; +import com.example.gamemate.domain.user.enums.Role; import com.example.gamemate.domain.user.enums.UserStatus; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @Table(name = "user") @Getter @@ -33,7 +31,7 @@ public class User extends BaseEntity { private String password; @Enumerated(EnumType.STRING) - private Authority auth; + private Role role; private Boolean isPremium; @@ -45,7 +43,7 @@ public User(String email, String name, String nickname, String password) { this.name = name; this.nickname = nickname; this.password = password; - this.auth = Authority.USER; + this.role = Role.USER; this.isPremium = false; this.userStatus = UserStatus.ACTIVE; } diff --git a/src/main/java/com/example/gamemate/domain/user/enums/Authority.java b/src/main/java/com/example/gamemate/domain/user/enums/Role.java similarity index 77% rename from src/main/java/com/example/gamemate/domain/user/enums/Authority.java rename to src/main/java/com/example/gamemate/domain/user/enums/Role.java index cf2528c..e7a9b5b 100644 --- a/src/main/java/com/example/gamemate/domain/user/enums/Authority.java +++ b/src/main/java/com/example/gamemate/domain/user/enums/Role.java @@ -3,14 +3,14 @@ import lombok.Getter; @Getter -public enum Authority { +public enum Role { USER("user"), ADMIN("admin"); private String name; - Authority(String name) { + Role(String name) { this.name = name; } } diff --git a/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java index c6f04a7..67029ca 100644 --- a/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/gamemate/domain/user/repository/UserRepository.java @@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + } diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java new file mode 100644 index 0000000..ceb9850 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -0,0 +1,32 @@ +package com.example.gamemate.global.config; + +import com.example.gamemate.domain.user.enums.Role; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(crsf->crsf.disable()) //CSRF 보호 비활성화 (REST API이므로) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/signup", "/auth/login").permitAll() + .requestMatchers("/관리자관련url").hasRole("admin") + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} From 2d5bb7c191b1d4e43ddcc2d8e0512b9138059663 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:11:02 +0900 Subject: [PATCH 027/215] =?UTF-8?q?feat:=20add=20=EA=B2=8C=EC=9E=84?= =?UTF-8?q?=EA=B3=BC=20=EB=A6=AC=EB=B7=B0=20=EA=B4=80=EB=A0=A8=EB=90=9C=20?= =?UTF-8?q?=20=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 1. 게임 단건 조회시 리뷰 리스트 페이징으로 불러오기 구현 2. 게임 다건(검샘포함) 평균별점, 리뷰 수 불러오기 구현 --- .../game/controller/GameController.java | 48 ++++++++++- .../game/dto/GameCreateRequestDto.java | 5 +- .../game/dto/GameCreateResponseDto.java | 7 ++ .../game/dto/GameFindAllResponseDto.java | 20 +++++ .../game/dto/GameFindByIdResponseDto.java | 40 ++++++++++ .../game/dto/GameSearchResponseDto.java | 19 +++++ .../game/dto/GameUpdateResponseDto.java | 4 + .../example/gamemate/game/entity/Game.java | 4 + .../gamemate/game/service/GameService.java | 79 ++++++++++++++++++- .../review/repository/ReviewRepository.java | 5 +- .../review/service/ReviewService.java | 3 + 11 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java diff --git a/src/main/java/com/example/gamemate/game/controller/GameController.java b/src/main/java/com/example/gamemate/game/controller/GameController.java index 64dbcc6..aaa6d1a 100644 --- a/src/main/java/com/example/gamemate/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/game/controller/GameController.java @@ -42,7 +42,8 @@ public ResponseEntity createGame(@RequestBody GameCreateR * @return */ @GetMapping - public ResponseEntity> findAllGame(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int szie) { + public ResponseEntity> findAllGame(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int szie) { Page games = gameService.findAllGame(page, szie); return ResponseEntity.ok(games); @@ -55,9 +56,9 @@ public ResponseEntity> findAllGame(@RequestParam(de * @return */ @GetMapping("/{id}") - public ResponseEntity findGameById(@PathVariable Long id) { + public ResponseEntity findGameById(@PathVariable Long id) { - GameFindByResponseDto gameById = gameService.findGameById(id); + GameFindByIdResponseDto gameById = gameService.findGameById(id); return ResponseEntity.ok(gameById); } @@ -93,5 +94,46 @@ public ResponseEntity> searchGame(@RequestParam Stri Page games = gameService.searchGame(keyword, genre, platform, page, size); return ResponseEntity.ok(games); + } + + //게임 요청 관련 + /** + * 게임등록 요청 + * @param requestDto + * @return + */ + @PostMapping("/requests") + public ResponseEntityCreateGameEnrollRequest(@RequestBody GameEnrollRequestCreateRequestDto requestDto){ + + GameEnrollRequestResponseDto responseDto = gameService.createGameEnrollRequest(requestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } + /** + * 게임등록 요청 전체 조회 + * @return + */ + @GetMapping("/requests") + public ResponseEntity>findAllGameEnrollRequest(){ + + Page gameEnrollRequestAll = gameService.findAllGameEnrollRequest(); + return ResponseEntity.ok(gameEnrollRequestAll); + } + /** + * 게임등록 요청 단건 조회 + * @param id + * @return + */ + @GetMapping("/requests/{id}") + public ResponseEntity findGameEnrollRequestById(@PathVariable Long id) { + + GameEnrollRequestResponseDto gameEnrollRequestById = gameService.findGameEnrollRequestById(id); + return ResponseEntity.ok(gameEnrollRequestById); + } + + @PatchMapping("/requests/{id}") + public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { + + GameEnrollRequestResponseDto responseDto = gameService.updateGameEnroll(id, requestDto); + return ResponseEntity.ok(responseDto); } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java b/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java index 050effd..92f8633 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java @@ -1,6 +1,7 @@ package com.example.gamemate.game.dto; import lombok.Getter; +import java.time.LocalDateTime; @Getter public class GameCreateRequestDto { @@ -10,10 +11,12 @@ public class GameCreateRequestDto { private String description; - public GameCreateRequestDto(String title, String genre, String platform , String description) { + + public GameCreateRequestDto(String title, String genre, String platform , String description ) { this.title = title; this.genre = genre; this.platform = platform; this.description = description; + } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java index bec4451..d7af61e 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java @@ -3,6 +3,8 @@ import com.example.gamemate.game.entity.Game; import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class GameCreateResponseDto { private Long id; @@ -10,6 +12,8 @@ public class GameCreateResponseDto { private String genre; private String platform; private String description; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; public GameCreateResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -18,5 +22,8 @@ public GameCreateResponseDto(Game game) { this.genre = game.getGenre(); this.platform = game.getPlatform(); this.description = game.getDescription(); + this.createdAt = game.getCreatedAt(); + this.modifiedAt =game.getModifiedAt(); + } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java index 1a5a988..69d1ccb 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java @@ -1,14 +1,19 @@ package com.example.gamemate.game.dto; import com.example.gamemate.game.entity.Game; +import com.example.gamemate.review.entity.Review; import lombok.Getter; +import java.util.List; + @Getter public class GameFindAllResponseDto { private final Long id; private final String title; private final String genre; private final String platform; + private final Long reviewCount; + private final Double averageStar; public GameFindAllResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -16,5 +21,20 @@ public GameFindAllResponseDto(Game game) { this.title = game.getTitle(); this.genre = game.getGenre(); this.platform = game.getPlatform(); + this.reviewCount = (long) game.getReviews().size(); + this.averageStar = calculateAverageStar(game.getReviews()); + } + + private Double calculateAverageStar(List reviews) { + if (reviews.isEmpty()) { + return 0.0; + } + double average = reviews.stream() + .mapToInt(Review::getStar) + .average() + .orElse(0.0); + + // 소수점 둘째 자리에서 반올림 + return Math.round(average * 10.0) / 10.0; } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java new file mode 100644 index 0000000..9662770 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java @@ -0,0 +1,40 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt", "modifiedAt", "reviews" }) +public class GameFindByIdResponseDto { + private final Long id; + private final String title; + private final String genre; + private final String platform; + private final String description; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; + private final Page reviews; +// private final List reviews; + + public GameFindByIdResponseDto(Game game, Page reviews) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); + this.title = game.getTitle(); + this.genre = game.getGenre(); + this.platform = game.getPlatform(); + this.description = game.getDescription(); + this.createdAt = game.getCreatedAt(); + this.modifiedAt = game.getModifiedAt(); + this.reviews = reviews; +// this.reviews = game.getReviews().stream() +// .map(ReviewFindByAllResponseDto::new) +// .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java index 9bfb826..a3ee6f2 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java @@ -1,9 +1,11 @@ package com.example.gamemate.game.dto; import com.example.gamemate.game.entity.Game; +import com.example.gamemate.review.entity.Review; import lombok.Getter; import java.time.LocalDateTime; +import java.util.List; @Getter public class GameSearchResponseDto { @@ -13,6 +15,8 @@ public class GameSearchResponseDto { private final String platform; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; + private final Long reviewCount; + private final Double averageStar; public GameSearchResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -22,5 +26,20 @@ public GameSearchResponseDto(Game game) { this.platform = game.getPlatform(); this.createdAt = game.getCreatedAt(); this.modifiedAt = game.getModifiedAt(); + this.reviewCount = (long) game.getReviews().size(); + this.averageStar = calculateAverageStar(game.getReviews()); + } + + private Double calculateAverageStar(List reviews) { + if (reviews.isEmpty()) { + return 0.0; + } + double average = reviews.stream() + .mapToInt(Review::getStar) + .average() + .orElse(0.0); + + // 소수점 둘째 자리에서 반올림 + return Math.round(average * 10.0) / 10.0; } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java index 23f81e4..b4b4023 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java @@ -3,6 +3,8 @@ import com.example.gamemate.game.entity.Game; import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class GameUpdateResponseDto { private Long id; @@ -10,6 +12,7 @@ public class GameUpdateResponseDto { private String genre; private String platform; private String description; + private final LocalDateTime modifiedAt; public GameUpdateResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -18,5 +21,6 @@ public GameUpdateResponseDto(Game game) { this.genre = game.getGenre(); this.platform = game.getPlatform(); this.description = game.getDescription(); + this.modifiedAt = game.getModifiedAt(); } } diff --git a/src/main/java/com/example/gamemate/game/entity/Game.java b/src/main/java/com/example/gamemate/game/entity/Game.java index 08bf720..29f84fe 100644 --- a/src/main/java/com/example/gamemate/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/game/entity/Game.java @@ -1,6 +1,7 @@ package com.example.gamemate.game.entity; import com.example.gamemate.base.BaseEntity; +import com.example.gamemate.review.entity.Review; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -34,6 +35,9 @@ public class Game extends BaseEntity { @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) private List gameImages = new ArrayList<>(); + @OneToMany(mappedBy = "game", fetch = FetchType.LAZY) + private List reviews = new ArrayList<>(); + public Game(String title, String genre, String platform, String description) { this.title = title; this.genre = genre; diff --git a/src/main/java/com/example/gamemate/game/service/GameService.java b/src/main/java/com/example/gamemate/game/service/GameService.java index 62a92cf..e155974 100644 --- a/src/main/java/com/example/gamemate/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/game/service/GameService.java @@ -1,8 +1,13 @@ package com.example.gamemate.game.service; import com.example.gamemate.game.dto.*; +import com.example.gamemate.game.entity.GamaEnrollRequest; import com.example.gamemate.game.entity.Game; +import com.example.gamemate.game.repository.GameEnrollRequestRepository; import com.example.gamemate.game.repository.GameRepository; +import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; +import com.example.gamemate.review.entity.Review; +import com.example.gamemate.review.repository.ReviewRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -19,11 +24,15 @@ @Slf4j public class GameService { private final GameRepository gameRepository; + private final ReviewRepository reviewRepository; + private final GameEnrollRequestRepository gameEnrollRequestRepository; @Autowired - public GameService(GameRepository gameRepository) { + public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, GameEnrollRequestRepository gameEnrollRequestRepository) { this.gameRepository = gameRepository; + this.reviewRepository=reviewRepository; + this.gameEnrollRequestRepository = gameEnrollRequestRepository; } public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto) { @@ -46,9 +55,19 @@ public Page findAllGame(int page, int size) { } @Transactional - public GameFindByResponseDto findGameById(Long id) { + public GameFindByIdResponseDto findGameById(Long id) { + Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); - return new GameFindByResponseDto(game); + + Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); + Page reviewPage = reviewRepository.findAllByGame(game, pageable); + + // Review를 ReviewFindByAllResponseDto로 변환 + Page reviews = reviewPage.map(review -> + new ReviewFindByAllResponseDto(review) + ); + + return new GameFindByIdResponseDto(game, reviews); } @Transactional @@ -78,4 +97,58 @@ public Page searchGame(String keyword, String genre, Stri Page games = gameRepository.searchGames(keyword, genre, platform, pageable); return games.map(GameSearchResponseDto::new); } + + public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto) { + GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + GamaEnrollRequest saveEnrollRequest = gameEnrollRequestRepository.save(gameEnrollRequest); + return new GameEnrollRequestResponseDto(saveEnrollRequest); + } + + public Page findAllGameEnrollRequest() { + + Pageable pageable = PageRequest.of(0, 10); + + return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); + } + + @Transactional + public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { + + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id) + .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); + + return new GameEnrollRequestResponseDto(gamaEnrollRequest); + } + + @Transactional + public GameEnrollRequestResponseDto updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + + gamaEnrollRequest.updateGameEnroll( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription(), + requestDto.getIsAccepted() + ); + GamaEnrollRequest updateGameEnroll = gameEnrollRequestRepository.save(gamaEnrollRequest); + + Boolean accepted = requestDto.getIsAccepted(); + if(accepted == true){ + Game game = new Game( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + gameRepository.save(game); + } + return new GameEnrollRequestResponseDto(updateGameEnroll); + } + } diff --git a/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java b/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java index 7b75422..0b5534a 100644 --- a/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java @@ -1,11 +1,14 @@ package com.example.gamemate.review.repository; +import com.example.gamemate.game.entity.Game; import com.example.gamemate.like.entity.ReviewLike; import com.example.gamemate.review.entity.Review; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface ReviewRepository extends JpaRepository { - + Page findAllByGame(Game game, Pageable pageable); } diff --git a/src/main/java/com/example/gamemate/review/service/ReviewService.java b/src/main/java/com/example/gamemate/review/service/ReviewService.java index c83a2b4..9256361 100644 --- a/src/main/java/com/example/gamemate/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/review/service/ReviewService.java @@ -59,4 +59,7 @@ public void deleteReview(Long id) { reviewRepository.delete(review); } + + + } From 38bb8fc7c4a3be0b0cef6d3ddd2ba50ee912168d Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 9 Jan 2025 17:46:33 +0900 Subject: [PATCH 028/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 30 +++++++++++++ .../match/dto/MatchCreateRequestDto.java | 14 ++++++ .../match/dto/MatchCreateResponseDto.java | 12 ++++++ .../gamemate/domain/match/entity/Match.java | 39 +++++++++++++++++ .../domain/match/enums/MatchStatus.java | 13 ++++++ .../match/repository/MatchRepository.java | 12 ++++++ .../domain/match/service/MatchService.java | 43 +++++++++++++++++++ .../gamemate/global/constant/ErrorCode.java | 1 + 8 files changed, 164 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/match/controller/MatchController.java create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/match/entity/Match.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/MatchStatus.java create mode 100644 src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/match/service/MatchService.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java new file mode 100644 index 0000000..1d19951 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -0,0 +1,30 @@ +package com.example.gamemate.domain.match.controller; + +import com.example.gamemate.domain.match.service.MatchService; +import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; +import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/matches") +public class MatchController { + private final MatchService matchService; + + /** + * 매칭 요청 생성 + * @param dto MatchCreateRequestDto 상대 유저 id, 상대방에게 전할 메세지 + * @return message = "매칭이 요청되었습니다." + */ + @PostMapping + public ResponseEntity createMatch(@RequestBody MatchCreateRequestDto dto) { + MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto); + return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java new file mode 100644 index 0000000..22bd2aa --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java @@ -0,0 +1,14 @@ +package com.example.gamemate.domain.match.dto; + +import lombok.Getter; + +@Getter +public class MatchCreateRequestDto { + private Long userId; + private String message; + + public MatchCreateRequestDto(Long userId, String message) { + this.userId = userId; + this.message = message; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java new file mode 100644 index 0000000..6dcb140 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.match.dto; + +import lombok.Getter; + +@Getter +public class MatchCreateResponseDto { + private String message; + + public MatchCreateResponseDto(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/Match.java b/src/main/java/com/example/gamemate/domain/match/entity/Match.java new file mode 100644 index 0000000..5e9fc08 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/entity/Match.java @@ -0,0 +1,39 @@ +package com.example.gamemate.domain.match.entity; + +import com.example.gamemate.domain.match.enums.MatchStatus; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "matches") +public class Match { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private MatchStatus status; + + @Column + private String message; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id", nullable = false) + private User sender; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "receiver_id", nullable = false) + private User receiver; + + public Match() { + } + + public Match(String message, User sender, User receiver) { + this.status = MatchStatus.PENDING; + this.message = message; + this.sender = sender; + this.receiver = receiver; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/MatchStatus.java b/src/main/java/com/example/gamemate/domain/match/enums/MatchStatus.java new file mode 100644 index 0000000..2cccc1f --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/MatchStatus.java @@ -0,0 +1,13 @@ +package com.example.gamemate.domain.match.enums; + +public enum MatchStatus { + ACCEPTED("accepted"), + PENDING("pending"), + REJECTED("rejected"); + + private final String name; + + MatchStatus(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java new file mode 100644 index 0000000..7ba2708 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.match.repository; + +import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.match.enums.MatchStatus; +import com.example.gamemate.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MatchRepository extends JpaRepository { + Boolean existsBySenderAndReceiverAndStatus(User sender, User receiver, MatchStatus status); +} diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java new file mode 100644 index 0000000..e21a27b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -0,0 +1,43 @@ +package com.example.gamemate.domain.match.service; + +import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.match.enums.MatchStatus; +import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; +import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; +import com.example.gamemate.domain.match.repository.MatchRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.UserStatus; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MatchService { + private final UserRepository userRepository; + private final MatchRepository matchRepository; + + // 매칭 요청 생성 + // todo : 현재 로그인이 구현되어 있지 않아, 로그인 유저를 1번 유저로 설정 + @Transactional + public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { + User loginUser = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User receiver = userRepository.findById(dto.getUserId()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if (receiver.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + } + + if (matchRepository.existsBySenderAndReceiverAndStatus(loginUser, receiver, MatchStatus.PENDING)) { + throw new ApiException(ErrorCode.IS_ALREADY_PENDING); + } + + Match match = new Match(dto.getMessage(), loginUser, receiver); + matchRepository.save(match); + + return new MatchCreateResponseDto("매칭이 요청되었습니다."); + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 1c1cdf1..9c03c36 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -12,6 +12,7 @@ public enum ErrorCode { INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 유저입니다."), IS_WITHDRAW_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 유저입니다."), + IS_ALREADY_PENDING(HttpStatus.BAD_REQUEST, "IS_ALREADY_PENDING", "이미 대기중인 요청이 있습니다."), /* 401 세션 없음 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From 3fdaf1c99293946b4129039e714340c2d0cdc376 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 9 Jan 2025 17:48:50 +0900 Subject: [PATCH 029/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=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 --- .../notification/entity/Notification.java | 33 +++++++++++++++++++ .../notification/enums/NotificationType.java | 19 +++++++++++ .../repository/NotificationRepository.java | 12 +++++++ .../service/NotificationService.java | 26 +++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/notification/entity/Notification.java create mode 100644 src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java create mode 100644 src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java diff --git a/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java new file mode 100644 index 0000000..e1dfa4a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java @@ -0,0 +1,33 @@ +package com.example.gamemate.domain.notification.entity; + +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Getter +public class Notification { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String content; + + @Enumerated(EnumType.STRING) + @Column + private NotificationType type; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + public Notification() { + } + + public Notification(String content, NotificationType type, User user) { + this.content = content; + this.type = type; + this.user = user; + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java b/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java new file mode 100644 index 0000000..f6ffe03 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.notification.enums; + +import lombok.Getter; + +@Getter +public enum NotificationType { + FOLLOW("follow", "새 팔로워가 생겼습니다."), + COMMENT("comment", "새로운 댓글이 달렸습니다."), + MATCHING("matching", "매칭에 성공했습니다."), + LIKE("like", "좋아요가 달렸습니다."); + + private final String name; + private final String content; + + NotificationType(String name, String content) { + this.name = name; + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..f63d7b2 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.notification.repository; + +import com.example.gamemate.domain.notification.entity.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NotificationRepository extends JpaRepository { + List findAllByUserId(Long userId); +} diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java new file mode 100644 index 0000000..604209a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -0,0 +1,26 @@ +package com.example.gamemate.domain.notification.service; + +import com.example.gamemate.domain.notification.dto.NotificationResponseDto; +import com.example.gamemate.domain.notification.entity.Notification; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.repository.NotificationRepository; +import com.example.gamemate.domain.user.entity.User; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final NotificationRepository notificationRepository; + + // 알림 생성 + @Transactional + public void createNotification(User user, NotificationType type) { + Notification notification = new Notification(type.getContent(), type, user); + notificationRepository.save(notification); + } +} From e73b138c388c08b09c55a2d1c1a4d75bd92ee522 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 9 Jan 2025 17:49:23 +0900 Subject: [PATCH 030/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 25 +++++++++++++++++++ .../dto/NotificationResponseDto.java | 22 ++++++++++++++++ .../service/NotificationService.java | 11 ++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java create mode 100644 src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java new file mode 100644 index 0000000..800bda3 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -0,0 +1,25 @@ +package com.example.gamemate.domain.notification.controller; + +import com.example.gamemate.domain.notification.dto.NotificationResponseDto; +import com.example.gamemate.domain.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +public class NotificationController { + private final NotificationService notificationService; + + @GetMapping + public ResponseEntity> findAllNotification() { + List NotificationResponseDtoList = notificationService.findAllNotification(); + return new ResponseEntity<>(NotificationResponseDtoList, HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java new file mode 100644 index 0000000..09d44d5 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.domain.notification.dto; + +import com.example.gamemate.domain.notification.entity.Notification; +import com.example.gamemate.domain.notification.enums.NotificationType; +import lombok.Getter; + +@Getter +public class NotificationResponseDto { + private Long id; + private String content; + private NotificationType type; + + public NotificationResponseDto(Long id, String content, NotificationType type) { + this.id = id; + this.content = content; + this.type = type; + } + + public static NotificationResponseDto toDto(Notification notification) { + return new NotificationResponseDto(notification.getId(), notification.getContent(), notification.getType()); + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 604209a..b774c3b 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -23,4 +23,15 @@ public void createNotification(User user, NotificationType type) { Notification notification = new Notification(type.getContent(), type, user); notificationRepository.save(notification); } + + // 알림 전체 보기 + // todo 현재 로그인이 구현되어 있지 않아 1번 유저의 알림 목록을 불러오게 설정, 추후 로그인 구현시 로그인한 유저의 id값을 넣도록 변경 + public List findAllNotification() { + List notificationList = notificationRepository.findAllByUserId(1L); + + return notificationList + .stream() + .map(NotificationResponseDto::toDto) + .toList(); + } } From 15d9b471211e8adc9e45a73e1dac477af95a9d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 9 Jan 2025 19:44:27 +0900 Subject: [PATCH 031/215] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=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 --- .../auth/controller/AuthController.java | 7 +++++++ .../domain/auth/dto/EmailLoginRequestDto.java | 18 ++++++++++++++++ .../auth/dto/EmailLoginResponseDto.java | 14 +++++++++++++ .../domain/auth/service/AuthService.java | 21 +++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index a07b9ea..b91e843 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.auth.controller; +import com.example.gamemate.domain.auth.dto.EmailLoginRequestDto; import com.example.gamemate.domain.auth.dto.SignupRequestDto; import com.example.gamemate.domain.auth.dto.SignupResponseDto; import com.example.gamemate.domain.auth.service.AuthService; @@ -25,5 +26,11 @@ public ResponseEntity signup(@Valid @RequestBody SignupReques return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } + @PostMapping("/login") + public ResponseEntity emailLogin(@Valid @RequestBody EmailLoginRequestDto requestDto) { + authService.emailLogin(requestDto); + return new ResponseEntity<>("로그인 되었습니다.", HttpStatus.OK); + } + } diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java new file mode 100644 index 0000000..d27fb61 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java @@ -0,0 +1,18 @@ +package com.example.gamemate.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class EmailLoginRequestDto { + + @NotBlank(message = "이메일을 입력해주세요.") + private final String email; + + @NotBlank(message = "비밀번호를 입력해주세요.") + private final String password; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java new file mode 100644 index 0000000..6a74ee0 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java @@ -0,0 +1,14 @@ +package com.example.gamemate.domain.auth.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class EmailLoginResponseDto { + + private final String token; + private final String email; + private final String nickname; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index b43c4c1..cfb2994 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -1,5 +1,7 @@ package com.example.gamemate.domain.auth.service; +import com.example.gamemate.domain.auth.dto.EmailLoginRequestDto; +import com.example.gamemate.domain.auth.dto.EmailLoginResponseDto; import com.example.gamemate.domain.auth.dto.SignupRequestDto; import com.example.gamemate.domain.auth.dto.SignupResponseDto; import com.example.gamemate.domain.user.entity.User; @@ -7,6 +9,7 @@ import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.provider.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -19,6 +22,7 @@ public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; public SignupResponseDto signup(SignupRequestDto requestDto) { Optional findUser = userRepository.findByEmail(requestDto.getEmail()); @@ -38,6 +42,23 @@ public SignupResponseDto signup(SignupRequestDto requestDto) { return new SignupResponseDto(savedUser); } + public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto) { + User findUser = userRepository.findByEmail(requestDto.getEmail()) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if(findUser.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.WITHDRAWN_USER); + } + + if(!passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) { + throw new ApiException(ErrorCode.INVALID_PASSWORD); + } + + String jwtToken = jwtTokenProvider.createAccessToken(findUser.getEmail(), findUser.getRole()); + + //Todo 로그인응답dto에서 토큰만 주면 되나? + return new EmailLoginResponseDto(jwtToken, findUser.getEmail(), findUser.getNickname()); + } } From 13a4b1c2514f909c977e572ecf6f5d54779acd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 9 Jan 2025 19:45:41 +0900 Subject: [PATCH 032/215] =?UTF-8?q?feat:=20Spring=20Security=20JWT=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring Security - JWT - 프로필 기능에 토큰 검증 및 본인 검증 추가 --- .../user/controller/UserController.java | 20 ++++-- .../domain/user/service/UserService.java | 58 ++++++++++++---- .../global/config/CustomUserDetails.java | 64 +++++++++++++++++ .../config/CustomUserDetailsService.java | 25 +++++++ .../config/DelegatedAccessDeniedHandler.java | 30 ++++++++ .../DelegatedAuthenticationEntryPoint.java | 29 ++++++++ .../global/config/SecurityConfig.java | 36 +++++++++- .../gamemate/global/constant/ErrorCode.java | 10 ++- .../exception/GlobalExceptionHandler.java | 25 +++++++ .../filter/JwtAuthenticationFilter.java | 64 +++++++++++++++++ .../global/provider/JwtTokenProvider.java | 69 +++++++++++++++++++ src/main/resources/application.properties | 5 +- 12 files changed, 413 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/example/gamemate/global/config/CustomUserDetails.java create mode 100644 src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java create mode 100644 src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java create mode 100644 src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java create mode 100644 src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index ffdba68..94ccdea 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -17,25 +17,33 @@ public class UserController { private final UserService userService; @GetMapping("/{id}") - public ResponseEntity findProfile(@PathVariable Long id) { - ProfileResponseDto responseDto = userService.findProfile(id); + public ResponseEntity findProfile( + @PathVariable Long id, + @RequestHeader("Authorization") String token) { + + String jwtToken = token.substring(7); + ProfileResponseDto responseDto = userService.findProfile(id, jwtToken); return new ResponseEntity<>(responseDto, HttpStatus.OK); } @PatchMapping("/{id}") public ResponseEntity updateProfile( @PathVariable Long id, - @Valid @RequestBody ProfileUpdateRequestDto requestDto) { - ProfileResponseDto responseDto = userService.updateProfile(id, requestDto.getNewNickname()); + @Valid @RequestBody ProfileUpdateRequestDto requestDto, + @RequestHeader("Authorization") String token) { + String jwtToken = token.substring(7); + ProfileResponseDto responseDto = userService.updateProfile(id, requestDto.getNewNickname(), jwtToken); return new ResponseEntity<>(responseDto, HttpStatus.OK); } @PatchMapping("/{id}/password") public ResponseEntity updatePassword( @PathVariable Long id, - @Valid @RequestBody PasswordUpdateRequestDto requestDto) { + @Valid @RequestBody PasswordUpdateRequestDto requestDto, + @RequestHeader("Authorization") String token) { - userService.updatePassword(id, requestDto.getNewPassword()); + String jwtToken = token.substring(7); + userService.updatePassword(id, requestDto.getOldPassword(), requestDto.getNewPassword(), jwtToken); return new ResponseEntity<>("비밀번호가 변경되었습니다.", HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index a7e9938..a9f9791 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -6,28 +6,40 @@ import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.provider.JwtTokenProvider; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import javax.swing.*; - @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final PasswordEncoder passwordEncoder; + + public ProfileResponseDto findProfile(Long id, String token) { + + validateToken(token); - public ProfileResponseDto findProfile(Long id) { - User findUser = userRepository.findById(id).orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + User findUser = userRepository.findById(id) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); if(UserStatus.WITHDRAW.equals(findUser.getUserStatus())) { - throw new ApiException(ErrorCode.USER_WITHDRAWN); + throw new ApiException(ErrorCode.WITHDRAWN_USER); } return new ProfileResponseDto(findUser); } - public ProfileResponseDto updateProfile(Long id, String newNickname) { - User findUser = userRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + public ProfileResponseDto updateProfile(Long id, String newNickname, String token) { + + validateToken(token); + + User findUser = userRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + validateOwner(findUser, token); findUser.updateProfile(newNickname); User savedUser = userRepository.save(findUser); @@ -35,13 +47,35 @@ public ProfileResponseDto updateProfile(Long id, String newNickname) { return new ProfileResponseDto(savedUser); } - public void updatePassword(Long id, String newPassword) { - User findUser = userRepository.findById(id).orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + public void updatePassword(Long id, String oldPassword, String newPassword, String token) { - //Todo 비밀번호 검증 로직 Spring Security로 구현 + validateToken(token); - findUser.updatePassword(newPassword); - User savedUser = userRepository.save(findUser); + User findUser = userRepository.findById(id) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + validateOwner(findUser, token); + + if(!passwordEncoder.matches(oldPassword, findUser.getPassword())) { + throw new ApiException(ErrorCode.INVALID_PASSWORD); + } + + String encodedPassword = passwordEncoder.encode(newPassword); + findUser.updatePassword(encodedPassword); + userRepository.save(findUser); + } + + private void validateToken(String token) { + if(!jwtTokenProvider.validateToken(token)) { + throw new ApiException(ErrorCode.INVALID_TOKEN); + } + } + + private void validateOwner(User user, String token) { + String emailFromToken = jwtTokenProvider.getEmailFromToken(token); + if(!user.getEmail().equals(emailFromToken)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } } } diff --git a/src/main/java/com/example/gamemate/global/config/CustomUserDetails.java b/src/main/java/com/example/gamemate/global/config/CustomUserDetails.java new file mode 100644 index 0000000..603fc9b --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/CustomUserDetails.java @@ -0,0 +1,64 @@ +package com.example.gamemate.global.config; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.Role; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +@Slf4j +public class CustomUserDetails implements UserDetails { + + private final User user; + + @Override + public Collection getAuthorities() { + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); + + return authorities; + } + + //비밀번호 + @Override + public String getPassword() { + return user.getPassword(); + } + + //사용자계정 + @Override + public String getUsername() { + return user.getEmail(); + } + + //사용하지 않을 경우 true 리턴 + //초기값 true로 되어있음 + @Override + public boolean isAccountNonExpired() { + return UserDetails.super.isAccountNonExpired(); + } + + @Override + public boolean isAccountNonLocked() { + return UserDetails.super.isAccountNonLocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return UserDetails.super.isCredentialsNonExpired(); + } + + @Override + public boolean isEnabled() { + return UserDetails.super.isEnabled(); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java b/src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java new file mode 100644 index 0000000..dfc487d --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java @@ -0,0 +1,25 @@ +package com.example.gamemate.global.config; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(()-> new UsernameNotFoundException("유저를 찾을 수 없습니다.")); + return new CustomUserDetails(user); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java b/src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java new file mode 100644 index 0000000..dfa4f91 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java @@ -0,0 +1,30 @@ +package com.example.gamemate.global.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; + +@Component +public class DelegatedAccessDeniedHandler implements AccessDeniedHandler { + + private final HandlerExceptionResolver resolver; + + public DelegatedAccessDeniedHandler( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { + this.resolver = resolver; + } + + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + resolver.resolveException(request, response, null, accessDeniedException); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java b/src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java new file mode 100644 index 0000000..dc7ef81 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java @@ -0,0 +1,29 @@ +package com.example.gamemate.global.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; + +@Component +public class DelegatedAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final HandlerExceptionResolver resolver; + + public DelegatedAuthenticationEntryPoint( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { + this.resolver = resolver; + } + + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + resolver.resolveException(request, response, null, authException); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index ceb9850..05f66b3 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -1,18 +1,33 @@ package com.example.gamemate.global.config; import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; 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.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final DelegatedAuthenticationEntryPoint authenticationEntryPoint; + private final DelegatedAccessDeniedHandler accessDeniedHandler; + private final UserDetailsService userDetailsService; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(crsf->crsf.disable()) //CSRF 보호 비활성화 (REST API이므로) @@ -20,7 +35,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/signup", "/auth/login").permitAll() .requestMatchers("/관리자관련url").hasRole("admin") .anyRequest().authenticated() - ); + ) + .exceptionHandling(hanling-> hanling + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler)) + .sessionManagement(session-> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @@ -29,4 +50,17 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public RoleHierarchy roleHierarchy() { + return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_USER"); + } + } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 20dd27c..cf510da 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -10,14 +10,20 @@ public enum ErrorCode { /* 400 잘못된 입력값 */ INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), + DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "DUPLICATE_USER", "이미 사용 중인 이메일입니다."), + WITHDRAWN_USER(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "비밀번호가 일치하지 않습니다."), - /* 401 세션 없음 */ + /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "INVALID_TOKEN", "유효하지 않은 토큰입니다."), + + /* 403 권한 없음 */ + FORBIDDEN(HttpStatus.FORBIDDEN, "FORBIDDEN", "권한이 없습니다."), /* 404 찾을 수 없음 */ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), - USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), /* 500 서버 오류 */ diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 4f505d7..2ffdffc 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -1,10 +1,13 @@ package com.example.gamemate.global.exception; import com.example.gamemate.global.constant.ErrorCode; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; import lombok.extern.slf4j.Slf4j; import org.apache.coyote.Response; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; @@ -13,6 +16,8 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import javax.security.sasl.AuthenticationException; +import java.security.SignatureException; import java.util.List; import java.util.stream.Collectors; @@ -68,6 +73,26 @@ public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotVali return ResponseEntity.badRequest().body(errorResponse); } + @ExceptionHandler({ + ExpiredJwtException.class, + SignatureException.class, + MalformedJwtException.class, + AuthenticationException.class + }) + public ResponseEntity handleJwtException(Exception e) { + log.warn("handleJwtException", e); + ErrorCode errorCode = ErrorCode.INVALID_TOKEN; + return handleExceptionInternal(errorCode, errorCode.getMessage()); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException e) { + log.warn("handleAccessDeniedException", e); + ErrorCode errorCode = ErrorCode.FORBIDDEN; + return handleExceptionInternal(errorCode, errorCode.getMessage()); + } + + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getStatus()) .body(makeErrorResponse(errorCode)); diff --git a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..db9e1aa --- /dev/null +++ b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java @@ -0,0 +1,64 @@ +package com.example.gamemate.global.filter; + +import ch.qos.logback.core.util.StringUtil; +import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final UserDetailsService userDetailsService; + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + this.authenticate(request); + filterChain.doFilter(request, response); + } + + private void authenticate(HttpServletRequest request) { + log.info("인증 처리"); + + String token = this.getTokenFromRequest(request); + if(token == null || !jwtTokenProvider.validateToken(token)) { + return; + } + + String username = this.jwtTokenProvider.getEmailFromToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + + private String getTokenFromRequest(HttpServletRequest request) { + final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); + final String headerPrefix = "Bearer "; + + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(headerPrefix)) { + return bearerToken.substring(headerPrefix.length()); + } + return null; + } + +} diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java new file mode 100644 index 0000000..2939e02 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -0,0 +1,69 @@ +package com.example.gamemate.global.provider; + +import com.example.gamemate.domain.user.enums.Role; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + @Value("${spring.jwt.secret}") + private String secretKey; + //Todo 변수명 단위 보이도록 바꿀지 고민 + private final long accessTokenExpirationTime = 1000 * 60 * 60; //60분 + + public String createAccessToken(String email, Role role) { + Claims claims = Jwts.claims().setSubject(email); + claims.put("role", role.getName()); + + Date now = new Date(); + Date validity = new Date(now.getTime() + accessTokenExpirationTime); + Key signingKey = generateSigningKey(); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(signingKey) + .compact(); + } + + + public boolean validateToken(String token) { + try{ + getTokenClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e); + } + } + + public String getEmailFromToken(String token) { + return getTokenClaims(token).getSubject(); + } + + private Key generateSigningKey() { + return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); + } + + + private Claims getTokenClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(generateSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6ccbb84..711ad87 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,5 @@ spring.application.name=gamemate + spring.config.import=optional:file:.env[.properties] spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver @@ -14,4 +15,6 @@ spring.jpa.hibernate.ddl-auto=${JPA_HIBERNATE_DDL} spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy spring.jpa.generate-ddl=false spring.jpa.properties.hibernate.format_sql=true -spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true \ No newline at end of file +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true + +spring.jwt.secret=${JWT_SECRET} \ No newline at end of file From 5da344e2b6f70dd2136eba9ed81af991538255db Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:20:15 +0900 Subject: [PATCH 033/215] =?UTF-8?q?feat:=20add=20=EA=B2=8C=EC=9E=84?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=EB=90=9C=20=20=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 1. 게임등록 요청 2. 게임등록 요청 삭제 3. 게임등록 요청 수정 4. 게임등록 요청 조회 --- .../game/controller/GameController.java | 41 -------- .../GameEnrollRequestController.java | 80 ++++++++++++++++ .../GameEnrollRequestCreateRequestDto.java | 22 +++++ .../dto/GameEnrollRequestResponseDto.java | 34 +++++++ .../GameEnrollRequestUpdateRequestDto.java | 23 +++++ .../game/dto/GameFindByResponseDto.java | 28 ------ .../game/entity/GamaEnrollRequest.java | 59 ++++++++++++ .../GameEnrollRequestRepository.java | 11 +++ .../service/GameEnrollRequestService.java | 96 +++++++++++++++++++ .../gamemate/game/service/GameService.java | 57 +---------- .../like/dto/CreateReviewRequestDto.java | 14 +++ .../like/dto/CreateReviewResponseDto.java | 4 + 12 files changed, 345 insertions(+), 124 deletions(-) create mode 100644 src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java delete mode 100644 src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java create mode 100644 src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java create mode 100644 src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java create mode 100644 src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java create mode 100644 src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java create mode 100644 src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java diff --git a/src/main/java/com/example/gamemate/game/controller/GameController.java b/src/main/java/com/example/gamemate/game/controller/GameController.java index aaa6d1a..d0a7df7 100644 --- a/src/main/java/com/example/gamemate/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/game/controller/GameController.java @@ -94,46 +94,5 @@ public ResponseEntity> searchGame(@RequestParam Stri Page games = gameService.searchGame(keyword, genre, platform, page, size); return ResponseEntity.ok(games); - } - - //게임 요청 관련 - /** - * 게임등록 요청 - * @param requestDto - * @return - */ - @PostMapping("/requests") - public ResponseEntityCreateGameEnrollRequest(@RequestBody GameEnrollRequestCreateRequestDto requestDto){ - - GameEnrollRequestResponseDto responseDto = gameService.createGameEnrollRequest(requestDto); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); - } - /** - * 게임등록 요청 전체 조회 - * @return - */ - @GetMapping("/requests") - public ResponseEntity>findAllGameEnrollRequest(){ - - Page gameEnrollRequestAll = gameService.findAllGameEnrollRequest(); - return ResponseEntity.ok(gameEnrollRequestAll); - } - /** - * 게임등록 요청 단건 조회 - * @param id - * @return - */ - @GetMapping("/requests/{id}") - public ResponseEntity findGameEnrollRequestById(@PathVariable Long id) { - - GameEnrollRequestResponseDto gameEnrollRequestById = gameService.findGameEnrollRequestById(id); - return ResponseEntity.ok(gameEnrollRequestById); - } - - @PatchMapping("/requests/{id}") - public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { - - GameEnrollRequestResponseDto responseDto = gameService.updateGameEnroll(id, requestDto); - return ResponseEntity.ok(responseDto); } } diff --git a/src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java new file mode 100644 index 0000000..818556e --- /dev/null +++ b/src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java @@ -0,0 +1,80 @@ +package com.example.gamemate.game.controller; + +import com.example.gamemate.game.dto.GameEnrollRequestCreateRequestDto; +import com.example.gamemate.game.dto.GameEnrollRequestResponseDto; +import com.example.gamemate.game.dto.GameEnrollRequestUpdateRequestDto; +import com.example.gamemate.game.service.GameEnrollRequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/games/requests") +public class GameEnrollRequestController { + private final GameEnrollRequestService gameEnrollRequestService; + + @Autowired + public GameEnrollRequestController(GameEnrollRequestService gameEnrollRequestService) { + this.gameEnrollRequestService = gameEnrollRequestService; + } + + /** + * 게임등록 요청 + * + * @param requestDto + * @return + */ + @PostMapping + public ResponseEntity CreateGameEnrollRequest(@RequestBody GameEnrollRequestCreateRequestDto requestDto) { + + GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.createGameEnrollRequest(requestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } + + /** + * 게임등록 요청 전체 조회 + * + * @return + */ + @GetMapping + public ResponseEntity> findAllGameEnrollRequest() { + + Page gameEnrollRequestAll = gameEnrollRequestService.findAllGameEnrollRequest(); + return ResponseEntity.ok(gameEnrollRequestAll); + } + + /** + * 게임등록 요청 단건 조회 + * + * @param id + * @return + */ + @GetMapping("/{id}") + public ResponseEntity findGameEnrollRequestById(@PathVariable Long id) { + + GameEnrollRequestResponseDto gameEnrollRequestById = gameEnrollRequestService.findGameEnrollRequestById(id); + return ResponseEntity.ok(gameEnrollRequestById); + } + + /** + * 게임등록 요청 수정 & 게임등록 기능 연계 + * + * @param id + * @param requestDto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { + + GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.updateGameEnroll(id, requestDto); + return ResponseEntity.ok(responseDto); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteGame(@PathVariable Long id) { + gameEnrollRequestService.deleteGame(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java new file mode 100644 index 0000000..be8b891 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.game.dto; + +import lombok.Getter; + +@Getter + +public class GameEnrollRequestCreateRequestDto { + private String title; + private String genre; + private String platform; + private String description; + + + + public GameEnrollRequestCreateRequestDto(String title, String genre, String platform , String description ) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java new file mode 100644 index 0000000..0836ef6 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java @@ -0,0 +1,34 @@ +package com.example.gamemate.game.dto; + +import com.example.gamemate.game.entity.GamaEnrollRequest; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class GameEnrollRequestResponseDto { + private final String message; + private Long id; + private String title; + private String genre; + private String platform; + private String description; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private boolean isAccepted; + + public GameEnrollRequestResponseDto(GamaEnrollRequest gameEnrollRequest ) { + // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.message = "게임등록 요청이 완료되었습니다."; + this.id = gameEnrollRequest.getId(); + this.title = gameEnrollRequest.getTitle(); + this.genre = gameEnrollRequest.getGenre(); + this.platform = gameEnrollRequest.getPlatform(); + this.description = gameEnrollRequest.getDescription(); + this.createdAt = gameEnrollRequest.getCreatedAt(); + this.modifiedAt = gameEnrollRequest.getModifiedAt(); + this.isAccepted = gameEnrollRequest.getIsAccepted(); + + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java new file mode 100644 index 0000000..9bc2761 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java @@ -0,0 +1,23 @@ +package com.example.gamemate.game.dto; + +import lombok.Getter; + +@Getter + +public class GameEnrollRequestUpdateRequestDto { + private String title; + private String genre; + private String platform; + private String description; + private Boolean isAccepted; + + + public GameEnrollRequestUpdateRequestDto(String title, String genre, String platform , String description,Boolean isAccepted ) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + this.isAccepted = isAccepted; + + } +} diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java b/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java deleted file mode 100644 index 3ec3a66..0000000 --- a/src/main/java/com/example/gamemate/game/dto/GameFindByResponseDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.gamemate.game.dto; - -import com.example.gamemate.game.entity.Game; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -public class GameFindByResponseDto { - private final Long id; - private final String title; - private final String genre; - private final String platform; - private final String description; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; - - public GameFindByResponseDto(Game game) { - // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 - this.id = game.getId(); - this.title = game.getTitle(); - this.genre = game.getGenre(); - this.platform = game.getPlatform(); - this.description = game.getDescription(); - this.createdAt = game.getCreatedAt(); - this.modifiedAt = game.getModifiedAt(); - } -} diff --git a/src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java b/src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java new file mode 100644 index 0000000..6620821 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java @@ -0,0 +1,59 @@ +package com.example.gamemate.game.entity; + +import com.example.gamemate.base.BaseEntity; +import com.example.gamemate.review.entity.Review; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Table(name = "game_enroll_request") +@AllArgsConstructor +@NoArgsConstructor +public class GamaEnrollRequest extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title", nullable = false, length = 255, unique = false) + private String title; + + @Column(name = "genre", length = 10) + private String genre; + + @Column(name = "description", length = 255) + private String description; + + @Column(name = "platform", length = 255) + private String platform; + +// @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) +// private List gameImages = new ArrayList<>(); + + @Column(name = "is_accepted", columnDefinition = "BOOLEAN DEFAULT false") + private Boolean isAccepted = false; + + public GamaEnrollRequest(String title, String genre, String platform, String description ) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + + } + + public void updateGameEnroll(String title, String genre, String platform, String description,Boolean isAccepted) { + this.title = title; + this.genre = genre; + this.platform = platform; + this.description = description; + this.isAccepted = isAccepted; + } + + +} diff --git a/src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java b/src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java new file mode 100644 index 0000000..c0d346b --- /dev/null +++ b/src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java @@ -0,0 +1,11 @@ +package com.example.gamemate.game.repository; + +import com.example.gamemate.game.entity.GamaEnrollRequest; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GameEnrollRequestRepository extends JpaRepository { + + List findByIsAccepted(Boolean isAccepted); +} diff --git a/src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java new file mode 100644 index 0000000..6aadef9 --- /dev/null +++ b/src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java @@ -0,0 +1,96 @@ +package com.example.gamemate.game.service; + +import com.example.gamemate.game.dto.*; +import com.example.gamemate.game.entity.GamaEnrollRequest; +import com.example.gamemate.game.entity.Game; +import com.example.gamemate.game.repository.GameEnrollRequestRepository; +import com.example.gamemate.game.repository.GameRepository; +import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; +import com.example.gamemate.review.entity.Review; +import com.example.gamemate.review.repository.ReviewRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.ws.rs.NotFoundException; + + +@Service +@Slf4j +public class GameEnrollRequestService { + private final GameRepository gameRepository; + private final GameEnrollRequestRepository gameEnrollRequestRepository; + + @Autowired + public GameEnrollRequestService(GameRepository gameRepository , GameEnrollRequestRepository gameEnrollRequestRepository) { + + this.gameRepository = gameRepository; + + this.gameEnrollRequestRepository = gameEnrollRequestRepository; + } + + public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto) { + GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + GamaEnrollRequest saveEnrollRequest = gameEnrollRequestRepository.save(gameEnrollRequest); + return new GameEnrollRequestResponseDto(saveEnrollRequest); + } + + public Page findAllGameEnrollRequest() { + + Pageable pageable = PageRequest.of(0, 10); + + return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); + } + + @Transactional + public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { + + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id) + .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); + + return new GameEnrollRequestResponseDto(gamaEnrollRequest); + } + + @Transactional + public GameEnrollRequestResponseDto updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + + gamaEnrollRequest.updateGameEnroll( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription(), + requestDto.getIsAccepted() + ); + GamaEnrollRequest updateGameEnroll = gameEnrollRequestRepository.save(gamaEnrollRequest); + + // 만약에 관리자가 true로 바꾸면 게임등록도 함께 진행됨 + Boolean accepted = requestDto.getIsAccepted(); + if (accepted == true) { + Game game = new Game( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + gameRepository.save(game); + } + return new GameEnrollRequestResponseDto(updateGameEnroll); + } + + public void deleteGame(Long id) { + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); + gameEnrollRequestRepository.delete(gamaEnrollRequest); + } + +} diff --git a/src/main/java/com/example/gamemate/game/service/GameService.java b/src/main/java/com/example/gamemate/game/service/GameService.java index e155974..8b30571 100644 --- a/src/main/java/com/example/gamemate/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/game/service/GameService.java @@ -31,7 +31,7 @@ public class GameService { public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, GameEnrollRequestRepository gameEnrollRequestRepository) { this.gameRepository = gameRepository; - this.reviewRepository=reviewRepository; + this.reviewRepository = reviewRepository; this.gameEnrollRequestRepository = gameEnrollRequestRepository; } @@ -98,57 +98,4 @@ public Page searchGame(String keyword, String genre, Stri return games.map(GameSearchResponseDto::new); } - public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto) { - GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( - requestDto.getTitle(), - requestDto.getGenre(), - requestDto.getPlatform(), - requestDto.getDescription() - ); - GamaEnrollRequest saveEnrollRequest = gameEnrollRequestRepository.save(gameEnrollRequest); - return new GameEnrollRequestResponseDto(saveEnrollRequest); - } - - public Page findAllGameEnrollRequest() { - - Pageable pageable = PageRequest.of(0, 10); - - return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); - } - - @Transactional - public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { - - GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id) - .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); - - return new GameEnrollRequestResponseDto(gamaEnrollRequest); - } - - @Transactional - public GameEnrollRequestResponseDto updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { - GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); - - gamaEnrollRequest.updateGameEnroll( - requestDto.getTitle(), - requestDto.getGenre(), - requestDto.getPlatform(), - requestDto.getDescription(), - requestDto.getIsAccepted() - ); - GamaEnrollRequest updateGameEnroll = gameEnrollRequestRepository.save(gamaEnrollRequest); - - Boolean accepted = requestDto.getIsAccepted(); - if(accepted == true){ - Game game = new Game( - requestDto.getTitle(), - requestDto.getGenre(), - requestDto.getPlatform(), - requestDto.getDescription() - ); - gameRepository.save(game); - } - return new GameEnrollRequestResponseDto(updateGameEnroll); - } - -} +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java b/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java new file mode 100644 index 0000000..8ba24b3 --- /dev/null +++ b/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java @@ -0,0 +1,14 @@ +package com.example.gamemate.like.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CreateReviewRequestDto { + private String status; + + public CreateReviewRequestDto(String status){ + this.status =status; + } +} diff --git a/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java b/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java new file mode 100644 index 0000000..96c569e --- /dev/null +++ b/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java @@ -0,0 +1,4 @@ +package com.example.gamemate.like.dto; + +public class CreateReviewResponseDto { +} From 595667f86e52f6149ae479b66892defc8c2ce252 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 10 Jan 2025 16:53:16 +0900 Subject: [PATCH 034/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=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 1. 게시글 생성 2. 게시글 조회 3. 게시글 검색 4. 게시글 삭제 5. 게시글 삭제 --- build.gradle | 6 +- .../board/controller/BoardController.java | 108 ++++++++++++++ .../board/dto/BoardFindAllResponseDto.java | 32 +++++ .../board/dto/BoardFindOneResponseDto.java | 27 ++++ .../domain/board/dto/BoardRequestDto.java | 22 +++ .../domain/board/dto/BoardResponseDto.java | 19 +++ .../gamemate/domain/board/entity/Board.java | 48 +++++++ .../domain/board/enums/BoardCategory.java | 37 +++++ .../domain/board/enums/BoardListSize.java | 14 ++ .../repository/BoardQuerydslRepository.java | 12 ++ .../BoardQuerydslRepositoryImpl.java | 63 +++++++++ .../board/repository/BoardRepository.java | 17 +++ .../domain/board/service/BoardService.java | 132 ++++++++++++++++++ .../global/config/QuerydslConfig.java | 18 +++ .../gamemate/global/constant/ErrorCode.java | 1 + 15 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/gamemate/domain/board/controller/BoardController.java create mode 100644 src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/board/entity/Board.java create mode 100644 src/main/java/com/example/gamemate/domain/board/enums/BoardCategory.java create mode 100644 src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java create mode 100644 src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepositoryImpl.java create mode 100644 src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/board/service/BoardService.java create mode 100644 src/main/java/com/example/gamemate/global/config/QuerydslConfig.java diff --git a/build.gradle b/build.gradle index c2b577f..bff081e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' implementation 'at.favre.lib:bcrypt:0.10.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' @@ -40,6 +39,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // QueryDSL + implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java new file mode 100644 index 0000000..d71b8b6 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -0,0 +1,108 @@ +package com.example.gamemate.domain.board.controller; + +import com.example.gamemate.domain.board.dto.BoardRequestDto; +import com.example.gamemate.domain.board.dto.BoardResponseDto; +import com.example.gamemate.domain.board.dto.BoardFindAllResponseDto; +import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; +import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.board.service.BoardService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/boards") +public class BoardController { + + private final BoardService boardService; + + /** + * 게시글 생성 API + * @param dto + * @return + */ + @PostMapping + public ResponseEntity createBoard( + @Valid @RequestBody BoardRequestDto dto + ){ + + BoardResponseDto responseDto = boardService.createBoard(dto); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + /** + * 게시글 조회 + * @return + */ + @GetMapping + public ResponseEntity> findAllBoards( + @RequestParam(defaultValue = "0") int page, + @RequestParam(required = false) String category, + @RequestParam(required = false) String title, + @RequestParam(required = false) String content + ) { + + BoardCategory boardCategory = null; + if (category != null) { + boardCategory = BoardCategory.fromName(category); + } + + List dtos = boardService.findAllBoards(page,boardCategory,title,content); + if(dtos.isEmpty()){ + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(dtos, HttpStatus.OK); + } + + /** + * 게시글 단건 조회 API + * @param page + * @param id + * @return + */ + @GetMapping("/{id}") + public ResponseEntity findBoardById( + @RequestParam(required = false, defaultValue = "0") int page, + @PathVariable Long id + ){ + + BoardFindOneResponseDto dto = boardService.findBoardById(page,id); + return new ResponseEntity<>(dto, HttpStatus.OK); + } + + + /** + * 게시글 업데이트 API + * @param id + * @param dto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateBoard( + @PathVariable Long id, + @Valid @RequestBody BoardRequestDto dto + ){ + + BoardResponseDto responseDto = boardService.updateBoard(id, dto); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + /** + * 게시글 삭제 API + * @param id + * @return + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteBoard( + @PathVariable Long id + ){ + + boardService.deleteBoard(id); + return new ResponseEntity<>("삭제되었습니다", HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java new file mode 100644 index 0000000..916bd86 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java @@ -0,0 +1,32 @@ +package com.example.gamemate.domain.board.dto; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.enums.BoardCategory; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class BoardFindAllResponseDto { + private final Long id; + private final BoardCategory category; + private final String title; + private final LocalDateTime createdAt; + private final int views; + + public BoardFindAllResponseDto(Long id, BoardCategory category, String title, LocalDateTime createdAt, int views) { + this.id = id; + this.category = category; + this.title = title; + this.createdAt = createdAt; + this.views = views; + } + + public BoardFindAllResponseDto(Board board) { + this.id = board.getBoardId(); + this.category = board.getCategory(); + this.title = board.getTitle(); + this.createdAt = board.getCreatedAt(); + this.views = board.getViews(); + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java new file mode 100644 index 0000000..fc59278 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java @@ -0,0 +1,27 @@ +package com.example.gamemate.domain.board.dto; + +import com.example.gamemate.domain.board.enums.BoardCategory; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +public class BoardFindOneResponseDto { + private final Long id; + private final BoardCategory category; + private final String title; + private final String content; + //private final String nickname; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; + + public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, LocalDateTime createdAt, LocalDateTime modifiedAt) { + this.id = id; + this.category = category; + this.title = title; + this.content = content; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java new file mode 100644 index 0000000..96ded7a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java @@ -0,0 +1,22 @@ +package com.example.gamemate.domain.board.dto; + +import com.example.gamemate.domain.board.enums.BoardCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class BoardRequestDto { + @NotNull(message = "카테고리를 선택하세요.") + private final BoardCategory category; + @NotBlank(message = "제목을 입력하세요.") + private final String title; + @NotBlank(message = "내용을 입력하세요.") + private final String content; + + public BoardRequestDto(BoardCategory category, String title, String content) { + this.category = category; + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java new file mode 100644 index 0000000..3f28d54 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.board.dto; + +import com.example.gamemate.domain.board.enums.BoardCategory; +import lombok.Getter; + +@Getter +public class BoardResponseDto { + private final Long id; + private final BoardCategory category; + private final String title; + private final String content; + + public BoardResponseDto(Long id, BoardCategory category, String title, String content) { + this.id = id; + this.category = category; + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/entity/Board.java b/src/main/java/com/example/gamemate/domain/board/entity/Board.java new file mode 100644 index 0000000..8301212 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/entity/Board.java @@ -0,0 +1,48 @@ +package com.example.gamemate.domain.board.entity; + +import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.context.annotation.EnableMBeanExport; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "board") +public class Board extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long boardId; + + @Enumerated(EnumType.STRING) + private BoardCategory category; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + private final int views = 0; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + public Board(BoardCategory category, String title, String content) { + this.category = category; + this.title = title; + this.content = content; + } + + public void updateBoard(BoardCategory category, String title, String content) { + this.category = category; + this.title = title; + this.content = content; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/board/enums/BoardCategory.java b/src/main/java/com/example/gamemate/domain/board/enums/BoardCategory.java new file mode 100644 index 0000000..9b5eda4 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/enums/BoardCategory.java @@ -0,0 +1,37 @@ +package com.example.gamemate.domain.board.enums; + +import com.example.gamemate.global.exception.ApiException; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import static com.example.gamemate.global.constant.ErrorCode.INVALID_INPUT; + +@Getter +public enum BoardCategory { + FREE("자유_게시판"), + INFO("정보_게시판"), + RECRUIT("모집_게시판"); + + private final String name; + BoardCategory(String name) { + this.name = name; + } + + @JsonValue + public String getName(){ + return name; + } + + @JsonCreator + public static BoardCategory fromName(String name) { + for(BoardCategory category : BoardCategory.values()){ + if(category.name().equalsIgnoreCase(name) || category.getName().equals(name)){ + return category; + } + } + throw new ApiException(INVALID_INPUT); + } + +} diff --git a/src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java b/src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java new file mode 100644 index 0000000..a874775 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java @@ -0,0 +1,14 @@ +package com.example.gamemate.domain.board.enums; + +import lombok.Getter; + +@Getter +public enum BoardListSize { + LIST_SIZE(15); + + private final int size; + BoardListSize(int size) { + this.size = size; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepository.java b/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepository.java new file mode 100644 index 0000000..b92479d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepository.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.board.repository; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.enums.BoardCategory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface BoardQuerydslRepository { + Page searchBoardQuerydsl(BoardCategory category, String title, String content, Pageable pageable); +} diff --git a/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepositoryImpl.java b/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepositoryImpl.java new file mode 100644 index 0000000..d1ae209 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/repository/BoardQuerydslRepositoryImpl.java @@ -0,0 +1,63 @@ +package com.example.gamemate.domain.board.repository; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.entity.QBoard; +import com.example.gamemate.domain.board.enums.BoardCategory; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; + +@RequiredArgsConstructor +public class BoardQuerydslRepositoryImpl implements BoardQuerydslRepository { + private final JPAQueryFactory jpaQueryFactory; + + /** + * 게시글 조회 + * @param category + * @param title + * @param content + * @return + */ + @Override + public Page searchBoardQuerydsl(BoardCategory category, String title, String content, Pageable pageable) { + + QBoard board = QBoard.board; + + BooleanBuilder builder = new BooleanBuilder(); + + if(category != null) { + builder.and(board.category.eq(category)); + } + + if(title != null) { + builder.and(board.title.like("%"+title+"%")); + } + + if(content != null) { + builder.and(board.content.like("%"+content+"%")); + } + + JPAQuery query = jpaQueryFactory.selectFrom(board) + .where(builder) + .orderBy(new OrderSpecifier<>(Order.DESC, board.createdAt)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()); + + List contentList = query.fetch(); + + JPAQuery countQuery = jpaQueryFactory.select(board.count()) + .from(board) + .where(builder); + + return PageableExecutionUtils.getPage(contentList, pageable, countQuery::fetchOne); + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java b/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java new file mode 100644 index 0000000..e3b3f25 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java @@ -0,0 +1,17 @@ +package com.example.gamemate.domain.board.repository; + +import com.example.gamemate.domain.board.dto.BoardFindAllResponseDto; +import com.example.gamemate.domain.board.entity.Board; +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; + +@Repository +public interface BoardRepository extends JpaRepository, BoardQuerydslRepository{ + Page findByCategory(String category, Pageable pageable); +} diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java new file mode 100644 index 0000000..7e6260e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -0,0 +1,132 @@ +package com.example.gamemate.domain.board.service; + +import com.example.gamemate.domain.board.dto.BoardRequestDto; +import com.example.gamemate.domain.board.dto.BoardResponseDto; +import com.example.gamemate.domain.board.dto.BoardFindAllResponseDto; +import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.board.enums.BoardListSize; +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.global.exception.ApiException; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.gamemate.global.constant.ErrorCode.BOARD_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class BoardService { + + private final BoardRepository boardRepository; + + /** + * 게시글 생성 메서드 + * @param dto + * @return + */ + @Transactional + public BoardResponseDto createBoard(BoardRequestDto dto) { + // 게시글 생성 + Board newBoard = new Board(dto.getCategory(),dto.getTitle(),dto.getContent()); + Board createdBoard = boardRepository.save(newBoard); + return new BoardResponseDto( + createdBoard.getBoardId(), + createdBoard.getCategory(), + createdBoard.getTitle(), + createdBoard.getContent() + ); + } + + /** + * 게시판 리스트 조회 메서드 + * @param page + * @param category + * @return + */ + public List findAllBoards(int page, BoardCategory category, String title, String content) { + + Pageable pageable = PageRequest.of(page, BoardListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.desc("createdAt"))); + + Page boardPage = boardRepository.searchBoardQuerydsl(category, title, content, pageable); + + + return boardPage.stream() + .map(board -> new BoardFindAllResponseDto( + board.getBoardId(), + board.getCategory(), + board.getTitle(), + board.getCreatedAt(), + board.getViews() + )) + .collect(Collectors.toList()); + } + + /** + * 게시글 단건 조회 메서드 + * @param page + * @param id + * @return + */ + public BoardFindOneResponseDto findBoardById(int page, Long id) { + // page는 댓글 페이지네이션을 위해 필요 + // 게시글 조회 + Board findBoard = boardRepository.findById(id) + .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + + return new BoardFindOneResponseDto( + findBoard.getBoardId(), + findBoard.getCategory(), + findBoard.getTitle(), + findBoard.getContent(), + findBoard.getCreatedAt(), + findBoard.getModifiedAt() + ); + + + + } + + /** + * 게시글 업데이트 메서드 + * @param id + * @param dto + * @return + */ + @Transactional + public BoardResponseDto updateBoard(Long id, BoardRequestDto dto) { + // 게시글 조회 + Board findBoard = boardRepository.findById(id) + .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + + findBoard.updateBoard(dto.getCategory(),dto.getTitle(),dto.getContent()); + Board updatedBoard = boardRepository.save(findBoard); + return new BoardResponseDto( + updatedBoard.getBoardId(), + updatedBoard.getCategory(), + updatedBoard.getTitle(), + updatedBoard.getContent() + ); + } + + /** + * 게시글 삭제 메서드 + * @param id + */ + @Transactional + public void deleteBoard(Long id) { + //게시글 조회 + Board findBoard = boardRepository.findById(id) + .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + + boardRepository.delete(findBoard); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/QuerydslConfig.java b/src/main/java/com/example/gamemate/global/config/QuerydslConfig.java new file mode 100644 index 0000000..fe6d172 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/QuerydslConfig.java @@ -0,0 +1,18 @@ +package com.example.gamemate.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 20dd27c..bd628e0 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -18,6 +18,7 @@ public enum ErrorCode { /* 404 찾을 수 없음 */ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), + BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD_NOT_FOUND", "게시글을 찾을 수 없습니다."), /* 500 서버 오류 */ From a0d4660bacd36410c0ca2c591537fbb964ec2893 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 10 Jan 2025 17:35:44 +0900 Subject: [PATCH 035/215] =?UTF-8?q?fix:=20Board=20Controller=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EA=B0=92,=20=EC=9D=91=EB=8B=B5=EA=B0=92=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 1. 수정, 삭제 시에 데이터 반환 없이 204 코드로 응답하도록 수정 --- .../gamemate/domain/board/controller/BoardController.java | 8 ++++---- .../gamemate/domain/board/service/BoardService.java | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index d71b8b6..67f0366 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -83,13 +83,13 @@ public ResponseEntity findBoardById( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateBoard( + public ResponseEntity updateBoard( @PathVariable Long id, @Valid @RequestBody BoardRequestDto dto ){ - BoardResponseDto responseDto = boardService.updateBoard(id, dto); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + boardService.updateBoard(id, dto); + return new ResponseEntity<>("업데이트 되었습니다.", HttpStatus.NO_CONTENT); } /** @@ -103,6 +103,6 @@ public ResponseEntity deleteBoard( ){ boardService.deleteBoard(id); - return new ResponseEntity<>("삭제되었습니다", HttpStatus.OK); + return new ResponseEntity<>("삭제 되었습니다", HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 7e6260e..e5cd31a 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -102,19 +102,13 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { * @return */ @Transactional - public BoardResponseDto updateBoard(Long id, BoardRequestDto dto) { + public void updateBoard(Long id, BoardRequestDto dto) { // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); findBoard.updateBoard(dto.getCategory(),dto.getTitle(),dto.getContent()); Board updatedBoard = boardRepository.save(findBoard); - return new BoardResponseDto( - updatedBoard.getBoardId(), - updatedBoard.getCategory(), - updatedBoard.getTitle(), - updatedBoard.getContent() - ); } /** From 5c60d49de75978b0f61e085b2c26410ce98d41f8 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 18:14:31 +0900 Subject: [PATCH 036/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=88=98=EB=9D=BD/=EA=B1=B0=EC=A0=88=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 18 +++++++++++++---- .../match/dto/MatchUpdateRequestDto.java | 13 ++++++++++++ .../gamemate/domain/match/entity/Match.java | 7 ++++++- .../domain/match/service/MatchService.java | 20 +++++++++++++++++++ .../gamemate/global/constant/ErrorCode.java | 5 +++++ 5 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 1d19951..a9092bb 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,15 +1,13 @@ package com.example.gamemate.domain.match.controller; +import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -27,4 +25,16 @@ public ResponseEntity createMatch(@RequestBody MatchCrea MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto); return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); } + + /** + * 매칭 수락/거절하기 + * @param id 매칭 id + * @param dto MatchUpdateRequestDto 수락/거절 + * @return 204 NO CONTENT + */ + @PatchMapping("/{id}") + public ResponseEntity updateMatch(@PathVariable Long id, @RequestBody MatchUpdateRequestDto dto) { + matchService.updateMatch(id, dto); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } } diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java new file mode 100644 index 0000000..0ad753d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java @@ -0,0 +1,13 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.enums.MatchStatus; +import lombok.Getter; + +@Getter +public class MatchUpdateRequestDto { + private MatchStatus status; + + public MatchUpdateRequestDto(MatchStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/Match.java b/src/main/java/com/example/gamemate/domain/match/entity/Match.java index 5e9fc08..675629d 100644 --- a/src/main/java/com/example/gamemate/domain/match/entity/Match.java +++ b/src/main/java/com/example/gamemate/domain/match/entity/Match.java @@ -2,13 +2,14 @@ import com.example.gamemate.domain.match.enums.MatchStatus; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; @Getter @Entity @Table(name = "matches") -public class Match { +public class Match extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -36,4 +37,8 @@ public Match(String message, User sender, User receiver) { this.sender = sender; this.receiver = receiver; } + + public void updateStatus(MatchStatus status) { + this.status = status; + } } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index e21a27b..63ec931 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.match.service; +import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.entity.Match; import com.example.gamemate.domain.match.enums.MatchStatus; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; @@ -40,4 +41,23 @@ public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { return new MatchCreateResponseDto("매칭이 요청되었습니다."); } + + // 매칭 수락/거절 + // todo : 현재 로그인이 구현되어 있지 않아, receiver 를 1번 유저로 설정. 로그인 구현시 수정필요 + @Transactional + public void updateMatch(Long id, MatchUpdateRequestDto dto) { + Match findMatch = matchRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + + if (findMatch.getStatus() != MatchStatus.PENDING) { + throw new ApiException(ErrorCode.IS_ALREADY_PROCESSED); + } + + User loginUser = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if (loginUser != findMatch.getReceiver()) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + findMatch.updateStatus(dto.getStatus()); + } } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 9c03c36..f47af06 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -13,15 +13,20 @@ public enum ErrorCode { IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 유저입니다."), IS_WITHDRAW_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 유저입니다."), IS_ALREADY_PENDING(HttpStatus.BAD_REQUEST, "IS_ALREADY_PENDING", "이미 대기중인 요청이 있습니다."), + IS_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "IS_ALREADY_PROCESSED", "이미 처리된 요청입니다."), /* 401 세션 없음 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + /* 403 권한 없음 */ + FORBIDDEN(HttpStatus.FORBIDDEN, "FORBIDDEN", "권한이 없습니다."), + /* 404 찾을 수 없음 */ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND,"FOLLOW_NOT_FOUND", "팔로우를 찾을 수 없습니다."), + MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 79ab57d3a58709b0b077b576c089e06defe1c705 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 18:29:55 +0900 Subject: [PATCH 037/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 13 ++++++++++ .../match/dto/MatchFindResponseDto.java | 24 +++++++++++++++++++ .../match/repository/MatchRepository.java | 3 +++ .../domain/match/service/MatchService.java | 11 +++++++++ 4 files changed, 51 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index a9092bb..3375bc5 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.match.controller; +import com.example.gamemate.domain.match.dto.MatchFindResponseDto; import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; @@ -9,6 +10,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/matches") @@ -37,4 +40,14 @@ public ResponseEntity updateMatch(@PathVariable Long id, @RequestBody Matc matchService.updateMatch(id, dto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + /** + * 매칭 전체 조회 + * @return matchFindResponseDtoList + */ + @GetMapping + public ResponseEntity> findAllMatch() { + List matchFindResponseDtoList = matchService.findAllMatch(); + return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java new file mode 100644 index 0000000..1655541 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java @@ -0,0 +1,24 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.match.enums.MatchStatus; +import lombok.Getter; + +@Getter +public class MatchFindResponseDto { + private Long id; + private MatchStatus status; + private String nickname; + private String message; + + public MatchFindResponseDto(Long id, MatchStatus status, String nickname, String message) { + this.id = id; + this.status = status; + this.nickname = nickname; + this.message = message; + } + + public static MatchFindResponseDto toDto(Match match) { + return new MatchFindResponseDto(match.getId(), match.getStatus(), match.getSender().getNickname() , match.getMessage()); + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java index 7ba2708..5f1b967 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java @@ -6,7 +6,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface MatchRepository extends JpaRepository { Boolean existsBySenderAndReceiverAndStatus(User sender, User receiver, MatchStatus status); + List findAllByReceiverId(Long receiverId); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 63ec931..03d8ba4 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.match.service; +import com.example.gamemate.domain.match.dto.MatchFindResponseDto; import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.entity.Match; import com.example.gamemate.domain.match.enums.MatchStatus; @@ -15,6 +16,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class MatchService { @@ -60,4 +63,12 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto) { findMatch.updateStatus(dto.getStatus()); } + + // 매칭 전체 조회 + // todo : 현재 로그인이 구현 되어 있지 않아, 1번 유저의 목록을 불러오도록 설정. 로그인 구현시 수정 필요 + public List findAllMatch() { + List matchList = matchRepository.findAllByReceiverId(1L); + + return matchList.stream().map(MatchFindResponseDto::toDto).toList(); + } } From bd59dbc88589aef800c7ad4124aac21380535fd5 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 18:37:43 +0900 Subject: [PATCH 038/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/match/controller/MatchController.java | 11 +++++++++++ .../gamemate/domain/match/service/MatchService.java | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 3375bc5..0598200 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -50,4 +50,15 @@ public ResponseEntity> findAllMatch() { List matchFindResponseDtoList = matchService.findAllMatch(); return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); } + + /** + * 매칭 단일 조회 + * @param id 매칭 id + * @return matchFindResponseDto + */ + @GetMapping("/{id}") + public ResponseEntity findMatch(@PathVariable Long id) { + MatchFindResponseDto matchFindResponseDto = matchService.findMatch(id); + return new ResponseEntity<>(matchFindResponseDto, HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 03d8ba4..03a97e8 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -71,4 +71,12 @@ public List findAllMatch() { return matchList.stream().map(MatchFindResponseDto::toDto).toList(); } + + // 매칭 단일 조회 + // todo : 현재 로그인이 구현 되어 있지 않아, 간단하게 구현. 로그인 구현시 수정 필요 + public MatchFindResponseDto findMatch(Long id) { + Match findMatch = matchRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + + return MatchFindResponseDto.toDto(findMatch); + } } From 1c110f791ba182c8960af7b90fee191164e87d69 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 19:19:46 +0900 Subject: [PATCH 039/215] =?UTF-8?q?refactor=20:=20=ED=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => controller}/FollowController.java | 47 +++++++++++++------ .../domain/follow/{ => entity}/Follow.java | 2 +- .../{ => repository}/FollowRepository.java | 3 +- .../follow/{ => service}/FollowService.java | 46 +++++++++--------- .../gamemate/domain/user/entity/User.java | 3 +- 5 files changed, 61 insertions(+), 40 deletions(-) rename src/main/java/com/example/gamemate/domain/follow/{ => controller}/FollowController.java (62%) rename src/main/java/com/example/gamemate/domain/follow/{ => entity}/Follow.java (93%) rename src/main/java/com/example/gamemate/domain/follow/{ => repository}/FollowRepository.java (79%) rename src/main/java/com/example/gamemate/domain/follow/{ => service}/FollowService.java (78%) diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java similarity index 62% rename from src/main/java/com/example/gamemate/domain/follow/FollowController.java rename to src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index 23896a4..f117807 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -1,5 +1,6 @@ -package com.example.gamemate.domain.follow; +package com.example.gamemate.domain.follow.controller; +import com.example.gamemate.domain.follow.service.FollowService; import com.example.gamemate.domain.follow.dto.*; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -21,19 +22,25 @@ public class FollowController { * @return message = "팔로우 했습니다." */ @PostMapping - public ResponseEntity createFollow(@RequestBody FollowCreateRequestDto dto) { + public ResponseEntity createFollow( + @RequestBody FollowCreateRequestDto dto + ) { + FollowResponseDto followResponseDto = followService.createFollow(dto); return new ResponseEntity<>(followResponseDto, HttpStatus.CREATED); } /** * 팔로우 취소 - * @param followId 취소할 팔로우 식별자 + * @param id 취소할 팔로우 식별자 * @return message = "팔로우를 취소했습니다." */ - @DeleteMapping("/{followId}") - public ResponseEntity deleteFollow(@PathVariable Long followId) { - FollowResponseDto followResponseDto = followService.deleteFollow(followId); + @DeleteMapping("/{id}") + public ResponseEntity deleteFollow( + @PathVariable Long id + ) { + + FollowResponseDto followResponseDto = followService.deleteFollow(id); return new ResponseEntity<>(followResponseDto,HttpStatus.OK); } @@ -44,7 +51,11 @@ public ResponseEntity deleteFollow(@PathVariable Long followI * @return message = "팔로우 중 입니다." or "아직 팔로우 하지 않았습니다." */ @GetMapping("/status") - public ResponseEntity findFollow(@RequestParam String followerEmail, @RequestParam String followeeEmail) { + public ResponseEntity findFollow( + @RequestParam String followerEmail, + @RequestParam String followeeEmail + ) { + FollowResponseDto followResponseDto = followService.findFollow(followerEmail, followeeEmail); return new ResponseEntity<>(followResponseDto, HttpStatus.OK); } @@ -54,10 +65,13 @@ public ResponseEntity findFollow(@RequestParam String followe * @param email 팔로우 목록을 보고 싶은 유저 email * @return followerList */ - @GetMapping("/follower-list") - public ResponseEntity> findFollowerList(@RequestParam String email) { - List followerList = followService.findFollowerList(email); - return new ResponseEntity<>(followerList, HttpStatus.OK); + @GetMapping("/followers") + public ResponseEntity> findFollowers( + @RequestParam String email + ) { + + List followFindResponseDtoList = followService.findFollowers(email); + return new ResponseEntity<>(followFindResponseDtoList, HttpStatus.OK); } /** @@ -65,9 +79,12 @@ public ResponseEntity> findFollowerList(@RequestPara * @param email 팔로잉 목록을 보고 싶은 유저 email * @return followingList */ - @GetMapping("/following-list") - public ResponseEntity> findFollowingList(@RequestParam String email) { - List followingList = followService.findFollowingList(email); - return new ResponseEntity<>(followingList, HttpStatus.OK); + @GetMapping("/following") + public ResponseEntity> findFollowing( + @RequestParam String email + ) { + + List followFindResponseDtoList = followService.findFollowing(email); + return new ResponseEntity<>(followFindResponseDtoList, HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/follow/Follow.java b/src/main/java/com/example/gamemate/domain/follow/entity/Follow.java similarity index 93% rename from src/main/java/com/example/gamemate/domain/follow/Follow.java rename to src/main/java/com/example/gamemate/domain/follow/entity/Follow.java index dcf7ae3..837a819 100644 --- a/src/main/java/com/example/gamemate/domain/follow/Follow.java +++ b/src/main/java/com/example/gamemate/domain/follow/entity/Follow.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.follow; +package com.example.gamemate.domain.follow.entity; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseCreatedEntity; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java similarity index 79% rename from src/main/java/com/example/gamemate/domain/follow/FollowRepository.java rename to src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java index c0973f4..b91f7b0 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowRepository.java +++ b/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java @@ -1,5 +1,6 @@ -package com.example.gamemate.domain.follow; +package com.example.gamemate.domain.follow.repository; +import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/gamemate/domain/follow/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java similarity index 78% rename from src/main/java/com/example/gamemate/domain/follow/FollowService.java rename to src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 7f60ba6..b7a4555 100644 --- a/src/main/java/com/example/gamemate/domain/follow/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -1,6 +1,8 @@ -package com.example.gamemate.domain.follow; +package com.example.gamemate.domain.follow.service; import com.example.gamemate.domain.follow.dto.*; +import com.example.gamemate.domain.follow.entity.Follow; +import com.example.gamemate.domain.follow.repository.FollowRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; @@ -13,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -24,6 +27,7 @@ public class FollowService { // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional public FollowResponseDto createFollow(FollowCreateRequestDto dto) { + User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -48,8 +52,9 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { // 팔로우 취소하기 // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public FollowResponseDto deleteFollow(Long followId) { - Follow findFollow = followRepository.findById(followId).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); + public FollowResponseDto deleteFollow(Long id) { + + Follow findFollow = followRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (findFollow.getFollower() != follower) { @@ -64,6 +69,7 @@ public FollowResponseDto deleteFollow(Long followId) { // 팔로우 상태 확인 // todo : 로그인한 유저(follower) 기준으로 상대 유저(followee)가 팔로우 되어 있는지 확인이 필요한 것이므로, 로그인 구현시 코드 수정해야함. public FollowResponseDto findFollow(String followerEmail, String followeeEmail) { + User follower = userRepository.findByEmail(followerEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(followeeEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -79,46 +85,44 @@ public FollowResponseDto findFollow(String followerEmail, String followeeEmail) } // 팔로워 목록보기 - public List findFollowerList(String email) { + public List findFollowers(String email) { + User followee = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); } - List FollowListByFollowee = followRepository.findByFollowee(followee); - List FollowerListByFollowee = new ArrayList<>(); + List followListByFollowee = followRepository.findByFollowee(followee); - for (Follow follow : FollowListByFollowee) { - if (follow.getFollower().getUserStatus() != UserStatus.WITHDRAW) { - FollowerListByFollowee.add(follow.getFollower()); - } - } + List followersByFollowee = followListByFollowee.stream() + .map(Follow::getFollower) + .filter(follower -> follower.getUserStatus() != UserStatus.WITHDRAW) + .toList(); - return FollowerListByFollowee + return followersByFollowee .stream() .map(FollowFindResponseDto::toDto) .toList(); } // 팔로잉 목록보기 - public List findFollowingList(String email) { + public List findFollowing(String email) { + User follower = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (follower.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); } - List FollowListByFollower = followRepository.findByFollower(follower); - List FollowingListByFollower = new ArrayList<>(); + List followListByFollower = followRepository.findByFollower(follower); - for (Follow follow : FollowListByFollower) { - if (follow.getFollowee().getUserStatus() != UserStatus.WITHDRAW) { - FollowingListByFollower.add(follow.getFollowee()); - } - } + List followingByFollower = followListByFollower.stream() + .map(Follow::getFollowee) + .filter(followee -> followee.getUserStatus() != UserStatus.WITHDRAW) + .toList(); - return FollowingListByFollower + return followingByFollower .stream() .map(FollowFindResponseDto::toDto) .toList(); diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index a9a425f..7499afb 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -1,6 +1,6 @@ package com.example.gamemate.domain.user.entity; -import com.example.gamemate.domain.follow.Follow; +import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.global.common.BaseEntity; import com.example.gamemate.domain.user.enums.Authority; import com.example.gamemate.domain.user.enums.UserStatus; @@ -8,7 +8,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; import java.util.List; @Entity From ade8e216cd104930454bd65ca9d570e4aaae6493 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 19:23:56 +0900 Subject: [PATCH 040/215] =?UTF-8?q?refactor=20:=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20=EC=B7=A8=EC=86=8C(=EC=82=AD=EC=A0=9C)=20=EC=9D=98?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=EC=9D=84=20204=20NO=5FCONTENT=20=EB=A1=9C?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/follow/controller/FollowController.java | 6 +++--- .../gamemate/domain/follow/service/FollowService.java | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index f117807..ec44cd7 100644 --- a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -36,12 +36,12 @@ public ResponseEntity createFollow( * @return message = "팔로우를 취소했습니다." */ @DeleteMapping("/{id}") - public ResponseEntity deleteFollow( + public ResponseEntity deleteFollow( @PathVariable Long id ) { - FollowResponseDto followResponseDto = followService.deleteFollow(id); - return new ResponseEntity<>(followResponseDto,HttpStatus.OK); + followService.deleteFollow(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index b7a4555..47ee80e 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -52,7 +52,7 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { // 팔로우 취소하기 // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public FollowResponseDto deleteFollow(Long id) { + public void deleteFollow(Long id) { Follow findFollow = followRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -62,8 +62,6 @@ public FollowResponseDto deleteFollow(Long id) { } followRepository.delete(findFollow); - - return new FollowResponseDto("팔로우가 취소되었습니다."); } // 팔로우 상태 확인 From 622b14b0eb178dbfe24fc00d0c59f80637e0465e Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 10 Jan 2025 19:36:28 +0900 Subject: [PATCH 041/215] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=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 1. 댓글 생성 2. 댓글 수정 3. 댓글 삭제 --- .../comment/controller/CommentController.java | 55 ++++++++++++++ .../domain/comment/dto/CommentRequestDto.java | 14 ++++ .../comment/dto/CommentResponseDto.java | 20 ++++++ .../domain/comment/entity/Comment.java | 38 ++++++++++ .../comment/repository/CommentRepository.java | 7 ++ .../comment/service/CommentService.java | 72 +++++++++++++++++++ .../gamemate/global/constant/ErrorCode.java | 1 + 7 files changed, 207 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java create mode 100644 src/main/java/com/example/gamemate/domain/comment/dto/CommentRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/comment/entity/Comment.java create mode 100644 src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/comment/service/CommentService.java diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..47cd742 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -0,0 +1,55 @@ +package com.example.gamemate.domain.comment.controller; + +import com.example.gamemate.domain.comment.dto.CommentRequestDto; +import com.example.gamemate.domain.comment.dto.CommentResponseDto; +import com.example.gamemate.domain.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/boards/{boardId}/comments") +public class CommentController { + + private final CommentService commentService; + + /** + * 댓글 생성 API + * @param boardId + * @param requestDto + * @return + */ + @PostMapping + public ResponseEntity createComment( + @PathVariable Long boardId, + @RequestBody CommentRequestDto requestDto + ){ + CommentResponseDto dto = commentService.createComment(boardId, requestDto); + return new ResponseEntity<>(dto, HttpStatus.CREATED); + } + + /** + * 댓글 수정 API + * @param id + * @param requestDto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateComment( + @PathVariable Long id, + @RequestBody CommentRequestDto requestDto + ){ + commentService.updateComment(id, requestDto); + return new ResponseEntity<>("업데이트 되었습니다", HttpStatus.NO_CONTENT); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteComment( + @PathVariable Long id + ){ + commentService.deleteComment(id); + return new ResponseEntity<>("삭제 되었습니다.", HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/com/example/gamemate/domain/comment/dto/CommentRequestDto.java b/src/main/java/com/example/gamemate/domain/comment/dto/CommentRequestDto.java new file mode 100644 index 0000000..885e563 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/dto/CommentRequestDto.java @@ -0,0 +1,14 @@ +package com.example.gamemate.domain.comment.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CommentRequestDto { + private String content; + + public CommentRequestDto(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java b/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java new file mode 100644 index 0000000..d893b96 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.comment.dto; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CommentResponseDto { + private final Long id; + private final String content; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public CommentResponseDto(Long id, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java new file mode 100644 index 0000000..733513a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java @@ -0,0 +1,38 @@ +package com.example.gamemate.domain.comment.entity; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "comment") +public class Comment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long commentId; + + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "board_id") + private Board board; + + public Comment(String content, Board board) { + this.content = content; + this.board = board; + } + + public void updateComment(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java b/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..94df027 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.example.gamemate.domain.comment.repository; + +import com.example.gamemate.domain.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java new file mode 100644 index 0000000..17cc6ea --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -0,0 +1,72 @@ +package com.example.gamemate.domain.comment.service; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.comment.dto.CommentRequestDto; +import com.example.gamemate.domain.comment.dto.CommentResponseDto; +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + private final BoardRepository boardRepository; + + /** + * 댓글 생성 메서드 + * @param boardId + * @param requestDto + * @return + */ + @Transactional + public CommentResponseDto createComment(Long boardId, CommentRequestDto requestDto) { + // 게시글 조회 + Board findBoard = boardRepository.findById(boardId) + .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)); + + Comment comment = new Comment(requestDto.getContent(), findBoard); + Comment createComment = commentRepository.save(comment); + + return new CommentResponseDto( + createComment.getCommentId(), + createComment.getContent(), + createComment.getCreatedAt(), + createComment.getModifiedAt() + ); + } + + /** + * 댓글 업데이트 메서드 + * @param id + * @param requestDto + */ + @Transactional + public void updateComment(Long id, CommentRequestDto requestDto) { + // 댓글 조회 + Comment findComment = commentRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + findComment.updateComment(requestDto.getContent()); + commentRepository.save(findComment); + } + + /** + * 댓글 삭제 메서드 + * @param id + */ + @Transactional + public void deleteComment(Long id) { + // 댓글 조회 + Comment findComment = commentRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + commentRepository.delete(findComment); + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index bd628e0..2463feb 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -19,6 +19,7 @@ public enum ErrorCode { USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD_NOT_FOUND", "게시글을 찾을 수 없습니다."), + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), /* 500 서버 오류 */ From 2868349642cf87935464b4ba803d21be72952937 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Fri, 10 Jan 2025 19:57:51 +0900 Subject: [PATCH 042/215] =?UTF-8?q?refactor:=20add=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 도메인.game 패키지 추가로 인한 파일 이동 --- .../GameEnrollRequestController.java | 16 +++++------ .../GameEnrollRequestCreateRequestDto.java | 2 +- .../dto/GameEnrollRequestResponseDto.java | 5 ++-- .../GameEnrollRequestUpdateRequestDto.java | 2 +- .../game/dto/GameFindAllResponseDto.java | 6 ++--- .../game/dto/GameFindByIdResponseDto.java | 8 +++--- .../game/dto/GameSearchResponseDto.java | 6 ++--- .../game/dto/GameUpdateRequestDto.java | 2 +- .../game/dto/GameUpdateResponseDto.java | 4 +-- .../GameEnrollRequestRepository.java | 4 +-- .../service/GameEnrollRequestService.java | 27 +++++++++---------- 11 files changed, 39 insertions(+), 43 deletions(-) rename src/main/java/com/example/gamemate/{ => domain}/game/controller/GameEnrollRequestController.java (81%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameEnrollRequestCreateRequestDto.java (90%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameEnrollRequestResponseDto.java (87%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameEnrollRequestUpdateRequestDto.java (92%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameFindAllResponseDto.java (87%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameFindByIdResponseDto.java (86%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameSearchResponseDto.java (89%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameUpdateRequestDto.java (90%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameUpdateResponseDto.java (87%) rename src/main/java/com/example/gamemate/{ => domain}/game/repository/GameEnrollRequestRepository.java (67%) rename src/main/java/com/example/gamemate/{ => domain}/game/service/GameEnrollRequestService.java (79%) diff --git a/src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java similarity index 81% rename from src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java rename to src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 818556e..35ac852 100644 --- a/src/main/java/com/example/gamemate/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -1,9 +1,9 @@ -package com.example.gamemate.game.controller; +package com.example.gamemate.domain.game.controller; -import com.example.gamemate.game.dto.GameEnrollRequestCreateRequestDto; -import com.example.gamemate.game.dto.GameEnrollRequestResponseDto; -import com.example.gamemate.game.dto.GameEnrollRequestUpdateRequestDto; -import com.example.gamemate.game.service.GameEnrollRequestService; +import com.example.gamemate.domain.game.dto.GameEnrollRequestCreateRequestDto; +import com.example.gamemate.domain.game.dto.GameEnrollRequestResponseDto; +import com.example.gamemate.domain.game.dto.GameEnrollRequestUpdateRequestDto; +import com.example.gamemate.domain.game.service.GameEnrollRequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; @@ -68,13 +68,13 @@ public ResponseEntity findGameEnrollRequestById(@P @PatchMapping("/{id}") public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { - GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.updateGameEnroll(id, requestDto); - return ResponseEntity.ok(responseDto); + gameEnrollRequestService.updateGameEnroll(id, requestDto); + return ResponseEntity.noContent().build(); } @DeleteMapping("/{id}") public ResponseEntity deleteGame(@PathVariable Long id) { gameEnrollRequestService.deleteGame(id); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java similarity index 90% rename from src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java index be8b891..9e7c76d 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java similarity index 87% rename from src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java index 0836ef6..29f8308 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java @@ -1,7 +1,6 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.GamaEnrollRequest; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.example.gamemate.domain.game.entity.GamaEnrollRequest; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java similarity index 92% rename from src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java index 9bc2761..5887a8f 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameEnrollRequestUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java similarity index 87% rename from src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java index 69d1ccb..82fa020 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameFindAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java @@ -1,7 +1,7 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; import java.util.List; diff --git a/src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java similarity index 86% rename from src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java index 9662770..3c36bb8 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java @@ -1,14 +1,12 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.Getter; import org.springframework.data.domain.Page; import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; @Getter @JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt", "modifiedAt", "reviews" }) diff --git a/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java similarity index 89% rename from src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java index a3ee6f2..ab9e27f 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameSearchResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java @@ -1,7 +1,7 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java similarity index 90% rename from src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java index 671dca6..998eebf 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java similarity index 87% rename from src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java index b4b4023..7ac7a14 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java @@ -1,6 +1,6 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.Game; +import com.example.gamemate.domain.game.entity.Game; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/GameEnrollRequestRepository.java similarity index 67% rename from src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java rename to src/main/java/com/example/gamemate/domain/game/repository/GameEnrollRequestRepository.java index c0d346b..0e4ec8d 100644 --- a/src/main/java/com/example/gamemate/game/repository/GameEnrollRequestRepository.java +++ b/src/main/java/com/example/gamemate/domain/game/repository/GameEnrollRequestRepository.java @@ -1,6 +1,6 @@ -package com.example.gamemate.game.repository; +package com.example.gamemate.domain.game.repository; -import com.example.gamemate.game.entity.GamaEnrollRequest; +import com.example.gamemate.domain.game.entity.GamaEnrollRequest; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; diff --git a/src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java similarity index 79% rename from src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java rename to src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index 6aadef9..bb2444a 100644 --- a/src/main/java/com/example/gamemate/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -1,19 +1,18 @@ -package com.example.gamemate.game.service; - -import com.example.gamemate.game.dto.*; -import com.example.gamemate.game.entity.GamaEnrollRequest; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.game.repository.GameEnrollRequestRepository; -import com.example.gamemate.game.repository.GameRepository; -import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; -import com.example.gamemate.review.entity.Review; -import com.example.gamemate.review.repository.ReviewRepository; +package com.example.gamemate.domain.game.service; + +import com.example.gamemate.domain.game.dto.GameEnrollRequestCreateRequestDto; +import com.example.gamemate.domain.game.dto.GameEnrollRequestResponseDto; +import com.example.gamemate.domain.game.dto.GameEnrollRequestUpdateRequestDto; + +import com.example.gamemate.domain.game.entity.GamaEnrollRequest; +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.game.repository.GameEnrollRequestRepository; +import com.example.gamemate.domain.game.repository.GameRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -62,8 +61,9 @@ public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { } @Transactional - public GameEnrollRequestResponseDto updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { - GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository + .findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); gamaEnrollRequest.updateGameEnroll( requestDto.getTitle(), @@ -85,7 +85,6 @@ public GameEnrollRequestResponseDto updateGameEnroll(Long id, GameEnrollRequestU ); gameRepository.save(game); } - return new GameEnrollRequestResponseDto(updateGameEnroll); } public void deleteGame(Long id) { From d3bb2cfb28a6cd6f4ab4f1a0687888ddc6f965c7 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Fri, 10 Jan 2025 19:58:56 +0900 Subject: [PATCH 043/215] =?UTF-8?q?refactor:=20add=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 도메인.review 패키지 추가로 인한 파일 이동 --- .../review/controller/ReviewController.java | 19 +++++++++--------- .../review/dto/ReviewCreateRequestDto.java | 6 +----- .../review/dto/ReviewCreateResponseDto.java | 4 ++-- .../dto/ReviewFindByAllResponseDto.java | 4 ++-- .../review/dto/ReviewUpdateRequestDto.java | 2 +- .../review/dto/ReviewUpdateResponseDto.java | 4 ++-- .../{ => domain}/review/entity/Review.java | 10 +++------- .../review/repository/ReviewRepository.java | 9 +++------ .../review/service/ReviewService.java | 20 +++++++++---------- 9 files changed, 33 insertions(+), 45 deletions(-) rename src/main/java/com/example/gamemate/{ => domain}/review/controller/ReviewController.java (72%) rename src/main/java/com/example/gamemate/{ => domain}/review/dto/ReviewCreateRequestDto.java (70%) rename src/main/java/com/example/gamemate/{ => domain}/review/dto/ReviewCreateResponseDto.java (84%) rename src/main/java/com/example/gamemate/{ => domain}/review/dto/ReviewFindByAllResponseDto.java (84%) rename src/main/java/com/example/gamemate/{ => domain}/review/dto/ReviewUpdateRequestDto.java (90%) rename src/main/java/com/example/gamemate/{ => domain}/review/dto/ReviewUpdateResponseDto.java (84%) rename src/main/java/com/example/gamemate/{ => domain}/review/entity/Review.java (81%) rename src/main/java/com/example/gamemate/{ => domain}/review/repository/ReviewRepository.java (56%) rename src/main/java/com/example/gamemate/{ => domain}/review/service/ReviewService.java (74%) diff --git a/src/main/java/com/example/gamemate/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java similarity index 72% rename from src/main/java/com/example/gamemate/review/controller/ReviewController.java rename to src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 3d1cd81..a113334 100644 --- a/src/main/java/com/example/gamemate/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -1,11 +1,10 @@ -package com.example.gamemate.review.controller; +package com.example.gamemate.domain.review.controller; -import com.example.gamemate.review.dto.ReviewCreateRequestDto; -import com.example.gamemate.review.dto.ReviewCreateResponseDto; -import com.example.gamemate.review.dto.ReviewUpdateRequestDto; -import com.example.gamemate.review.dto.ReviewUpdateResponseDto; -import com.example.gamemate.review.entity.Review; -import com.example.gamemate.review.service.ReviewService; +import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; +import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; +import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; +import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; +import com.example.gamemate.domain.review.service.ReviewService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -48,8 +47,8 @@ public ResponseEntity createReview(@PathVariable Long g @PatchMapping("/{id}") public ResponseEntity updateReview(@PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto) { - ReviewUpdateResponseDto responseDto = reviewService.updateReview(gameId, id, requestDto); - return ResponseEntity.ok(responseDto); + reviewService.updateReview(gameId, id, requestDto); + return ResponseEntity.noContent().build(); } /** @@ -63,6 +62,6 @@ public ResponseEntity updateReview(@PathVariable Long g public ResponseEntity deleteReview(@PathVariable Long gameId, @PathVariable Long id) { reviewService.deleteReview(id); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java similarity index 70% rename from src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java index 93bebc5..b002560 100644 --- a/src/main/java/com/example/gamemate/review/dto/ReviewCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java @@ -1,8 +1,4 @@ -package com.example.gamemate.review.dto; - -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.review.entity.Review; -import com.example.gamemate.user.entity.User; +package com.example.gamemate.domain.review.dto; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java similarity index 84% rename from src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java index a5f0fc3..d2d4334 100644 --- a/src/main/java/com/example/gamemate/review/dto/ReviewCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java @@ -1,6 +1,6 @@ -package com.example.gamemate.review.dto; +package com.example.gamemate.domain.review.dto; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java similarity index 84% rename from src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java index 842f146..237b7ae 100644 --- a/src/main/java/com/example/gamemate/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java @@ -1,6 +1,6 @@ -package com.example.gamemate.review.dto; +package com.example.gamemate.domain.review.dto; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java similarity index 90% rename from src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java index e529649..10896f4 100644 --- a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.review.dto; +package com.example.gamemate.domain.review.dto; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java similarity index 84% rename from src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java index ceab845..27dc113 100644 --- a/src/main/java/com/example/gamemate/review/dto/ReviewUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java @@ -1,6 +1,6 @@ -package com.example.gamemate.review.dto; +package com.example.gamemate.domain.review.dto; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/review/entity/Review.java b/src/main/java/com/example/gamemate/domain/review/entity/Review.java similarity index 81% rename from src/main/java/com/example/gamemate/review/entity/Review.java rename to src/main/java/com/example/gamemate/domain/review/entity/Review.java index 794bfae..9588f3f 100644 --- a/src/main/java/com/example/gamemate/review/entity/Review.java +++ b/src/main/java/com/example/gamemate/domain/review/entity/Review.java @@ -1,8 +1,6 @@ -package com.example.gamemate.review.entity; +package com.example.gamemate.domain.review.entity; -import com.example.gamemate.base.BaseEntity; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.user.entity.User; +import com.example.gamemate.domain.game.entity.Game; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,7 +9,7 @@ @Getter @NoArgsConstructor @Table(name = "review") -public class Review extends BaseEntity { +public class Review extends com.example.gamemate.global.common.BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -44,6 +42,4 @@ public void updateReview(String content, Integer star){ this.star =star; } - - } diff --git a/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java b/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java similarity index 56% rename from src/main/java/com/example/gamemate/review/repository/ReviewRepository.java rename to src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java index 0b5534a..4e2c07e 100644 --- a/src/main/java/com/example/gamemate/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java @@ -1,14 +1,11 @@ -package com.example.gamemate.review.repository; +package com.example.gamemate.domain.review.repository; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.like.entity.ReviewLike; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.review.entity.Review; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface ReviewRepository extends JpaRepository { Page findAllByGame(Game game, Pageable pageable); } diff --git a/src/main/java/com/example/gamemate/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java similarity index 74% rename from src/main/java/com/example/gamemate/review/service/ReviewService.java rename to src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 9256361..774e056 100644 --- a/src/main/java/com/example/gamemate/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -1,13 +1,13 @@ -package com.example.gamemate.review.service; - -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.game.repository.GameRepository; -import com.example.gamemate.review.dto.ReviewCreateRequestDto; -import com.example.gamemate.review.dto.ReviewCreateResponseDto; -import com.example.gamemate.review.dto.ReviewUpdateRequestDto; -import com.example.gamemate.review.dto.ReviewUpdateResponseDto; -import com.example.gamemate.review.entity.Review; -import com.example.gamemate.review.repository.ReviewRepository; +package com.example.gamemate.domain.review.service; + +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; +import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; +import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; +import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; +import com.example.gamemate.domain.review.entity.Review; +import com.example.gamemate.domain.review.repository.ReviewRepository; import org.springframework.stereotype.Service; import javax.ws.rs.NotFoundException; From 7861c8a2cf5b7c32da4a2e04de44caf5a289ed45 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 19:49:02 +0900 Subject: [PATCH 044/215] =?UTF-8?q?refactor=20:=20=ED=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../follow/controller/FollowController.java | 2 +- .../domain/follow/service/FollowService.java | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index ec44cd7..591772b 100644 --- a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -33,7 +33,7 @@ public ResponseEntity createFollow( /** * 팔로우 취소 * @param id 취소할 팔로우 식별자 - * @return message = "팔로우를 취소했습니다." + * @return NO_CONTENT */ @DeleteMapping("/{id}") public ResponseEntity deleteFollow( diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 47ee80e..fe7efbd 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -28,8 +28,10 @@ public class FollowService { @Transactional public FollowResponseDto createFollow(FollowCreateRequestDto dto) { - User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - User followee = userRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User follower = userRepository.findById(1L) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User followee = userRepository.findByEmail(dto.getEmail()) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); @@ -54,8 +56,10 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { @Transactional public void deleteFollow(Long id) { - Follow findFollow = followRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); - User follower = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + Follow findFollow = followRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); + User follower = userRepository.findById(1L) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (findFollow.getFollower() != follower) { throw new ApiException(ErrorCode.INVALID_INPUT); @@ -68,8 +72,10 @@ public void deleteFollow(Long id) { // todo : 로그인한 유저(follower) 기준으로 상대 유저(followee)가 팔로우 되어 있는지 확인이 필요한 것이므로, 로그인 구현시 코드 수정해야함. public FollowResponseDto findFollow(String followerEmail, String followeeEmail) { - User follower = userRepository.findByEmail(followerEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - User followee = userRepository.findByEmail(followeeEmail).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User follower = userRepository.findByEmail(followerEmail) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User followee = userRepository.findByEmail(followeeEmail) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); @@ -85,7 +91,8 @@ public FollowResponseDto findFollow(String followerEmail, String followeeEmail) // 팔로워 목록보기 public List findFollowers(String email) { - User followee = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User followee = userRepository.findByEmail(email) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); @@ -107,7 +114,8 @@ public List findFollowers(String email) { // 팔로잉 목록보기 public List findFollowing(String email) { - User follower = userRepository.findByEmail(email).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User follower = userRepository.findByEmail(email) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (follower.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); From c997e5048d19b5914aece76453b70c1d044b1749 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:48:20 +0900 Subject: [PATCH 045/215] =?UTF-8?q?feat:=20add=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20,=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=BD=94=EB=93=9C=20=ED=86=B5=ED=95=A9=20/games?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게임 전체 조회 , 검색 코드 통합 /games 로 통합하여 사용 --- .../game/controller/GameController.java | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) rename src/main/java/com/example/gamemate/{ => domain}/game/controller/GameController.java (54%) diff --git a/src/main/java/com/example/gamemate/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java similarity index 54% rename from src/main/java/com/example/gamemate/game/controller/GameController.java rename to src/main/java/com/example/gamemate/domain/game/controller/GameController.java index d0a7df7..b07785c 100644 --- a/src/main/java/com/example/gamemate/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -1,13 +1,17 @@ -package com.example.gamemate.game.controller; +package com.example.gamemate.domain.game.controller; -import com.example.gamemate.game.dto.*; -import com.example.gamemate.game.service.GameService; +import com.example.gamemate.domain.game.dto.*; +import com.example.gamemate.domain.game.service.GameService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/games") @@ -17,20 +21,29 @@ public class GameController { @Autowired public GameController(GameService gameService) { + this.gameService = gameService; } /** - * 게임생성 - * - * @param gameCreateRequestDto + * @param requestDto + * @param file * @return */ - @PostMapping + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createGame(@RequestBody GameCreateRequestDto gameCreateRequestDto) { + public ResponseEntity createGame(@RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile file) { - GameCreateResponseDto responseDto = gameService.createGame(gameCreateRequestDto); + ObjectMapper mapper = new ObjectMapper(); + GameCreateRequestDto requestDto; + try { + requestDto = mapper.readValue(gameDataString, GameCreateRequestDto.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("Invalid JSON format", e); + } + + GameCreateResponseDto responseDto = gameService.createGame(requestDto, file); return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); } @@ -42,10 +55,22 @@ public ResponseEntity createGame(@RequestBody GameCreateR * @return */ @GetMapping - public ResponseEntity> findAllGame(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int szie) { + public ResponseEntity> findAllGame(@RequestParam(required = false) String keyword, + @RequestParam(required = false) String genre, + @RequestParam(required = false) String platform, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + + log.info("Search parameters - keyword: {}, genre: {}, platform: {}, page: {}, size: {}", + keyword, genre, platform, page, size); + Page games; + if (keyword != null || genre != null || platform != null) { + games = gameService.searchGame(keyword, genre, platform, page, size); + } else { + games = gameService.findAllGame(page, size); + } + - Page games = gameService.findAllGame(page, szie); return ResponseEntity.ok(games); } @@ -72,27 +97,13 @@ public ResponseEntity findGameById(@PathVariable Long i @PatchMapping("/{id}") public ResponseEntity updateGame(@PathVariable Long id, @RequestBody GameUpdateRequestDto requestDto) { - GameUpdateResponseDto responseDto = gameService.updateGame(id, requestDto); - return ResponseEntity.ok(responseDto); + gameService.updateGame(id, requestDto); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") public ResponseEntity deleteGame(@PathVariable Long id) { gameService.deleteGame(id); - return ResponseEntity.ok().build(); - } - - @GetMapping("/search") - public ResponseEntity> searchGame(@RequestParam String keyword, - @RequestParam(required = false) String genre, - @RequestParam(required = false) String platform, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - // 파라미터 값 확인을 위한 로깅 - log.info("Search parameters - keyword: {}, genre: {}, platform: {}, page: {}, size: {}", - keyword, platform, genre, page, size); - Page games = gameService.searchGame(keyword, genre, platform, page, size); - return ResponseEntity.ok(games); - + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } From 4cd51332b7857088d4201478b1f6614d19d119d7 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 10 Jan 2025 20:56:32 +0900 Subject: [PATCH 046/215] =?UTF-8?q?refactor=20:=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20=EC=9A=94=EC=B2=AD,=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 간단한 메세지 반환에서 구체적인 follow 에 대한 정보를 알려주도록 리팩토링 --- .../follow/controller/FollowController.java | 14 ++++++------- .../follow/dto/FollowBooleanResponseDto.java | 16 ++++++++++++++ .../domain/follow/dto/FollowResponseDto.java | 14 ++++++++++--- .../domain/follow/service/FollowService.java | 21 +++++++++++++++---- 4 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/follow/dto/FollowBooleanResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index 591772b..1803f21 100644 --- a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -19,7 +19,7 @@ public class FollowController { /** * 팔로우 하기 * @param dto FollowCreateRequestDto - * @return message = "팔로우 했습니다." + * @return followResponseDto */ @PostMapping public ResponseEntity createFollow( @@ -48,22 +48,22 @@ public ResponseEntity deleteFollow( * 팔로우 상태 확인 (follower 가 followee 를 팔로우 했는지 확인) * @param followerEmail * @param followeeEmail - * @return message = "팔로우 중 입니다." or "아직 팔로우 하지 않았습니다." + * @return followBooleanResponseDto */ @GetMapping("/status") - public ResponseEntity findFollow( + public ResponseEntity findFollow( @RequestParam String followerEmail, @RequestParam String followeeEmail ) { - FollowResponseDto followResponseDto = followService.findFollow(followerEmail, followeeEmail); - return new ResponseEntity<>(followResponseDto, HttpStatus.OK); + FollowBooleanResponseDto followBooleanResponseDto = followService.findFollow(followerEmail, followeeEmail); + return new ResponseEntity<>(followBooleanResponseDto, HttpStatus.OK); } /** * 팔로우 목록 보기 * @param email 팔로우 목록을 보고 싶은 유저 email - * @return followerList + * @return followFindResponseDtoList */ @GetMapping("/followers") public ResponseEntity> findFollowers( @@ -77,7 +77,7 @@ public ResponseEntity> findFollowers( /** * 팔로잉 목록 보기 * @param email 팔로잉 목록을 보고 싶은 유저 email - * @return followingList + * @return followFindResponseDtoList */ @GetMapping("/following") public ResponseEntity> findFollowing( diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowBooleanResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowBooleanResponseDto.java new file mode 100644 index 0000000..b351d7d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowBooleanResponseDto.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.follow.dto; + +import lombok.Getter; + +@Getter +public class FollowBooleanResponseDto { + private boolean isFollowing; + private Long followerId; + private Long followeeId; + + public FollowBooleanResponseDto(boolean isFollowing, Long followerId, Long followeeId) { + this.isFollowing = isFollowing; + this.followerId = followerId; + this.followeeId = followeeId; + } +} diff --git a/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java b/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java index d16e800..78b0d4f 100644 --- a/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/follow/dto/FollowResponseDto.java @@ -2,11 +2,19 @@ import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class FollowResponseDto { - private String message; + private Long id; + private Long followerId; + private Long followeeId; + private LocalDateTime createdAt; - public FollowResponseDto(String message) { - this.message = message; + public FollowResponseDto(Long id, Long followerId, Long followeeId, LocalDateTime createdAt) { + this.id = id; + this.followerId = followerId; + this.followeeId = followeeId; + this.createdAt = createdAt; } } diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index fe7efbd..0947b59 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -48,7 +48,12 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { Follow follow = new Follow(follower, followee); followRepository.save(follow); - return new FollowResponseDto("팔로우 했습니다."); + return new FollowResponseDto( + follow.getId(), + follow.getFollower().getId(), + follow.getFollowee().getId(), + follow.getCreatedAt() + ); } // 팔로우 취소하기 @@ -70,7 +75,7 @@ public void deleteFollow(Long id) { // 팔로우 상태 확인 // todo : 로그인한 유저(follower) 기준으로 상대 유저(followee)가 팔로우 되어 있는지 확인이 필요한 것이므로, 로그인 구현시 코드 수정해야함. - public FollowResponseDto findFollow(String followerEmail, String followeeEmail) { + public FollowBooleanResponseDto findFollow(String followerEmail, String followeeEmail) { User follower = userRepository.findByEmail(followerEmail) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -82,10 +87,18 @@ public FollowResponseDto findFollow(String followerEmail, String followeeEmail) } if (!followRepository.existsByFollowerAndFollowee(follower, followee)) { - return new FollowResponseDto("아직 팔로우 하지 않았습니다."); + return new FollowBooleanResponseDto( + false, + follower.getId(), + followee.getId() + ); } - return new FollowResponseDto("팔로우 중 입니다."); + return new FollowBooleanResponseDto( + true, + follower.getId(), + followee.getId() + ); } // 팔로워 목록보기 From 587b7d64443d004d0de8843bcb6c7433e2221e3b Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Sat, 11 Jan 2025 02:03:28 +0900 Subject: [PATCH 047/215] =?UTF-8?q?feat:=20add=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B2=A8=EB=B6=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B4=80=EB=A0=A8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게임 등록시 이미지 첨부 파일 기능 구현 2. 게임 삭제시 이미지 데이터 삭제 기능 구현 --- .../game/controller/GameController.java | 1 + .../game/dto/GameCreateRequestDto.java | 7 +- .../game/dto/GameCreateResponseDto.java | 11 ++- .../game/dto/GameFindAllResponseDto.java | 6 ++ .../game/dto/GameFindByIdResponseDto.java | 8 +- .../{ => domain}/game/entity/Game.java | 24 ++++-- .../domain/game/entity/GameImage.java | 39 ++++++++++ .../game/repository/GameImageRepository.java | 11 +++ .../game/repository/GameRepository.java | 5 +- .../game/service/GameService.java | 73 +++++++++++++------ .../domain/game/service/S3Service.java | 57 +++++++++++++++ .../gamemate/global/config/S3Config.java | 32 ++++++++ 12 files changed, 237 insertions(+), 37 deletions(-) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameCreateRequestDto.java (80%) rename src/main/java/com/example/gamemate/{ => domain}/game/dto/GameCreateResponseDto.java (65%) rename src/main/java/com/example/gamemate/{ => domain}/game/entity/Game.java (67%) create mode 100644 src/main/java/com/example/gamemate/domain/game/entity/GameImage.java create mode 100644 src/main/java/com/example/gamemate/domain/game/repository/GameImageRepository.java rename src/main/java/com/example/gamemate/{ => domain}/game/repository/GameRepository.java (90%) rename src/main/java/com/example/gamemate/{ => domain}/game/service/GameService.java (53%) create mode 100644 src/main/java/com/example/gamemate/domain/game/service/S3Service.java create mode 100644 src/main/java/com/example/gamemate/global/config/S3Config.java diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index b07785c..2873610 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -103,6 +103,7 @@ public ResponseEntity updateGame(@PathVariable Long id, @ @DeleteMapping("/{id}") public ResponseEntity deleteGame(@PathVariable Long id) { + gameService.deleteGame(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java similarity index 80% rename from src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java index 92f8633..b385cac 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java @@ -1,9 +1,11 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; import lombok.Getter; -import java.time.LocalDateTime; +import lombok.NoArgsConstructor; + @Getter +@NoArgsConstructor public class GameCreateRequestDto { private String title; private String genre; @@ -19,4 +21,5 @@ public GameCreateRequestDto(String title, String genre, String platform , String this.description = description; } + } diff --git a/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java similarity index 65% rename from src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java index d7af61e..4414f31 100644 --- a/src/main/java/com/example/gamemate/game/dto/GameCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java @@ -1,6 +1,6 @@ -package com.example.gamemate.game.dto; +package com.example.gamemate.domain.game.dto; -import com.example.gamemate.game.entity.Game; +import com.example.gamemate.domain.game.entity.Game; import lombok.Getter; import java.time.LocalDateTime; @@ -14,6 +14,8 @@ public class GameCreateResponseDto { private String description; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; + private final String fileName; + private final String imageUrl; public GameCreateResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -24,6 +26,9 @@ public GameCreateResponseDto(Game game) { this.description = game.getDescription(); this.createdAt = game.getCreatedAt(); this.modifiedAt =game.getModifiedAt(); - + this.fileName = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFileName(); + this.imageUrl = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFilePath(); } } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java index 82fa020..b3e8c60 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java @@ -14,6 +14,8 @@ public class GameFindAllResponseDto { private final String platform; private final Long reviewCount; private final Double averageStar; + private final String fileName; + private final String imageUrl; public GameFindAllResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -23,6 +25,10 @@ public GameFindAllResponseDto(Game game) { this.platform = game.getPlatform(); this.reviewCount = (long) game.getReviews().size(); this.averageStar = calculateAverageStar(game.getReviews()); + this.fileName = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFileName(); + this.imageUrl = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFilePath(); } private Double calculateAverageStar(List reviews) { diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java index 3c36bb8..517a707 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Getter -@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt", "modifiedAt", "reviews" }) +@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt","fileName","imageUrl", "modifiedAt", "reviews" }) public class GameFindByIdResponseDto { private final Long id; private final String title; @@ -18,6 +18,8 @@ public class GameFindByIdResponseDto { private final String description; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; + private final String fileName; + private final String imageUrl; private final Page reviews; // private final List reviews; @@ -30,6 +32,10 @@ public GameFindByIdResponseDto(Game game, Page revie this.description = game.getDescription(); this.createdAt = game.getCreatedAt(); this.modifiedAt = game.getModifiedAt(); + this.fileName = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFileName(); + this.imageUrl = game.getImages().isEmpty() ? null : + game.getImages().get(0).getFilePath(); this.reviews = reviews; // this.reviews = game.getReviews().stream() // .map(ReviewFindByAllResponseDto::new) diff --git a/src/main/java/com/example/gamemate/game/entity/Game.java b/src/main/java/com/example/gamemate/domain/game/entity/Game.java similarity index 67% rename from src/main/java/com/example/gamemate/game/entity/Game.java rename to src/main/java/com/example/gamemate/domain/game/entity/Game.java index 29f84fe..b4fda84 100644 --- a/src/main/java/com/example/gamemate/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/Game.java @@ -1,7 +1,7 @@ -package com.example.gamemate.game.entity; +package com.example.gamemate.domain.game.entity; -import com.example.gamemate.base.BaseEntity; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.domain.review.entity.Review; +import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,11 +29,11 @@ public class Game extends BaseEntity { @Column(name = "description", nullable = false, length = 255) private String description; - @Column(name = "platform", nullable = false, length = 255) + @Column(name = "platform", nullable = false, length = 20) private String platform; - @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) - private List gameImages = new ArrayList<>(); + @OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true) + private List images = new ArrayList<>(); @OneToMany(mappedBy = "game", fetch = FetchType.LAZY) private List reviews = new ArrayList<>(); @@ -52,6 +52,18 @@ public void updateGame(String title, String genre, String platform, String descr this.description = description; } + public void addImage(GameImage gameImage) { + this.images.add(gameImage); + } + + public void removeGameImage(GameImage gameImage) { + this.images.remove(gameImage); + } + + public void clearImages() { + this.images.clear(); + } + } diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java b/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java new file mode 100644 index 0000000..06a831b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java @@ -0,0 +1,39 @@ +package com.example.gamemate.domain.game.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "game_image") +public class GameImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "game_id") + private Game game; + + @Column(name = "file_name", nullable = false) + private String fileName; + + @Column(name = "file_type", nullable = false) + private String fileType; + + @Column(name = "file_path", nullable = false) + private String filePath; + + public GameImage(String fileName, String fileType, String filePath, Game game) { + this.fileName = fileName; + this.fileType = fileType; + this.filePath = filePath; + this.game = game; + if (game != null) { + game.getImages().add(this); + } + } + +} diff --git a/src/main/java/com/example/gamemate/domain/game/repository/GameImageRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/GameImageRepository.java new file mode 100644 index 0000000..ae82e26 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/repository/GameImageRepository.java @@ -0,0 +1,11 @@ +package com.example.gamemate.domain.game.repository; + +import com.example.gamemate.domain.game.entity.GameImage; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GameImageRepository extends JpaRepository { + // 게임별 이미지 찾기 + List findGameImagesByGameId(Long gameId); +} diff --git a/src/main/java/com/example/gamemate/game/repository/GameRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/GameRepository.java similarity index 90% rename from src/main/java/com/example/gamemate/game/repository/GameRepository.java rename to src/main/java/com/example/gamemate/domain/game/repository/GameRepository.java index 130409f..79ffa82 100644 --- a/src/main/java/com/example/gamemate/game/repository/GameRepository.java +++ b/src/main/java/com/example/gamemate/domain/game/repository/GameRepository.java @@ -1,13 +1,12 @@ -package com.example.gamemate.game.repository; +package com.example.gamemate.domain.game.repository; -import com.example.gamemate.game.entity.Game; +import com.example.gamemate.domain.game.entity.Game; 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 java.util.List; import java.util.Optional; public interface GameRepository extends JpaRepository { diff --git a/src/main/java/com/example/gamemate/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java similarity index 53% rename from src/main/java/com/example/gamemate/game/service/GameService.java rename to src/main/java/com/example/gamemate/domain/game/service/GameService.java index 8b30571..c60f5d6 100644 --- a/src/main/java/com/example/gamemate/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -1,13 +1,13 @@ -package com.example.gamemate.game.service; - -import com.example.gamemate.game.dto.*; -import com.example.gamemate.game.entity.GamaEnrollRequest; -import com.example.gamemate.game.entity.Game; -import com.example.gamemate.game.repository.GameEnrollRequestRepository; -import com.example.gamemate.game.repository.GameRepository; -import com.example.gamemate.review.dto.ReviewFindByAllResponseDto; -import com.example.gamemate.review.entity.Review; -import com.example.gamemate.review.repository.ReviewRepository; +package com.example.gamemate.domain.game.service; + +import com.example.gamemate.domain.game.dto.*; + +import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.game.entity.GameImage; +import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; +import com.example.gamemate.domain.review.entity.Review; +import com.example.gamemate.domain.review.repository.ReviewRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -16,8 +16,10 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.ws.rs.NotFoundException; +import java.io.IOException; @Service @@ -25,27 +27,43 @@ public class GameService { private final GameRepository gameRepository; private final ReviewRepository reviewRepository; - private final GameEnrollRequestRepository gameEnrollRequestRepository; + private final S3Service s3Service; @Autowired - public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, GameEnrollRequestRepository gameEnrollRequestRepository) { + public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, S3Service s3Service) { this.gameRepository = gameRepository; this.reviewRepository = reviewRepository; - this.gameEnrollRequestRepository = gameEnrollRequestRepository; + this.s3Service = s3Service; } - public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto) { + public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto , MultipartFile file) { + // 게임 엔티티 생성 Game game = new Game( gameCreateRequestDto.getTitle(), gameCreateRequestDto.getGenre(), gameCreateRequestDto.getPlatform(), gameCreateRequestDto.getDescription() ); + + if (file != null && !file.isEmpty()) { + try { + String fileUrl = s3Service.uploadFile(file); + GameImage gameImage = new GameImage( + file.getOriginalFilename(), + file.getContentType(), + fileUrl, + game + ); + game.addImage(gameImage); + } catch (IOException e) { + throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e); + } + } + Game savedGame = gameRepository.save(game); return new GameCreateResponseDto(savedGame); - } public Page findAllGame(int page, int size) { @@ -57,7 +75,8 @@ public Page findAllGame(int page, int size) { @Transactional public GameFindByIdResponseDto findGameById(Long id) { - Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); + Game game = gameRepository.findGameById(id) + .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); Page reviewPage = reviewRepository.findAllByGame(game, pageable); @@ -71,8 +90,9 @@ public GameFindByIdResponseDto findGameById(Long id) { } @Transactional - public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto) { - Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + public void updateGame(Long id, GameUpdateRequestDto requestDto) { + Game game = gameRepository.findGameById(id) + .orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); game.updateGame( requestDto.getTitle(), @@ -81,21 +101,30 @@ public GameUpdateResponseDto updateGame(Long id, GameUpdateRequestDto requestDto requestDto.getDescription() ); Game updateGame = gameRepository.save(game); - return new GameUpdateResponseDto(updateGame); } + @Transactional public void deleteGame(Long id) { - Game game = gameRepository.findGameById(id).orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); + Game game = gameRepository.findGameById(id) + .orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); + + // 게임에 연결된 모든 이미지 삭제 + if (!game.getImages().isEmpty()) { + for (GameImage image : game.getImages()) { + s3Service.deleteFile(image.getFilePath()); + } + } + gameRepository.delete(game); } - public Page searchGame(String keyword, String genre, String platform, int page, int size) { + public Page searchGame(String keyword, String genre, String platform, int page, int size) { log.info("Searching games with parameters - keyword: {}, genre: {}, platform: {}", keyword, genre, platform); Pageable pageable = PageRequest.of(page, size); Page games = gameRepository.searchGames(keyword, genre, platform, pageable); - return games.map(GameSearchResponseDto::new); + return games.map(GameFindAllResponseDto::new); } } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/game/service/S3Service.java b/src/main/java/com/example/gamemate/domain/game/service/S3Service.java new file mode 100644 index 0000000..2922b9b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/service/S3Service.java @@ -0,0 +1,57 @@ +package com.example.gamemate.domain.game.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.beans.factory.annotation.Value; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class S3Service { + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String uploadFile(MultipartFile file) throws IOException { + String fileName = createFileName(file.getOriginalFilename()); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + // ACL 설정 제거하고 기본 putObject 사용 + amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, + file.getInputStream(), metadata)); + + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + private String createFileName(String originalFileName) { + return UUID.randomUUID().toString() + "-" + originalFileName; + } + + public void deleteFile(String fileUrl) { + try { + // URL에서 파일 키(경로) 추출 + String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1); + + // S3에서 파일 삭제 + amazonS3Client.deleteObject(bucket, fileName); + } catch (Exception e) { + log.error("파일 삭제 중 오류 발생: {}", e.getMessage()); + throw new RuntimeException("파일 삭제 실패"); + } + + } +} diff --git a/src/main/java/com/example/gamemate/global/config/S3Config.java b/src/main/java/com/example/gamemate/global/config/S3Config.java new file mode 100644 index 0000000..324d4af --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/S3Config.java @@ -0,0 +1,32 @@ +package com.example.gamemate.global.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} From 8a9de516bec6c024a73caa784a7dcfae32c2852c Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Sat, 11 Jan 2025 02:09:33 +0900 Subject: [PATCH 048/215] =?UTF-8?q?refact:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=201=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. PR 전 간단한 코드 정리 --- .../GameEnrollRequestController.java | 4 +- .../game/dto/GameSearchResponseDto.java | 90 +++++++++---------- .../game/entity/GamaEnrollRequest.java | 11 +-- .../review/controller/ReviewController.java | 4 +- .../like/dto/CreateReviewRequestDto.java | 14 --- .../like/dto/CreateReviewResponseDto.java | 4 - 6 files changed, 52 insertions(+), 75 deletions(-) rename src/main/java/com/example/gamemate/{ => domain}/game/entity/GamaEnrollRequest.java (83%) delete mode 100644 src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java delete mode 100644 src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 35ac852..e76ccfe 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -69,12 +69,12 @@ public ResponseEntity findGameEnrollRequestById(@P public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { gameEnrollRequestService.updateGameEnroll(id, requestDto); - return ResponseEntity.noContent().build(); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") public ResponseEntity deleteGame(@PathVariable Long id) { gameEnrollRequestService.deleteGame(id); - return ResponseEntity.noContent().build(); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java index ab9e27f..d8062c1 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java @@ -1,45 +1,45 @@ -package com.example.gamemate.domain.game.dto; - -import com.example.gamemate.domain.game.entity.Game; -import com.example.gamemate.domain.review.entity.Review; -import lombok.Getter; - -import java.time.LocalDateTime; -import java.util.List; - -@Getter -public class GameSearchResponseDto { - private final Long id; - private final String title; - private final String genre; - private final String platform; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; - private final Long reviewCount; - private final Double averageStar; - - public GameSearchResponseDto(Game game) { - // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 - this.id = game.getId(); - this.title = game.getTitle(); - this.genre = game.getGenre(); - this.platform = game.getPlatform(); - this.createdAt = game.getCreatedAt(); - this.modifiedAt = game.getModifiedAt(); - this.reviewCount = (long) game.getReviews().size(); - this.averageStar = calculateAverageStar(game.getReviews()); - } - - private Double calculateAverageStar(List reviews) { - if (reviews.isEmpty()) { - return 0.0; - } - double average = reviews.stream() - .mapToInt(Review::getStar) - .average() - .orElse(0.0); - - // 소수점 둘째 자리에서 반올림 - return Math.round(average * 10.0) / 10.0; - } -} +//package com.example.gamemate.domain.game.dto; +// +//import com.example.gamemate.domain.game.entity.Game; +//import com.example.gamemate.domain.review.entity.Review; +//import lombok.Getter; +// +//import java.time.LocalDateTime; +//import java.util.List; +// +//@Getter +//public class GameSearchResponseDto { +// private final Long id; +// private final String title; +// private final String genre; +// private final String platform; +// private final LocalDateTime createdAt; +// private final LocalDateTime modifiedAt; +// private final Long reviewCount; +// private final Double averageStar; +// +// public GameSearchResponseDto(Game game) { +// // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 +// this.id = game.getId(); +// this.title = game.getTitle(); +// this.genre = game.getGenre(); +// this.platform = game.getPlatform(); +// this.createdAt = game.getCreatedAt(); +// this.modifiedAt = game.getModifiedAt(); +// this.reviewCount = (long) game.getReviews().size(); +// this.averageStar = calculateAverageStar(game.getReviews()); +// } +// +// private Double calculateAverageStar(List reviews) { +// if (reviews.isEmpty()) { +// return 0.0; +// } +// double average = reviews.stream() +// .mapToInt(Review::getStar) +// .average() +// .orElse(0.0); +// +// // 소수점 둘째 자리에서 반올림 +// return Math.round(average * 10.0) / 10.0; +// } +//} diff --git a/src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java similarity index 83% rename from src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java rename to src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java index 6620821..65085df 100644 --- a/src/main/java/com/example/gamemate/game/entity/GamaEnrollRequest.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java @@ -1,15 +1,10 @@ -package com.example.gamemate.game.entity; +package com.example.gamemate.domain.game.entity; -import com.example.gamemate.base.BaseEntity; -import com.example.gamemate.review.entity.Review; +import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; - -import java.util.ArrayList; -import java.util.List; @Entity @Getter @@ -30,7 +25,7 @@ public class GamaEnrollRequest extends BaseEntity { @Column(name = "description", length = 255) private String description; - @Column(name = "platform", length = 255) + @Column(name = "platform", length = 20) private String platform; // @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index a113334..113c7be 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -48,7 +48,7 @@ public ResponseEntity createReview(@PathVariable Long g public ResponseEntity updateReview(@PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto) { reviewService.updateReview(gameId, id, requestDto); - return ResponseEntity.noContent().build(); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** @@ -62,6 +62,6 @@ public ResponseEntity updateReview(@PathVariable Long g public ResponseEntity deleteReview(@PathVariable Long gameId, @PathVariable Long id) { reviewService.deleteReview(id); - return ResponseEntity.noContent().build(); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java b/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java deleted file mode 100644 index 8ba24b3..0000000 --- a/src/main/java/com/example/gamemate/like/dto/CreateReviewRequestDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.gamemate.like.dto; - -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public class CreateReviewRequestDto { - private String status; - - public CreateReviewRequestDto(String status){ - this.status =status; - } -} diff --git a/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java b/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java deleted file mode 100644 index 96c569e..0000000 --- a/src/main/java/com/example/gamemate/like/dto/CreateReviewResponseDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.gamemate.like.dto; - -public class CreateReviewResponseDto { -} From 29eaa242db00d737224b23c282a40b9196fe33fd Mon Sep 17 00:00:00 2001 From: sumyeom Date: Sat, 11 Jan 2025 19:37:08 +0900 Subject: [PATCH 049/215] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게시판 단건 조회 시 댓글 출력되도록 구현 --- .../board/dto/BoardFindOneResponseDto.java | 7 ++++- .../{BoardListSize.java => ListSize.java} | 4 +-- .../domain/board/service/BoardService.java | 27 ++++++++++++++----- .../comment/dto/CommentFindResponseDto.java | 20 ++++++++++++++ .../comment/repository/CommentRepository.java | 7 +++++ 5 files changed, 56 insertions(+), 9 deletions(-) rename src/main/java/com/example/gamemate/domain/board/enums/{BoardListSize.java => ListSize.java} (73%) create mode 100644 src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java index fc59278..77bf9ab 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java @@ -1,10 +1,13 @@ package com.example.gamemate.domain.board.dto; import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; +import com.example.gamemate.domain.comment.entity.Comment; import lombok.Getter; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; @Getter public class BoardFindOneResponseDto { @@ -15,13 +18,15 @@ public class BoardFindOneResponseDto { //private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; + private final List comments; - public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, LocalDateTime createdAt, LocalDateTime modifiedAt) { + public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, LocalDateTime createdAt, LocalDateTime modifiedAt, List comments) { this.id = id; this.category = category; this.title = title; this.content = content; this.createdAt = createdAt; this.modifiedAt = modifiedAt; + this.comments = comments; } } diff --git a/src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java b/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java similarity index 73% rename from src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java rename to src/main/java/com/example/gamemate/domain/board/enums/ListSize.java index a874775..791fb63 100644 --- a/src/main/java/com/example/gamemate/domain/board/enums/BoardListSize.java +++ b/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java @@ -3,11 +3,11 @@ import lombok.Getter; @Getter -public enum BoardListSize { +public enum ListSize { LIST_SIZE(15); private final int size; - BoardListSize(int size) { + ListSize(int size) { this.size = size; } diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index e5cd31a..8ad8c97 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -6,8 +6,11 @@ import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.board.enums.BoardCategory; -import com.example.gamemate.domain.board.enums.BoardListSize; +import com.example.gamemate.domain.board.enums.ListSize; import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.comment.repository.CommentRepository; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -27,6 +30,7 @@ public class BoardService { private final BoardRepository boardRepository; + private final CommentRepository commentRepository; /** * 게시글 생성 메서드 @@ -54,7 +58,7 @@ public BoardResponseDto createBoard(BoardRequestDto dto) { */ public List findAllBoards(int page, BoardCategory category, String title, String content) { - Pageable pageable = PageRequest.of(page, BoardListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.desc("createdAt"))); + Pageable pageable = PageRequest.of(page, ListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.desc("createdAt"))); Page boardPage = boardRepository.searchBoardQuerydsl(category, title, content, pageable); @@ -78,21 +82,32 @@ public List findAllBoards(int page, BoardCategory categ */ public BoardFindOneResponseDto findBoardById(int page, Long id) { // page는 댓글 페이지네이션을 위해 필요 + Pageable pageable = PageRequest.of(page, ListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.asc("createdAt"))); // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + // 댓글 조회 + Page comments = commentRepository.findByBoard(findBoard,pageable); + + List commentDtos = comments.stream() + .map(comment-> new CommentFindResponseDto( + comment.getCommentId(), + comment.getContent(), + comment.getCreatedAt(), + comment.getModifiedAt() + )) + .collect(Collectors.toList()); + return new BoardFindOneResponseDto( findBoard.getBoardId(), findBoard.getCategory(), findBoard.getTitle(), findBoard.getContent(), findBoard.getCreatedAt(), - findBoard.getModifiedAt() + findBoard.getModifiedAt(), + commentDtos ); - - - } /** diff --git a/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java b/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java new file mode 100644 index 0000000..a825982 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.comment.dto; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CommentFindResponseDto { + private final Long commentId; + private final String content; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public CommentFindResponseDto(Long commentId, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.commentId = commentId; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java b/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java index 94df027..2b88a0e 100644 --- a/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/gamemate/domain/comment/repository/CommentRepository.java @@ -1,7 +1,14 @@ package com.example.gamemate.domain.comment.repository; +import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.comment.entity.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface CommentRepository extends JpaRepository { + + Page findByBoard(Board findBoard, Pageable pageable); } From daf7abaa989e0988bb13350b016d166e8e6bff3e Mon Sep 17 00:00:00 2001 From: sumyeom Date: Sun, 12 Jan 2025 14:14:12 +0900 Subject: [PATCH 050/215] =?UTF-8?q?fix:=20Comment=20controller=20update,?= =?UTF-8?q?=20delete=20=EB=A6=AC=ED=84=B4=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. update, delete 시에 리턴 값 void로 수정 --- .../domain/comment/controller/CommentController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index 47cd742..e0cf180 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -37,19 +37,19 @@ public ResponseEntity createComment( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateComment( + public ResponseEntity updateComment( @PathVariable Long id, @RequestBody CommentRequestDto requestDto ){ commentService.updateComment(id, requestDto); - return new ResponseEntity<>("업데이트 되었습니다", HttpStatus.NO_CONTENT); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") - public ResponseEntity deleteComment( + public ResponseEntity deleteComment( @PathVariable Long id ){ commentService.deleteComment(id); - return new ResponseEntity<>("삭제 되었습니다.", HttpStatus.NO_CONTENT); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } From 5061298c11cf5acf647f62d1ee8e975a403cb807 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Sun, 12 Jan 2025 14:40:00 +0900 Subject: [PATCH 051/215] =?UTF-8?q?fix:=20void=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. void -> Void 변경 --- .../gamemate/domain/comment/controller/CommentController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index e0cf180..75906bf 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -37,7 +37,7 @@ public ResponseEntity createComment( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateComment( + public ResponseEntity updateComment( @PathVariable Long id, @RequestBody CommentRequestDto requestDto ){ @@ -46,7 +46,7 @@ public ResponseEntity updateComment( } @DeleteMapping("/{id}") - public ResponseEntity deleteComment( + public ResponseEntity deleteComment( @PathVariable Long id ){ commentService.deleteComment(id); From 4287a27447bff66cf488ebc06a7899dc8d18e09c Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 13 Jan 2025 04:13:07 +0900 Subject: [PATCH 052/215] =?UTF-8?q?refactor=20:=20=EB=A7=A4=EC=B9=AD=20CRU?= =?UTF-8?q?D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매칭 단일 조회, 매칭 전체 조회 - 삭제 받은 매칭 전체 조회, 보낸 매칭 전체 조회 - 추가 보낸 매칭 삭제 - 추가 --- .../match/controller/MatchController.java | 46 ++++++++++++++----- .../match/repository/MatchRepository.java | 3 ++ .../domain/match/service/MatchService.java | 42 ++++++++++++----- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 0598200..80f4c4e 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -24,7 +24,10 @@ public class MatchController { * @return message = "매칭이 요청되었습니다." */ @PostMapping - public ResponseEntity createMatch(@RequestBody MatchCreateRequestDto dto) { + public ResponseEntity createMatch( + @RequestBody MatchCreateRequestDto dto + ) { + MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto); return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); } @@ -36,29 +39,48 @@ public ResponseEntity createMatch(@RequestBody MatchCrea * @return 204 NO CONTENT */ @PatchMapping("/{id}") - public ResponseEntity updateMatch(@PathVariable Long id, @RequestBody MatchUpdateRequestDto dto) { + public ResponseEntity updateMatch( + @PathVariable Long id, + @RequestBody MatchUpdateRequestDto dto + ) { + matchService.updateMatch(id, dto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** - * 매칭 전체 조회 + * 받은 매칭 전체 조회 + * @return matchFindResponseDtoList + */ + @GetMapping("/received-match") + public ResponseEntity> findAllReceivedMatch() { + + List matchFindResponseDtoList = matchService.findAllReceivedMatch(); + return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); + } + + /** + * 받은 매칭 전체 조회 * @return matchFindResponseDtoList */ - @GetMapping - public ResponseEntity> findAllMatch() { - List matchFindResponseDtoList = matchService.findAllMatch(); + @GetMapping("/sent-match") + public ResponseEntity> findAllSentMatch() { + + List matchFindResponseDtoList = matchService.findAllSentMatch(); return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); } /** - * 매칭 단일 조회 + * 매칭 삭제(취소) * @param id 매칭 id - * @return matchFindResponseDto + * @return NO_CONTENT */ - @GetMapping("/{id}") - public ResponseEntity findMatch(@PathVariable Long id) { - MatchFindResponseDto matchFindResponseDto = matchService.findMatch(id); - return new ResponseEntity<>(matchFindResponseDto, HttpStatus.OK); + @DeleteMapping("/{id}") + public ResponseEntity deleteMatch( + @PathVariable Long id + ) { + + matchService.deleteMatch(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java index 5f1b967..cc09ca7 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchRepository.java @@ -10,6 +10,9 @@ @Repository public interface MatchRepository extends JpaRepository { + Boolean existsBySenderAndReceiverAndStatus(User sender, User receiver, MatchStatus status); List findAllByReceiverId(Long receiverId); + List findAllBySenderId(Long senderId); + } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 03a97e8..643de21 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -21,6 +21,7 @@ @Service @RequiredArgsConstructor public class MatchService { + private final UserRepository userRepository; private final MatchRepository matchRepository; @@ -28,8 +29,11 @@ public class MatchService { // todo : 현재 로그인이 구현되어 있지 않아, 로그인 유저를 1번 유저로 설정 @Transactional public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { - User loginUser = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - User receiver = userRepository.findById(dto.getUserId()).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + User loginUser = userRepository.findById(1L) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User receiver = userRepository.findById(dto.getUserId()) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (receiver.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAW_USER); @@ -49,13 +53,16 @@ public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { // todo : 현재 로그인이 구현되어 있지 않아, receiver 를 1번 유저로 설정. 로그인 구현시 수정필요 @Transactional public void updateMatch(Long id, MatchUpdateRequestDto dto) { - Match findMatch = matchRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + + Match findMatch = matchRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); if (findMatch.getStatus() != MatchStatus.PENDING) { throw new ApiException(ErrorCode.IS_ALREADY_PROCESSED); } - User loginUser = userRepository.findById(1L).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User loginUser = userRepository.findById(1L) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (loginUser != findMatch.getReceiver()) { throw new ApiException(ErrorCode.FORBIDDEN); @@ -64,19 +71,32 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto) { findMatch.updateStatus(dto.getStatus()); } - // 매칭 전체 조회 + // 보낸 매칭 전체 조회 // todo : 현재 로그인이 구현 되어 있지 않아, 1번 유저의 목록을 불러오도록 설정. 로그인 구현시 수정 필요 - public List findAllMatch() { + public List findAllReceivedMatch() { + List matchList = matchRepository.findAllByReceiverId(1L); return matchList.stream().map(MatchFindResponseDto::toDto).toList(); } - // 매칭 단일 조회 - // todo : 현재 로그인이 구현 되어 있지 않아, 간단하게 구현. 로그인 구현시 수정 필요 - public MatchFindResponseDto findMatch(Long id) { - Match findMatch = matchRepository.findById(id).orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + // 받은 매칭 전체 조회 + // todo : 현재 로그인이 구현 되어 있지 않아, 1번 유저의 목록을 불러오도록 설정. 로그인 구현시 수정 필요 + public List findAllSentMatch() { + + List matchList = matchRepository.findAllBySenderId(1L); + + return matchList.stream().map(MatchFindResponseDto::toDto).toList(); + } + + // 매치 삭제 (취소) + // todo : 로그인 구현시 로그인한유저가 sender 일때만 삭제 가능하도록 수정 + @Transactional + public void deleteMatch(Long id) { + + Match findMatch = matchRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); - return MatchFindResponseDto.toDto(findMatch); + matchRepository.delete(findMatch); } } From 2dee0b3635f921550c0fe7bda0a84dbcc6bd7c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 13 Jan 2025 07:29:08 +0900 Subject: [PATCH 053/215] =?UTF-8?q?feat:=20JWT=20=EB=8B=A4=EC=A4=91?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보안을 위해 추가 - accessToken - refreshToken --- build.gradle | 2 +- .../auth/controller/AuthController.java | 41 +++++++--- .../auth/dto/EmailLoginResponseDto.java | 4 +- .../auth/dto/TokenRefreshResponseDto.java | 12 +++ .../domain/auth/service/AuthService.java | 74 +++++++++++++++++-- .../user/controller/UserController.java | 27 ++++++- .../gamemate/domain/user/entity/User.java | 15 ++++ .../domain/user/service/UserService.java | 22 ++++++ .../global/config/SecurityConfig.java | 30 ++++++-- .../config/{ => auth}/CustomUserDetails.java | 14 ++-- .../{ => auth}/CustomUserDetailsService.java | 2 +- .../DelegatedAccessDeniedHandler.java | 3 +- .../DelegatedAuthenticationEntryPoint.java | 2 +- .../filter/JwtAuthenticationFilter.java | 58 ++++++++++++++- .../global/provider/JwtTokenProvider.java | 25 +++++-- 15 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/TokenRefreshResponseDto.java rename src/main/java/com/example/gamemate/global/config/{ => auth}/CustomUserDetails.java (75%) rename src/main/java/com/example/gamemate/global/config/{ => auth}/CustomUserDetailsService.java (95%) rename src/main/java/com/example/gamemate/global/config/{ => auth}/DelegatedAccessDeniedHandler.java (92%) rename src/main/java/com/example/gamemate/global/config/{ => auth}/DelegatedAuthenticationEntryPoint.java (95%) diff --git a/build.gradle b/build.gradle index 36ea43d..36057ed 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.querydsl:querydsl-jpa:5.0.1:jakarta' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' implementation 'at.favre.lib:bcrypt:0.10.2' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index b91e843..60edaad 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -1,17 +1,14 @@ package com.example.gamemate.domain.auth.controller; -import com.example.gamemate.domain.auth.dto.EmailLoginRequestDto; -import com.example.gamemate.domain.auth.dto.SignupRequestDto; -import com.example.gamemate.domain.auth.dto.SignupResponseDto; +import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.auth.service.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/auth") @@ -21,16 +18,36 @@ public class AuthController { private final AuthService authService; @PostMapping("/signup") - public ResponseEntity signup(@Valid @RequestBody SignupRequestDto requestDto) { + public ResponseEntity signup( + @Valid @RequestBody SignupRequestDto requestDto + ) { SignupResponseDto responseDto = authService.signup(requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } - @PostMapping("/login") - public ResponseEntity emailLogin(@Valid @RequestBody EmailLoginRequestDto requestDto) { - authService.emailLogin(requestDto); - return new ResponseEntity<>("로그인 되었습니다.", HttpStatus.OK); + public ResponseEntity emailLogin( + @Valid @RequestBody EmailLoginRequestDto requestDto, + HttpServletResponse response + ) { + EmailLoginResponseDto responseDto = authService.emailLogin(requestDto, response); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @PostMapping("/logout") + public ResponseEntity logout( + HttpServletRequest request, + HttpServletResponse response + ) { + authService.logout(request, response); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + @PostMapping("/refresh") + public ResponseEntity refreshToken( + @CookieValue(name = "refresh_token") String refreshToken + ) { + TokenRefreshResponseDto responseDto = authService.refreshAccessToken(refreshToken); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } } diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java index 6a74ee0..018193c 100644 --- a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java @@ -7,8 +7,6 @@ @RequiredArgsConstructor public class EmailLoginResponseDto { - private final String token; - private final String email; - private final String nickname; + private final String accessToken; } diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/TokenRefreshResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/TokenRefreshResponseDto.java new file mode 100644 index 0000000..a20b917 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/TokenRefreshResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.auth.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class TokenRefreshResponseDto { + + private final String accessToken; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index cfb2994..534739e 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -1,15 +1,15 @@ package com.example.gamemate.domain.auth.service; -import com.example.gamemate.domain.auth.dto.EmailLoginRequestDto; -import com.example.gamemate.domain.auth.dto.EmailLoginResponseDto; -import com.example.gamemate.domain.auth.dto.SignupRequestDto; -import com.example.gamemate.domain.auth.dto.SignupResponseDto; +import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -42,7 +42,7 @@ public SignupResponseDto signup(SignupRequestDto requestDto) { return new SignupResponseDto(savedUser); } - public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto) { + public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto, HttpServletResponse response) { User findUser = userRepository.findByEmail(requestDto.getEmail()) .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -55,10 +55,68 @@ public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto) { throw new ApiException(ErrorCode.INVALID_PASSWORD); } - String jwtToken = jwtTokenProvider.createAccessToken(findUser.getEmail(), findUser.getRole()); + String accessToken = jwtTokenProvider.createAccessToken(findUser.getEmail(), findUser.getRole()); + String refreshToken = jwtTokenProvider.createRefreshToken(findUser.getEmail()); - //Todo 로그인응답dto에서 토큰만 주면 되나? - return new EmailLoginResponseDto(jwtToken, findUser.getEmail(), findUser.getNickname()); + findUser.updateRefreshToken(refreshToken); + userRepository.save(findUser); + + addRefreshTokenToCookie(response, refreshToken); + + return new EmailLoginResponseDto(accessToken); + } + + public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { + if(!jwtTokenProvider.validateToken(refreshToken)) { + throw new ApiException(ErrorCode.INVALID_TOKEN); + } + + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + User user = userRepository.findByEmail(email) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if(!refreshToken.equals(user.getRefreshToken())) { + throw new ApiException(ErrorCode.INVALID_TOKEN); + } + + String newAccessToken = jwtTokenProvider.createAccessToken(email, user.getRole()); + return new TokenRefreshResponseDto(newAccessToken); } + public void logout(HttpServletRequest request, HttpServletResponse response) { + String refreshToken = extractRefreshTokenFromCookie(request); + if(refreshToken != null) { + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + userRepository.findByEmail(email).ifPresent(user -> { + user.removeRefreshToken(); + userRepository.save(user); + }); + + Cookie cookie = new Cookie("refresh_token", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + } + + private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie("refresh_token", refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); // HTTPS에서만 전송 + cookie.setPath("/"); + cookie.setMaxAge(3 * 24 * 60 * 60); // 3일 + response.addCookie(cookie); + } + + private String extractRefreshTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("refresh_token".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } } diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index 94ccdea..00ae2c4 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -1,9 +1,12 @@ package com.example.gamemate.domain.user.controller; +import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.user.dto.PasswordUpdateRequestDto; import com.example.gamemate.domain.user.dto.ProfileResponseDto; import com.example.gamemate.domain.user.dto.ProfileUpdateRequestDto; import com.example.gamemate.domain.user.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -15,6 +18,7 @@ @RequestMapping("/users") public class UserController { private final UserService userService; + private final AuthService authService; @GetMapping("/{id}") public ResponseEntity findProfile( @@ -27,23 +31,38 @@ public ResponseEntity findProfile( } @PatchMapping("/{id}") - public ResponseEntity updateProfile( + public ResponseEntity updateProfile( @PathVariable Long id, @Valid @RequestBody ProfileUpdateRequestDto requestDto, @RequestHeader("Authorization") String token) { String jwtToken = token.substring(7); ProfileResponseDto responseDto = userService.updateProfile(id, requestDto.getNewNickname(), jwtToken); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @PatchMapping("/{id}/password") - public ResponseEntity updatePassword( + public ResponseEntity updatePassword( @PathVariable Long id, @Valid @RequestBody PasswordUpdateRequestDto requestDto, @RequestHeader("Authorization") String token) { String jwtToken = token.substring(7); userService.updatePassword(id, requestDto.getOldPassword(), requestDto.getNewPassword(), jwtToken); - return new ResponseEntity<>("비밀번호가 변경되었습니다.", HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + @DeleteMapping("/withdraw") + public ResponseEntity withdraw( + @RequestHeader("Authorization") String token, + HttpServletRequest request, + HttpServletResponse response + ) { + String jwtToken = token.substring(7); + userService.withdrawUser(jwtToken); + + authService.logout(request, response); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + } diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index ccbf109..1bd9f39 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -38,6 +38,8 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private UserStatus userStatus; + private String refreshToken; + public User(String email, String name, String nickname, String password) { this.email = email; this.name = name; @@ -46,6 +48,7 @@ public User(String email, String name, String nickname, String password) { this.role = Role.USER; this.isPremium = false; this.userStatus = UserStatus.ACTIVE; + this.refreshToken = null; } public void updatePassword(String newPassword) { @@ -56,8 +59,20 @@ public void updateProfile(String newNickname) { this.nickname = newNickname; } + public void updateUserStatus(UserStatus status) { + this.userStatus = status; + } + public void deleteSoftly() { markDeletedAt(); } + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void removeRefreshToken() { + this.refreshToken = null; + } + } diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index a9f9791..266b392 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.user.service; +import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.user.dto.ProfileResponseDto; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; @@ -18,6 +19,7 @@ public class UserService { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; private final PasswordEncoder passwordEncoder; + private final AuthService authService; public ProfileResponseDto findProfile(Long id, String token) { @@ -65,6 +67,26 @@ public void updatePassword(Long id, String oldPassword, String newPassword, Stri userRepository.save(findUser); } + public void withdrawUser(String token) { + + validateToken(token); + + String email = jwtTokenProvider.getEmailFromToken(token); + User findUser = userRepository.findByEmail(email) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if(UserStatus.WITHDRAW.equals(findUser.getUserStatus())) { + throw new ApiException(ErrorCode.WITHDRAWN_USER); + } + + findUser.deleteSoftly(); + findUser.updateUserStatus(UserStatus.WITHDRAW); + findUser.removeRefreshToken(); + + userRepository.save(findUser); + + } + private void validateToken(String token) { if(!jwtTokenProvider.validateToken(token)) { throw new ApiException(ErrorCode.INVALID_TOKEN); diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index 05f66b3..715d3c1 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -1,14 +1,17 @@ package com.example.gamemate.global.config; -import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.config.auth.DelegatedAccessDeniedHandler; +import com.example.gamemate.global.config.auth.DelegatedAuthenticationEntryPoint; import com.example.gamemate.global.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -18,6 +21,9 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +/** + *스프링 시큐리티의 인가 및 설정을 담당 + */ @EnableWebSecurity @Configuration @RequiredArgsConstructor @@ -28,19 +34,22 @@ public class SecurityConfig { private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(crsf->crsf.disable()) //CSRF 보호 비활성화 (REST API이므로) + .csrf(csrf->csrf.disable()) //CSRF 보호 비활성화 (REST API이므로) + .sessionManagement(session-> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/signup", "/auth/login").permitAll() - .requestMatchers("/관리자관련url").hasRole("admin") - .anyRequest().authenticated() + .requestMatchers("/v3/api-docs/**", "/swagger-resources/**" ,"/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/auth/signup", "/auth/login", "auth/refresh").permitAll() + //Todo 관리자 접근 가능 url 수정 + .requestMatchers("/관리자관련url").hasRole("admin") + .anyRequest().authenticated() ) .exceptionHandling(hanling-> hanling .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)) - .sessionManagement(session-> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @@ -50,6 +59,11 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } +// @Bean +// public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { +// return config.getAuthenticationManager(); +// } + @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -63,4 +77,4 @@ public RoleHierarchy roleHierarchy() { return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_USER"); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/CustomUserDetails.java b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java similarity index 75% rename from src/main/java/com/example/gamemate/global/config/CustomUserDetails.java rename to src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java index 603fc9b..7d303ed 100644 --- a/src/main/java/com/example/gamemate/global/config/CustomUserDetails.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java @@ -1,7 +1,6 @@ -package com.example.gamemate.global.config; +package com.example.gamemate.global.config.auth; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.enums.Role; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,7 +21,7 @@ public class CustomUserDetails implements UserDetails { @Override public Collection getAuthorities() { - List authorities = new ArrayList<>(); + Collection authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); return authorities; @@ -41,24 +40,23 @@ public String getUsername() { } //사용하지 않을 경우 true 리턴 - //초기값 true로 되어있음 @Override public boolean isAccountNonExpired() { - return UserDetails.super.isAccountNonExpired(); + return true; } @Override public boolean isAccountNonLocked() { - return UserDetails.super.isAccountNonLocked(); + return true; } @Override public boolean isCredentialsNonExpired() { - return UserDetails.super.isCredentialsNonExpired(); + return true; } @Override public boolean isEnabled() { - return UserDetails.super.isEnabled(); + return true; } } diff --git a/src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java similarity index 95% rename from src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java rename to src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java index dfc487d..11c00e1 100644 --- a/src/main/java/com/example/gamemate/global/config/CustomUserDetailsService.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java @@ -1,4 +1,4 @@ -package com.example.gamemate.global.config; +package com.example.gamemate.global.config.auth; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; diff --git a/src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java b/src/main/java/com/example/gamemate/global/config/auth/DelegatedAccessDeniedHandler.java similarity index 92% rename from src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java rename to src/main/java/com/example/gamemate/global/config/auth/DelegatedAccessDeniedHandler.java index dfa4f91..7f3656f 100644 --- a/src/main/java/com/example/gamemate/global/config/DelegatedAccessDeniedHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/DelegatedAccessDeniedHandler.java @@ -1,9 +1,8 @@ -package com.example.gamemate.global.config; +package com.example.gamemate.global.config.auth; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; diff --git a/src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java b/src/main/java/com/example/gamemate/global/config/auth/DelegatedAuthenticationEntryPoint.java similarity index 95% rename from src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java rename to src/main/java/com/example/gamemate/global/config/auth/DelegatedAuthenticationEntryPoint.java index dc7ef81..ce9df55 100644 --- a/src/main/java/com/example/gamemate/global/config/DelegatedAuthenticationEntryPoint.java +++ b/src/main/java/com/example/gamemate/global/config/auth/DelegatedAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package com.example.gamemate.global.config; +package com.example.gamemate.global.config.auth; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java index db9e1aa..e1f1557 100644 --- a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java @@ -10,7 +10,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -19,6 +23,8 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.Collections; +import java.util.List; @Component @RequiredArgsConstructor @@ -30,11 +36,59 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - this.authenticate(request); + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getServletPath(); + return path.startsWith("/auth/login") || + path.startsWith("/auth/signup") || + path.startsWith("/swagger-ui") || + path.startsWith("/v3/api-docs"); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // Authorization 헤더에서 JWT 토큰 추출 + String token = extractToken(request); + + // 토큰이 유효한 경우에만 인증 처리 + if (token != null && jwtTokenProvider.validateToken(token)) { + String email = jwtTokenProvider.getEmailFromToken(token); + Authentication authentication = createAuthentication(email); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); } + private Authentication createAuthentication(String email) { + List authorities = Collections.singletonList( + new SimpleGrantedAuthority("ROLE_USER") + ); + + // UserDetails 객체 생성 + UserDetails userDetails = User.builder() + .username(email) + .password("") // 토큰 기반 인증이므로 비밀번호는 불필요 + .authorities(authorities) + .build(); + + // Authentication 객체 생성 및 반환 + return new UsernamePasswordAuthenticationToken( + userDetails, + "", + authorities + ); + } + + private String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + private void authenticate(HttpServletRequest request) { log.info("인증 처리"); diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java index 2939e02..bc416a4 100644 --- a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -13,20 +14,22 @@ import java.security.Key; import java.util.Date; +@Slf4j @Component public class JwtTokenProvider { @Value("${spring.jwt.secret}") private String secretKey; - //Todo 변수명 단위 보이도록 바꿀지 고민 - private final long accessTokenExpirationTime = 1000 * 60 * 60; //60분 + + private final long accessTokenExpirationMs = 1000 * 60 * 60; //60분 + private final long refreshTokenExpirationMs = 1000 * 60 * 60 * 24 * 3; //3일 public String createAccessToken(String email, Role role) { Claims claims = Jwts.claims().setSubject(email); claims.put("role", role.getName()); Date now = new Date(); - Date validity = new Date(now.getTime() + accessTokenExpirationTime); + Date validity = new Date(now.getTime() + accessTokenExpirationMs); Key signingKey = generateSigningKey(); return Jwts.builder() @@ -37,6 +40,20 @@ public String createAccessToken(String email, Role role) { .compact(); } + public String createRefreshToken(String email) { + Claims claims = Jwts.claims().setSubject(email); + + Date now = new Date(); + Date validity = new Date(now.getTime() + refreshTokenExpirationMs); + Key signingKey = generateSigningKey(); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(signingKey) + .compact(); + } public boolean validateToken(String token) { try{ @@ -64,6 +81,4 @@ private Claims getTokenClaims(String token) { .getBody(); } - - } From a318c0f366f01759ae5d7a0a82dc75f5c24cca62 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:13:54 +0900 Subject: [PATCH 054/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=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 1. 게임 등록 첨부 파일 등록 기능 --- build.gradle | 5 ++ .../game/controller/GameController.java | 17 +++++-- .../domain/game/dto/GameUpdateRequestDto.java | 2 + .../service/GameEnrollRequestService.java | 12 +++-- .../domain/game/service/GameService.java | 51 ++++++++++++++++--- .../domain/review/service/ReviewService.java | 11 ++-- .../gamemate/global/constant/ErrorCode.java | 2 + .../game/service => global/s3}/S3Service.java | 5 +- src/main/resources/application.yml | 11 ++++ 9 files changed, 94 insertions(+), 22 deletions(-) rename src/main/java/com/example/gamemate/{domain/game/service => global/s3}/S3Service.java (90%) create mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index 56ace75..04ef844 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,11 @@ dependencies { // test testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' + + //S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'com.fasterxml.jackson.core:jackson-databind' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 2873610..bf5d873 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -26,7 +26,8 @@ public GameController(GameService gameService) { } /** - * @param requestDto + * + * @param gameDataString * @param file * @return */ @@ -95,9 +96,19 @@ public ResponseEntity findGameById(@PathVariable Long i * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGame(@PathVariable Long id, @RequestBody GameUpdateRequestDto requestDto) { + public ResponseEntity updateGame(@PathVariable Long id, + @RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile newFile) { + + ObjectMapper mapper = new ObjectMapper(); + GameUpdateRequestDto requestDto; + try { + requestDto = mapper.readValue(gameDataString, GameUpdateRequestDto.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("Invalid JSON format", e); + } - gameService.updateGame(id, requestDto); + gameService.updateGame(id, requestDto, newFile); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java index 998eebf..f5e178d 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java @@ -1,8 +1,10 @@ package com.example.gamemate.domain.game.dto; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class GameUpdateRequestDto { private String title; private String genre; diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index bb2444a..5bb23dd 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -8,6 +8,8 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.repository.GameEnrollRequestRepository; import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -16,7 +18,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import javax.ws.rs.NotFoundException; + +import static com.example.gamemate.global.constant.ErrorCode.GAME_NOT_FOUND; @Service @@ -55,7 +58,7 @@ public Page findAllGameEnrollRequest() { public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id) - .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); + .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); return new GameEnrollRequestResponseDto(gamaEnrollRequest); } @@ -63,7 +66,7 @@ public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { @Transactional public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository - .findById(id).orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + .findById(id).orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); gamaEnrollRequest.updateGameEnroll( requestDto.getTitle(), @@ -88,7 +91,8 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD } public void deleteGame(Long id) { - GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id).orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository + .findById(id).orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); gameEnrollRequestRepository.delete(gamaEnrollRequest); } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index c60f5d6..94cb4e9 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -4,10 +4,13 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.entity.GameImage; +import com.example.gamemate.domain.game.repository.GameImageRepository; import com.example.gamemate.domain.game.repository.GameRepository; import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; +import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.s3.S3Service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -18,8 +21,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import javax.ws.rs.NotFoundException; + import java.io.IOException; +import java.util.List; + +import static com.example.gamemate.global.constant.ErrorCode.GAME_NOT_FOUND; @Service @@ -28,13 +34,15 @@ public class GameService { private final GameRepository gameRepository; private final ReviewRepository reviewRepository; private final S3Service s3Service; + private final GameImageRepository gameImageRepository; @Autowired - public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, S3Service s3Service) { + public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, S3Service s3Service,GameImageRepository gameImageRepository) { this.gameRepository = gameRepository; this.reviewRepository = reviewRepository; this.s3Service = s3Service; + this.gameImageRepository=gameImageRepository; } public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto , MultipartFile file) { @@ -76,7 +84,7 @@ public Page findAllGame(int page, int size) { public GameFindByIdResponseDto findGameById(Long id) { Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new NotFoundException("게임이 존재하지 않습니다.")); + .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); Page reviewPage = reviewRepository.findAllByGame(game, pageable); @@ -90,9 +98,38 @@ public GameFindByIdResponseDto findGameById(Long id) { } @Transactional - public void updateGame(Long id, GameUpdateRequestDto requestDto) { + public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile) { Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new NotFoundException("게임이 존해 하지 않습니다.")); + .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + + // 기존 파일이 있고 새 파일이 업로드된 경우 + // 1. 기존 S3 파일 삭제 +// if (!game.getImages().isEmpty()) { +// for (GameImage image : game.getImages()) { +// s3Service.deleteFile(image.getFilePath()); +// } +// } + + List gameImages = gameImageRepository.findGameImagesByGameId(id); + if (!gameImages.isEmpty()) { + gameImageRepository.deleteAll(gameImages); + } + + // 2. 새 파일 업로드 + if (newFile != null && !newFile.isEmpty()) { + try { + String fileUrl = s3Service.uploadFile(newFile); + GameImage gameImage = new GameImage( + newFile.getOriginalFilename(), + newFile.getContentType(), + fileUrl, + game + ); + game.addImage(gameImage); + } catch (IOException e) { + throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e); + } + } game.updateGame( requestDto.getTitle(), @@ -100,13 +137,13 @@ public void updateGame(Long id, GameUpdateRequestDto requestDto) { requestDto.getPlatform(), requestDto.getDescription() ); - Game updateGame = gameRepository.save(game); + gameRepository.save(game); } @Transactional public void deleteGame(Long id) { Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new NotFoundException("게임을 찾을 없습니다.")); + .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); // 게임에 연결된 모든 이미지 삭제 if (!game.getImages().isEmpty()) { diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 774e056..594b800 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -8,9 +8,12 @@ import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; import org.springframework.stereotype.Service; -import javax.ws.rs.NotFoundException; +import static com.example.gamemate.global.constant.ErrorCode.REVIEW_NOT_FOUND; + @Service public class ReviewService { @@ -26,7 +29,7 @@ public ReviewService(ReviewRepository reviewRepository, public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto) { Game game = gameRepository.findById(gameId) - .orElseThrow(() -> new NotFoundException("Game not found")); + .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); Review review = new Review( requestDto.getContent(), @@ -41,7 +44,7 @@ public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto public ReviewUpdateResponseDto updateReview(Long gameId, Long id, ReviewUpdateRequestDto requestDto) { Review review = reviewRepository.findById(id) - .orElseThrow(() -> new NotFoundException("리뷰가 존재하지 않습니다.")); + .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); review.updateReview( requestDto.getContent(), @@ -55,7 +58,7 @@ public ReviewUpdateResponseDto updateReview(Long gameId, Long id, ReviewUpdateRe public void deleteReview(Long id) { Review review = reviewRepository.findById(id) - .orElseThrow(() -> new NotFoundException("리뷰가 존재하지 않습니다.")); + .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); reviewRepository.delete(review); } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 59635a9..062afba 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -22,6 +22,8 @@ public enum ErrorCode { USER_WITHDRAWN(HttpStatus.NOT_FOUND, "USER_WITHDRAWN", "탈퇴한 유저입니다."), FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND,"FOLLOW_NOT_FOUND", "팔로우를 찾을 수 없습니다."), BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD_NOT_FOUND", "게시글을 찾을 수 없습니다."), + GAME_NOT_FOUND(HttpStatus.NOT_FOUND,"GAME_NOT_FOUND","게임을 찾을 수 없습니다."), + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; diff --git a/src/main/java/com/example/gamemate/domain/game/service/S3Service.java b/src/main/java/com/example/gamemate/global/s3/S3Service.java similarity index 90% rename from src/main/java/com/example/gamemate/domain/game/service/S3Service.java rename to src/main/java/com/example/gamemate/global/s3/S3Service.java index 2922b9b..4f72e9e 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/S3Service.java +++ b/src/main/java/com/example/gamemate/global/s3/S3Service.java @@ -1,8 +1,6 @@ -package com.example.gamemate.domain.game.service; +package com.example.gamemate.global.s3; -import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import lombok.RequiredArgsConstructor; @@ -12,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value; import java.io.IOException; -import java.io.InputStream; import java.util.UUID; @Service diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..7601d74 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,11 @@ +cloud: + aws: + credentials: + access-key: YOUR_ACCESS_KEY + secret-key: YOUR_SECRET_KEY + s3: + bucket: YOUR_BUCKET_NAME + region: + static: ap-northeast-2 + stack: + auto: false From 8dde3bf0faa9ed282f7eb190fd28a34f5bb42fae Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 13 Jan 2025 10:18:14 +0900 Subject: [PATCH 055/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1(=EC=9D=B4=EB=A9=94=EC=9D=BC)=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매 10분마다, 사용자에게 새롭게 생성된 알림이 존재한다면 이메일로 새로운 알림이 생성되었다는 이메일 발송 --- build.gradle | 3 + .../example/gamemate/GameMateApplication.java | 4 ++ .../notification/entity/Notification.java | 13 +++- .../repository/NotificationRepository.java | 1 + .../service/NotificationService.java | 55 +++++++++++++++ .../gamemate/global/config/MailConfig.java | 69 +++++++++++++++++++ src/main/resources/application.yml | 11 +++ 7 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/gamemate/global/config/MailConfig.java create mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index f6660dd..132b9d4 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,9 @@ dependencies { // test testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' + + // mail + implementation 'org.springframework.boot:spring-boot-starter-mail' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/GameMateApplication.java b/src/main/java/com/example/gamemate/GameMateApplication.java index 80b6a71..77c5470 100644 --- a/src/main/java/com/example/gamemate/GameMateApplication.java +++ b/src/main/java/com/example/gamemate/GameMateApplication.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableJpaAuditing +@EnableScheduling public class GameMateApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java index e1dfa4a..839cea4 100644 --- a/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java +++ b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java @@ -2,12 +2,14 @@ import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseCreatedEntity; import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; @Entity @Getter -public class Notification { +public class Notification extends BaseCreatedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -20,8 +22,12 @@ public class Notification { private NotificationType type; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") private User user; + @Column + private boolean sentStatus; + public Notification() { } @@ -29,5 +35,10 @@ public Notification(String content, NotificationType type, User user) { this.content = content; this.type = type; this.user = user; + this.sentStatus = false; + } + + public void updateSentStatus(boolean sentStatus) { + this.sentStatus = sentStatus; } } diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index f63d7b2..3652322 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -9,4 +9,5 @@ @Repository public interface NotificationRepository extends JpaRepository { List findAllByUserId(Long userId); + List findAllBySentStatus(boolean sentStatus); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index b774c3b..2a43f84 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -5,17 +5,28 @@ import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.repository.NotificationRepository; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Slf4j public class NotificationService { private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + private final JavaMailSender javaMailSender; // 알림 생성 @Transactional @@ -34,4 +45,48 @@ public List findAllNotification() { .map(NotificationResponseDto::toDto) .toList(); } + + // 알림 발송 (이메일) + @Scheduled(cron = "0 0/10 * * * *") + public void sendNotificationMail() { + + List unnotifiedNotificationList = notificationRepository.findAllBySentStatus(false); + + if (unnotifiedNotificationList.isEmpty()) { + log.info("전송할 알림이 없습니다."); + return; + } + + Map> notificationMap = + unnotifiedNotificationList + .stream() + .collect(Collectors.groupingBy(Notification::getUser)); + + for (Map.Entry> entry : notificationMap.entrySet()) { + User user = entry.getKey(); + List notifications = entry.getValue(); + + try { + SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); + simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 + simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 + simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 + simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 + + javaMailSender.send(simpleMailMessage); + log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); + + updateNotificationStatus(notifications); + } catch (Exception e) { + log.error("알림 메일 전송 실패: {}", user.getEmail(), e); + } + } + } + + // 알림 전송 후 notified(false -> true) 상태 변경 + @Transactional + public void updateNotificationStatus(List notifications) { + notifications.forEach(notification -> notification.updateSentStatus(true)); + notificationRepository.saveAll(notifications); + } } diff --git a/src/main/java/com/example/gamemate/global/config/MailConfig.java b/src/main/java/com/example/gamemate/global/config/MailConfig.java new file mode 100644 index 0000000..92fa5d4 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/MailConfig.java @@ -0,0 +1,69 @@ +package com.example.gamemate.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +@RequiredArgsConstructor +public class MailConfig { + + private static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; + private static final String MAIL_DEBUG = "mail.smtp.debug"; + private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectionTimeout"; + private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable"; + + // SMTP 서버 + @Value("${spring.mail.host}") + private String host; + + // 계정 + @Value("${spring.mail.username}") + private String username; + + // 비밀번호 + @Value("${spring.mail.password}") + private String password; + + // 포트번호 + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.debug}") + private boolean debug; + + @Value("${spring.mail.properties.mail.smtp.connectionTimeout}") + private int connectionTimeout; + + @Value("${spring.mail.properties.mail.starttls.enable}") + private boolean startTlsEnable; + + @Bean + public JavaMailSender javaMailService() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + javaMailSender.setHost(host); + javaMailSender.setUsername(username); + javaMailSender.setPassword(password); + javaMailSender.setPort(port); + + Properties properties = javaMailSender.getJavaMailProperties(); + properties.put(MAIL_SMTP_AUTH, auth); + properties.put(MAIL_DEBUG, debug); + properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout); + properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable); + + javaMailSender.setJavaMailProperties(properties); + javaMailSender.setDefaultEncoding("UTF-8"); + + return javaMailSender; + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d4ef9e2 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + mail: + host: smtp.gmail.com + port: 587 + username: ${GMAIL_ACCOUNT} + password: ${APP_PASSWORD} + properties: + mail.smtp.debug: true + mail.smtp.connectionTimeout: 1000 #1초 + mail.starttls.enable: true + mail.smtp.auth: true From df09fe267d4d0ab355eaf128e71e36cba9aa49c3 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 13 Jan 2025 10:28:37 +0900 Subject: [PATCH 056/215] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/controller/NotificationController.java | 5 +++++ .../notification/repository/NotificationRepository.java | 2 ++ .../domain/notification/service/NotificationService.java | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index 800bda3..dd50213 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -17,8 +17,13 @@ public class NotificationController { private final NotificationService notificationService; + /** + * 알림 전체 보기 + * @return NotificationResponseDtoList + */ @GetMapping public ResponseEntity> findAllNotification() { + List NotificationResponseDtoList = notificationService.findAllNotification(); return new ResponseEntity<>(NotificationResponseDtoList, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index 3652322..0dea4a7 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -8,6 +8,8 @@ @Repository public interface NotificationRepository extends JpaRepository { + List findAllByUserId(Long userId); List findAllBySentStatus(boolean sentStatus); + } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 2a43f84..3db057b 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -25,7 +25,6 @@ public class NotificationService { private final NotificationRepository notificationRepository; - private final UserRepository userRepository; private final JavaMailSender javaMailSender; // 알림 생성 @@ -38,6 +37,7 @@ public void createNotification(User user, NotificationType type) { // 알림 전체 보기 // todo 현재 로그인이 구현되어 있지 않아 1번 유저의 알림 목록을 불러오게 설정, 추후 로그인 구현시 로그인한 유저의 id값을 넣도록 변경 public List findAllNotification() { + List notificationList = notificationRepository.findAllByUserId(1L); return notificationList From f657577ed21d00bcba94907bdf17432dc4cc0495 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:10:23 +0900 Subject: [PATCH 057/215] =?UTF-8?q?refact:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A4=84=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 컨트롤러 메서드에서 파라미터 줄바꿀 때 ( 뒤에서 엔터! --- .../game/controller/GameController.java | 29 +++++++++++-------- .../GameEnrollRequestController.java | 16 ++++++---- .../review/controller/ReviewController.java | 14 +++++++-- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index bf5d873..48cf73e 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -33,8 +33,9 @@ public GameController(GameService gameService) { */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createGame(@RequestPart(value = "gameData") String gameDataString, - @RequestPart(value = "file", required = false) MultipartFile file) { + public ResponseEntity createGame( + @RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile file) { ObjectMapper mapper = new ObjectMapper(); GameCreateRequestDto requestDto; @@ -56,11 +57,12 @@ public ResponseEntity createGame(@RequestPart(value = "ga * @return */ @GetMapping - public ResponseEntity> findAllGame(@RequestParam(required = false) String keyword, - @RequestParam(required = false) String genre, - @RequestParam(required = false) String platform, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { + public ResponseEntity> findAllGame( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String genre, + @RequestParam(required = false) String platform, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { log.info("Search parameters - keyword: {}, genre: {}, platform: {}, page: {}, size: {}", keyword, genre, platform, page, size); @@ -82,7 +84,8 @@ public ResponseEntity> findAllGame(@RequestParam(re * @return */ @GetMapping("/{id}") - public ResponseEntity findGameById(@PathVariable Long id) { + public ResponseEntity findGameById( + @PathVariable Long id) { GameFindByIdResponseDto gameById = gameService.findGameById(id); return ResponseEntity.ok(gameById); @@ -96,9 +99,10 @@ public ResponseEntity findGameById(@PathVariable Long i * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGame(@PathVariable Long id, - @RequestPart(value = "gameData") String gameDataString, - @RequestPart(value = "file", required = false) MultipartFile newFile) { + public ResponseEntity updateGame( + @PathVariable Long id, + @RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile newFile) { ObjectMapper mapper = new ObjectMapper(); GameUpdateRequestDto requestDto; @@ -113,7 +117,8 @@ public ResponseEntity updateGame(@PathVariable Long id, } @DeleteMapping("/{id}") - public ResponseEntity deleteGame(@PathVariable Long id) { + public ResponseEntity deleteGame( + @PathVariable Long id) { gameService.deleteGame(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index e76ccfe..7c6844a 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -16,7 +16,8 @@ public class GameEnrollRequestController { private final GameEnrollRequestService gameEnrollRequestService; @Autowired - public GameEnrollRequestController(GameEnrollRequestService gameEnrollRequestService) { + public GameEnrollRequestController( + GameEnrollRequestService gameEnrollRequestService) { this.gameEnrollRequestService = gameEnrollRequestService; } @@ -27,7 +28,8 @@ public GameEnrollRequestController(GameEnrollRequestService gameEnrollRequestSer * @return */ @PostMapping - public ResponseEntity CreateGameEnrollRequest(@RequestBody GameEnrollRequestCreateRequestDto requestDto) { + public ResponseEntity CreateGameEnrollRequest( + @RequestBody GameEnrollRequestCreateRequestDto requestDto) { GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.createGameEnrollRequest(requestDto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); @@ -52,7 +54,8 @@ public ResponseEntity> findAllGameEnrollReque * @return */ @GetMapping("/{id}") - public ResponseEntity findGameEnrollRequestById(@PathVariable Long id) { + public ResponseEntity findGameEnrollRequestById( + @PathVariable Long id) { GameEnrollRequestResponseDto gameEnrollRequestById = gameEnrollRequestService.findGameEnrollRequestById(id); return ResponseEntity.ok(gameEnrollRequestById); @@ -66,14 +69,17 @@ public ResponseEntity findGameEnrollRequestById(@P * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGameEnroll(@PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { + public ResponseEntity updateGameEnroll( + @PathVariable Long id, + @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { gameEnrollRequestService.updateGameEnroll(id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") - public ResponseEntity deleteGame(@PathVariable Long id) { + public ResponseEntity deleteGame( + @PathVariable Long id) { gameEnrollRequestService.deleteGame(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 113c7be..740acdb 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -18,6 +18,7 @@ public class ReviewController { @Autowired public ReviewController(ReviewService reviewService) { + this.reviewService = reviewService; } @@ -30,7 +31,9 @@ public ReviewController(ReviewService reviewService) { */ @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createReview(@PathVariable Long gameId, @RequestBody ReviewCreateRequestDto requestDto) { + public ResponseEntity createReview( + @PathVariable Long gameId, + @RequestBody ReviewCreateRequestDto requestDto) { ReviewCreateResponseDto responseDto = reviewService.createReview(gameId, requestDto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); @@ -45,7 +48,10 @@ public ResponseEntity createReview(@PathVariable Long g * @return */ @PatchMapping("/{id}") - public ResponseEntity updateReview(@PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto) { + public ResponseEntity updateReview( + @PathVariable Long gameId, + @PathVariable Long id, + @RequestBody ReviewUpdateRequestDto requestDto) { reviewService.updateReview(gameId, id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); @@ -59,7 +65,9 @@ public ResponseEntity updateReview(@PathVariable Long g * @return */ @DeleteMapping("/{id}") - public ResponseEntity deleteReview(@PathVariable Long gameId, @PathVariable Long id) { + public ResponseEntity deleteReview( + @PathVariable Long gameId, + @PathVariable Long id) { reviewService.deleteReview(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); From 1c70c504ee86d06450b53962db6388b54282b2e4 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:46:54 +0900 Subject: [PATCH 058/215] Add .DS_Store to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a00fec8..5526412 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ out/ .vscode/ .env +.DS_Store +**/.DS_Store From 7af9c8abcd8d4e683982561fd9964afa00f5c265 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:53:33 +0900 Subject: [PATCH 059/215] =?UTF-8?q?refact:=20final=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20@RequiredArgsConstructor=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 1. final 생성자는 @RequiredArgsConstructor 설정 --- .../domain/game/service/GameEnrollRequestService.java | 10 ++-------- .../gamemate/domain/game/service/GameService.java | 11 ++--------- .../gamemate/domain/review/service/ReviewService.java | 8 ++------ 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index 5bb23dd..d6f5acc 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -10,6 +10,7 @@ import com.example.gamemate.domain.game.repository.GameRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -24,18 +25,11 @@ @Service @Slf4j +@RequiredArgsConstructor public class GameEnrollRequestService { private final GameRepository gameRepository; private final GameEnrollRequestRepository gameEnrollRequestRepository; - @Autowired - public GameEnrollRequestService(GameRepository gameRepository , GameEnrollRequestRepository gameEnrollRequestRepository) { - - this.gameRepository = gameRepository; - - this.gameEnrollRequestRepository = gameEnrollRequestRepository; - } - public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto) { GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( requestDto.getTitle(), diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 94cb4e9..749acb1 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -11,6 +11,7 @@ import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.s3.S3Service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -30,21 +31,13 @@ @Service @Slf4j +@RequiredArgsConstructor public class GameService { private final GameRepository gameRepository; private final ReviewRepository reviewRepository; private final S3Service s3Service; private final GameImageRepository gameImageRepository; - @Autowired - public GameService(GameRepository gameRepository, ReviewRepository reviewRepository, S3Service s3Service,GameImageRepository gameImageRepository) { - - this.gameRepository = gameRepository; - this.reviewRepository = reviewRepository; - this.s3Service = s3Service; - this.gameImageRepository=gameImageRepository; - } - public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto , MultipartFile file) { // 게임 엔티티 생성 diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 594b800..479f701 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -10,22 +10,18 @@ import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import static com.example.gamemate.global.constant.ErrorCode.REVIEW_NOT_FOUND; @Service +@RequiredArgsConstructor public class ReviewService { private final ReviewRepository reviewRepository; private final GameRepository gameRepository; - public ReviewService(ReviewRepository reviewRepository, - GameRepository gameRepository) { - this.reviewRepository = reviewRepository; - this.gameRepository = gameRepository; - } - public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto) { Game game = gameRepository.findById(gameId) From 0859d081b9076005f8ab018027c45cb5bc4ad2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 13 Jan 2025 12:34:34 +0900 Subject: [PATCH 060/215] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EC=97=90=20@Transactional=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/domain/auth/service/AuthService.java | 2 ++ .../com/example/gamemate/domain/user/service/UserService.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index cf15090..fd53a7b 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -13,11 +13,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @RequiredArgsConstructor +@Transactional public class AuthService { private final UserRepository userRepository; diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index a1b661b..95cf5ff 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -11,9 +11,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional public class UserService { private final UserRepository userRepository; @@ -21,6 +23,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final AuthService authService; + @Transactional(readOnly = true) public ProfileResponseDto findProfile(Long id, String token) { validateToken(token); From 3b70f1f0ef6a57db80a11681b0676435f1f41841 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:34:24 +0900 Subject: [PATCH 061/215] =?UTF-8?q?refact:=20game=20feat=20:=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84,=20=EB=A6=AC=EB=B7=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#25=20PR=EC=BD=94=EB=93=9C=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. @AutoWired 말고 @ RequiredArgsConstructor 로 통일 2. 오타 및 줄간격 경로설정 수정 3. return 값을 new ResponseEntity<>(); 로 통일 4. 반환 타입 void 로 수정 5. 트랜잭션 처리 6. @AllArgsConstructor를 제한없이 사용하는 것 > 삭제 --- .../domain/game/controller/GameController.java | 11 +++++------ .../game/controller/GameEnrollRequestController.java | 7 ++++--- .../domain/game/dto/GameUpdateRequestDto.java | 2 +- .../domain/game/entity/GamaEnrollRequest.java | 2 +- .../com/example/gamemate/domain/game/entity/Game.java | 2 +- .../gamemate/domain/game/entity/GameImage.java | 2 ++ .../gamemate/domain/game/service/GameService.java | 2 +- .../domain/review/controller/ReviewController.java | 2 +- .../example/gamemate/domain/review/entity/Review.java | 3 ++- .../gamemate/domain/review/service/ReviewService.java | 5 +++-- 10 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 48cf73e..c21c24d 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -32,7 +32,6 @@ public GameController(GameService gameService) { * @return */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @ResponseStatus(HttpStatus.CREATED) public ResponseEntity createGame( @RequestPart(value = "gameData") String gameDataString, @RequestPart(value = "file", required = false) MultipartFile file) { @@ -46,14 +45,14 @@ public ResponseEntity createGame( } GameCreateResponseDto responseDto = gameService.createGame(requestDto, file); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } /** * 게임 전체 조회 * * @param page - * @param szie + * @param size * @return */ @GetMapping @@ -92,14 +91,14 @@ public ResponseEntity findGameById( } /** - * 게임 정보 수정 * * @param id - * @param requestDto + * @param gameDataString + * @param newFile * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGame( + public ResponseEntity updateGame( @PathVariable Long id, @RequestPart(value = "gameData") String gameDataString, @RequestPart(value = "file", required = false) MultipartFile newFile) { diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 7c6844a..aaf8d18 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -44,7 +44,8 @@ public ResponseEntity CreateGameEnrollRequest( public ResponseEntity> findAllGameEnrollRequest() { Page gameEnrollRequestAll = gameEnrollRequestService.findAllGameEnrollRequest(); - return ResponseEntity.ok(gameEnrollRequestAll); + return new ResponseEntity<>(gameEnrollRequestAll, HttpStatus.OK); + } /** @@ -58,7 +59,7 @@ public ResponseEntity findGameEnrollRequestById( @PathVariable Long id) { GameEnrollRequestResponseDto gameEnrollRequestById = gameEnrollRequestService.findGameEnrollRequestById(id); - return ResponseEntity.ok(gameEnrollRequestById); + return new ResponseEntity<>(gameEnrollRequestById, HttpStatus.OK); } /** @@ -69,7 +70,7 @@ public ResponseEntity findGameEnrollRequestById( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateGameEnroll( + public ResponseEntity updateGameEnroll( @PathVariable Long id, @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java index f5e178d..87a43bb 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java @@ -12,7 +12,7 @@ public class GameUpdateRequestDto { private String description; - public GameUpdateRequestDto(String title, String genre, String platform , String description) { + public GameUpdateRequestDto(String title, String genre, String platform, String description) { this.title = title; this.genre = genre; this.platform = platform; diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java index 65085df..0c79760 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java @@ -2,6 +2,7 @@ import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,7 +10,6 @@ @Entity @Getter @Table(name = "game_enroll_request") -@AllArgsConstructor @NoArgsConstructor public class GamaEnrollRequest extends BaseEntity { @Id diff --git a/src/main/java/com/example/gamemate/domain/game/entity/Game.java b/src/main/java/com/example/gamemate/domain/game/entity/Game.java index b4fda84..76bedd1 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/Game.java @@ -3,6 +3,7 @@ import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,7 +14,6 @@ @Entity @Getter @Table(name = "game") -@AllArgsConstructor @NoArgsConstructor public class Game extends BaseEntity { @Id diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java b/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java index 06a831b..4a5c0c5 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GameImage.java @@ -1,6 +1,8 @@ package com.example.gamemate.domain.game.entity; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 749acb1..c15b84f 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -13,7 +13,6 @@ import com.example.gamemate.global.s3.S3Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -38,6 +37,7 @@ public class GameService { private final S3Service s3Service; private final GameImageRepository gameImageRepository; + @Transactional public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto , MultipartFile file) { // 게임 엔티티 생성 diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 740acdb..66b56f1 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -48,7 +48,7 @@ public ResponseEntity createReview( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateReview( + public ResponseEntity updateReview( @PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto) { diff --git a/src/main/java/com/example/gamemate/domain/review/entity/Review.java b/src/main/java/com/example/gamemate/domain/review/entity/Review.java index 9588f3f..3d71be7 100644 --- a/src/main/java/com/example/gamemate/domain/review/entity/Review.java +++ b/src/main/java/com/example/gamemate/domain/review/entity/Review.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.review.entity; import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,7 +10,7 @@ @Getter @NoArgsConstructor @Table(name = "review") -public class Review extends com.example.gamemate.global.common.BaseEntity { +public class Review extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 479f701..61ed539 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -12,6 +12,7 @@ import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import static com.example.gamemate.global.constant.ErrorCode.REVIEW_NOT_FOUND; @@ -22,6 +23,7 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final GameRepository gameRepository; + @Transactional public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto) { Game game = gameRepository.findById(gameId) @@ -37,7 +39,7 @@ public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto return new ReviewCreateResponseDto(saveReview); } - public ReviewUpdateResponseDto updateReview(Long gameId, Long id, ReviewUpdateRequestDto requestDto) { + public void updateReview(Long gameId, Long id, ReviewUpdateRequestDto requestDto) { Review review = reviewRepository.findById(id) .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); @@ -48,7 +50,6 @@ public ReviewUpdateResponseDto updateReview(Long gameId, Long id, ReviewUpdateRe ); Review updateReview = reviewRepository.save(review); - return new ReviewUpdateResponseDto(updateReview); } public void deleteReview(Long id) { From bfdd4d897e44f225398602ceced6e412783ed0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 13 Jan 2025 13:49:44 +0900 Subject: [PATCH 062/215] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/domain/user/service/UserService.java | 3 +-- .../gamemate/global/config/auth/CustomUserDetailsService.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index 95cf5ff..d37c79a 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -37,7 +37,7 @@ public ProfileResponseDto findProfile(Long id, String token) { return new ProfileResponseDto(findUser); } - public ProfileResponseDto updateProfile(Long id, String newNickname, String token) { + public void updateProfile(Long id, String newNickname, String token) { validateToken(token); @@ -49,7 +49,6 @@ public ProfileResponseDto updateProfile(Long id, String newNickname, String toke findUser.updateProfile(newNickname); User savedUser = userRepository.save(findUser); - return new ProfileResponseDto(savedUser); } public void updatePassword(Long id, String oldPassword, String newPassword, String token) { diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java index 11c00e1..f942848 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java @@ -19,7 +19,7 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByEmail(username) - .orElseThrow(()-> new UsernameNotFoundException("유저를 찾을 수 없습니다.")); + .orElseThrow(()-> new UsernameNotFoundException("회원을 찾을 수 없습니다.")); return new CustomUserDetails(user); } } From 423726935d6bd8ecd1edfdfe556a305f17073415 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:54:12 +0900 Subject: [PATCH 063/215] =?UTF-8?q?refact:=20game=20feat=20:=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84,=20=EB=A6=AC=EB=B7=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#25=20PR=EC=BD=94=EB=93=9C=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. return 값을 new ResponseEntity<>(); 로 통일 --- .../gamemate/domain/game/controller/GameController.java | 5 +++-- .../domain/game/controller/GameEnrollRequestController.java | 2 +- .../gamemate/domain/review/controller/ReviewController.java | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index c21c24d..8a97e98 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -72,8 +72,8 @@ public ResponseEntity> findAllGame( games = gameService.findAllGame(page, size); } + return new ResponseEntity<>(games, HttpStatus.OK); - return ResponseEntity.ok(games); } /** @@ -87,7 +87,8 @@ public ResponseEntity findGameById( @PathVariable Long id) { GameFindByIdResponseDto gameById = gameService.findGameById(id); - return ResponseEntity.ok(gameById); + return new ResponseEntity<>(gameById, HttpStatus.OK); + } /** diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index aaf8d18..47634d4 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -32,7 +32,7 @@ public ResponseEntity CreateGameEnrollRequest( @RequestBody GameEnrollRequestCreateRequestDto requestDto) { GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.createGameEnrollRequest(requestDto); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + return new ResponseEntity<>(responseDto, HttpStatus.OK); } /** diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 66b56f1..cd3a7ca 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -30,13 +30,12 @@ public ReviewController(ReviewService reviewService) { * @return */ @PostMapping - @ResponseStatus(HttpStatus.CREATED) public ResponseEntity createReview( @PathVariable Long gameId, @RequestBody ReviewCreateRequestDto requestDto) { ReviewCreateResponseDto responseDto = reviewService.createReview(gameId, requestDto); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } /** From 5def770c8b7c1073dd1b4548036146ae3663a802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 13 Jan 2025 19:09:06 +0900 Subject: [PATCH 064/215] =?UTF-8?q?fix:=20JwtAuthenticationFilter=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 createAuthentication에서 CustomUserDetails를 사용하지 않는 부분 수정 --- .../filter/JwtAuthenticationFilter.java | 48 +++---------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java index e1f1557..d55ea4b 100644 --- a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java @@ -62,23 +62,13 @@ protected void doFilterInternal(HttpServletRequest request, } private Authentication createAuthentication(String email) { - List authorities = Collections.singletonList( - new SimpleGrantedAuthority("ROLE_USER") - ); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); - // UserDetails 객체 생성 - UserDetails userDetails = User.builder() - .username(email) - .password("") // 토큰 기반 인증이므로 비밀번호는 불필요 - .authorities(authorities) - .build(); - - // Authentication 객체 생성 및 반환 - return new UsernamePasswordAuthenticationToken( - userDetails, - "", - authorities - ); + return new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); } private String extractToken(HttpServletRequest request) { @@ -89,30 +79,4 @@ private String extractToken(HttpServletRequest request) { return null; } - private void authenticate(HttpServletRequest request) { - log.info("인증 처리"); - - String token = this.getTokenFromRequest(request); - if(token == null || !jwtTokenProvider.validateToken(token)) { - return; - } - - String username = this.jwtTokenProvider.getEmailFromToken(token); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - } - - private String getTokenFromRequest(HttpServletRequest request) { - final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); - final String headerPrefix = "Bearer "; - - if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(headerPrefix)) { - return bearerToken.substring(headerPrefix.length()); - } - return null; - } - } From 5531673c3e147d07db702b90f8eaa64d9511226d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 13 Jan 2025 19:11:24 +0900 Subject: [PATCH 065/215] =?UTF-8?q?fix:=20UserController=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 updateProfile 수정 --- .../example/gamemate/domain/user/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index 00ae2c4..64badb1 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -36,7 +36,7 @@ public ResponseEntity updateProfile( @Valid @RequestBody ProfileUpdateRequestDto requestDto, @RequestHeader("Authorization") String token) { String jwtToken = token.substring(7); - ProfileResponseDto responseDto = userService.updateProfile(id, requestDto.getNewNickname(), jwtToken); + userService.updateProfile(id, requestDto.getNewNickname(), jwtToken); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } From f06b762195f5a7bd44d4bc1baa33af6e660b548c Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:19:07 +0900 Subject: [PATCH 066/215] =?UTF-8?q?feat:=20review=20=EA=B6=8C=ED=95=9C(?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D)=20=EC=9C=A0=EC=A0=80=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=EC=82=AD=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 리뷰 작성자만, 리뷰 삭제할 수 있음 기능 구현 --- .../review/controller/ReviewController.java | 57 ++++++++++++++----- .../review/repository/ReviewRepository.java | 4 ++ .../domain/review/service/ReviewService.java | 43 ++++++++++++-- .../gamemate/global/constant/ErrorCode.java | 1 + 4 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index cd3a7ca..97a2ea2 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -5,36 +5,54 @@ import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; import com.example.gamemate.domain.review.service.ReviewService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.provider.JwtTokenProvider; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import static com.example.gamemate.global.constant.ErrorCode.USER_NOT_FOUND; + @RestController @RequestMapping("/games/{gameId}/reviews") +@AllArgsConstructor +@Slf4j public class ReviewController { private final ReviewService reviewService; + private final JwtTokenProvider jwtTokenProvider; - @Autowired - public ReviewController(ReviewService reviewService) { - - this.reviewService = reviewService; - } /** - * 리뷰등록 + * 게임에 대한 리뷰를 등록합니다. * - * @param gameId - * @param requestDto - * @return + * @param gameId 리뷰를 등록할 게임의 고유 식별자 + * @param requestDto 리뷰 정보를 담고 있는 DTO 객체 + * @param token 사용자 인증 토큰 + * @return 등록된 리뷰의 정보 + * @throws UnauthorizedException 유효하지 않은 토큰일 경우 + * @throws GameNotFoundException 해당 gameId의 게임이 존재하지 않을 경우 */ @PostMapping public ResponseEntity createReview( @PathVariable Long gameId, - @RequestBody ReviewCreateRequestDto requestDto) { + @RequestBody ReviewCreateRequestDto requestDto, + @RequestHeader("Authorization") String token) { - ReviewCreateResponseDto responseDto = reviewService.createReview(gameId, requestDto); + // "Bearer " 접두사 제거 + token = token.substring(7); + // 토큰에서 이메일 추출 + String email = jwtTokenProvider.getEmailFromToken(token); + + ReviewCreateResponseDto responseDto = reviewService.createReview(email, gameId, requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -50,9 +68,15 @@ public ResponseEntity createReview( public ResponseEntity updateReview( @PathVariable Long gameId, @PathVariable Long id, - @RequestBody ReviewUpdateRequestDto requestDto) { + @RequestBody ReviewUpdateRequestDto requestDto, + @RequestHeader("Authorization") String token) { + + // "Bearer " 접두사 제거 + token = token.substring(7); + // 토큰에서 이메일 추출 + String email = jwtTokenProvider.getEmailFromToken(token); - reviewService.updateReview(gameId, id, requestDto); + reviewService.updateReview(email, gameId, id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -66,9 +90,12 @@ public ResponseEntity updateReview( @DeleteMapping("/{id}") public ResponseEntity deleteReview( @PathVariable Long gameId, - @PathVariable Long id) { + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + - reviewService.deleteReview(id); + reviewService.deleteReview(userDetails.getUser(), id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java index 4e2c07e..807788f 100644 --- a/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/gamemate/domain/review/repository/ReviewRepository.java @@ -7,5 +7,9 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface ReviewRepository extends JpaRepository { + Page findAllByGame(Game game, Pageable pageable); + + boolean existsByUserIdAndGameId(Long userId, Long gameId); + } diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 61ed539..a27f4fc 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -8,13 +8,15 @@ import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static com.example.gamemate.global.constant.ErrorCode.REVIEW_NOT_FOUND; +import static com.example.gamemate.global.constant.ErrorCode.*; @Service @@ -22,9 +24,21 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final GameRepository gameRepository; + private final UserRepository userRepository; @Transactional - public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto) { + public ReviewCreateResponseDto createReview(String email, Long gameId, ReviewCreateRequestDto requestDto) { + + // 이메일로 유저 조회 + User user = userRepository.findByEmail(email). + orElseThrow(() -> new ApiException(USER_NOT_FOUND)); + Long userId = user.getId(); + + // 사용자가 이미 해당 게임에 대한 리뷰를 작성했는지 확인 + boolean hasReview = reviewRepository.existsByUserIdAndGameId(userId, gameId); + if (hasReview) { + throw new ApiException(REVIEW_ALREADY_EXISTS); + } Game game = gameRepository.findById(gameId) .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); @@ -33,30 +47,47 @@ public ReviewCreateResponseDto createReview(Long gameId, ReviewCreateRequestDto requestDto.getContent(), requestDto.getStar(), game, - requestDto.getUserId() + userId ); Review saveReview = reviewRepository.save(review); return new ReviewCreateResponseDto(saveReview); } - public void updateReview(Long gameId, Long id, ReviewUpdateRequestDto requestDto) { + public void updateReview(String email, Long gameId, Long id, ReviewUpdateRequestDto requestDto) { + + // 이메일로 유저 조회 + User user = userRepository.findByEmail(email). + orElseThrow(() -> new ApiException(USER_NOT_FOUND)); + Long userId = user.getId(); Review review = reviewRepository.findById(id) .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); + // 리뷰 작성자와 현재 사용자가 같은지 확인 + if (!review.getUserId().equals(userId)) { + throw new ApiException(FORBIDDEN); + } + review.updateReview( requestDto.getContent(), requestDto.getStar() ); - Review updateReview = reviewRepository.save(review); + reviewRepository.save(review); } - public void deleteReview(Long id) { + public void deleteReview(User loginUser, Long id) { + + Long userId = loginUser.getId(); Review review = reviewRepository.findById(id) .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); + // 리뷰 작성자와 현재 사용자가 같은지 확인 + if (!review.getUserId().equals(userId)) { + throw new ApiException(FORBIDDEN); + } + reviewRepository.delete(review); } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index bc694a4..eb222a2 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -14,6 +14,7 @@ public enum ErrorCode { IS_WITHDRAWN_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 회원입니다."), DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "DUPLICATE_USER", "이미 사용 중인 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "비밀번호가 일치하지 않습니다."), + REVIEW_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"REVIEW_ALREADY_EXISTS","이미 리뷰를 작성한 회원입니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From b4a1589fb745fc2200964d27811414cfc17a760b Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 13 Jan 2025 19:48:21 +0900 Subject: [PATCH 067/215] =?UTF-8?q?feat:=20=EB=B3=B4=EB=93=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 보드 생성 시에 유저 추가 2. 보드 수정, 삭제 시에 로그인한 유저 확인 추가 3. 보드 수정, 삭제 시에 return 값 Void로 변경 --- .../board/controller/BoardController.java | 28 ++++++++++------- .../domain/board/dto/BoardResponseDto.java | 10 ++++++- .../gamemate/domain/board/entity/Board.java | 3 +- .../domain/board/service/BoardService.java | 30 +++++++++++++++---- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index 67f0366..ca0df50 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -6,10 +6,15 @@ import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.service.BoardService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -28,10 +33,11 @@ public class BoardController { */ @PostMapping public ResponseEntity createBoard( - @Valid @RequestBody BoardRequestDto dto + @Valid @RequestBody BoardRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - BoardResponseDto responseDto = boardService.createBoard(dto); + BoardResponseDto responseDto = boardService.createBoard(customUserDetails.getUser(), dto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -83,13 +89,14 @@ public ResponseEntity findBoardById( * @return */ @PatchMapping("/{id}") - public ResponseEntity updateBoard( + public ResponseEntity updateBoard( @PathVariable Long id, - @Valid @RequestBody BoardRequestDto dto + @Valid @RequestBody BoardRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - boardService.updateBoard(id, dto); - return new ResponseEntity<>("업데이트 되었습니다.", HttpStatus.NO_CONTENT); + boardService.updateBoard(customUserDetails.getUser(), id, dto); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** @@ -98,11 +105,12 @@ public ResponseEntity updateBoard( * @return */ @DeleteMapping("/{id}") - public ResponseEntity deleteBoard( - @PathVariable Long id + public ResponseEntity deleteBoard( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - boardService.deleteBoard(id); - return new ResponseEntity<>("삭제 되었습니다", HttpStatus.NO_CONTENT); + boardService.deleteBoard(customUserDetails.getUser(), id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java index 3f28d54..b7465d0 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardResponseDto.java @@ -3,17 +3,25 @@ import com.example.gamemate.domain.board.enums.BoardCategory; import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class BoardResponseDto { private final Long id; private final BoardCategory category; private final String title; private final String content; + private final String nickname; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; - public BoardResponseDto(Long id, BoardCategory category, String title, String content) { + public BoardResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.category = category; this.title = title; this.content = content; + this.nickname = nickname; + this.createdAt = createdAt; + this.updatedAt = updatedAt; } } diff --git a/src/main/java/com/example/gamemate/domain/board/entity/Board.java b/src/main/java/com/example/gamemate/domain/board/entity/Board.java index 8301212..02d331e 100644 --- a/src/main/java/com/example/gamemate/domain/board/entity/Board.java +++ b/src/main/java/com/example/gamemate/domain/board/entity/Board.java @@ -33,10 +33,11 @@ public class Board extends BaseEntity { @JoinColumn(name = "user_id") private User user; - public Board(BoardCategory category, String title, String content) { + public Board(BoardCategory category, String title, String content, User user) { this.category = category; this.title = title; this.content = content; + this.user = user; } public void updateBoard(BoardCategory category, String title, String content) { diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 8ad8c97..f60d46b 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -11,12 +11,16 @@ import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.config.auth.CustomUserDetails; import com.example.gamemate.global.exception.ApiException; 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.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +28,7 @@ import java.util.stream.Collectors; import static com.example.gamemate.global.constant.ErrorCode.BOARD_NOT_FOUND; +import static com.example.gamemate.global.constant.ErrorCode.FORBIDDEN; @Service @RequiredArgsConstructor @@ -38,15 +43,18 @@ public class BoardService { * @return */ @Transactional - public BoardResponseDto createBoard(BoardRequestDto dto) { + public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { // 게시글 생성 - Board newBoard = new Board(dto.getCategory(),dto.getTitle(),dto.getContent()); + Board newBoard = new Board(dto.getCategory(),dto.getTitle(),dto.getContent(), loginUser); Board createdBoard = boardRepository.save(newBoard); return new BoardResponseDto( createdBoard.getBoardId(), createdBoard.getCategory(), createdBoard.getTitle(), - createdBoard.getContent() + createdBoard.getContent(), + createdBoard.getUser().getNickname(), + createdBoard.getCreatedAt(), + createdBoard.getModifiedAt() ); } @@ -117,13 +125,18 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { * @return */ @Transactional - public void updateBoard(Long id, BoardRequestDto dto) { + public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + // 게시글 작성자와 로그인한 사용자 확인 + if(!findBoard.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(FORBIDDEN); + } + findBoard.updateBoard(dto.getCategory(),dto.getTitle(),dto.getContent()); - Board updatedBoard = boardRepository.save(findBoard); + boardRepository.save(findBoard); } /** @@ -131,11 +144,16 @@ public void updateBoard(Long id, BoardRequestDto dto) { * @param id */ @Transactional - public void deleteBoard(Long id) { + public void deleteBoard(User loginUser, Long id) { //게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + // 게시글 작성자와 로그인한 사용자 확인 + if(!findBoard.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(FORBIDDEN); + } + boardRepository.delete(findBoard); } } From 4687145b6910c61e90bbe269cec84b8e2e135c49 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:51:04 +0900 Subject: [PATCH 068/215] =?UTF-8?q?fix:=20game=20=EB=8B=A8=EA=B1=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=A6=AC=EB=B7=B0=20=EB=8B=8C?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 1. 게임 단건 조회시 리뷰에 불러오는 데이터 닉네임 추가 --- .../game/controller/GameController.java | 7 +++++-- .../game/dto/GameFindByIdResponseDto.java | 4 +++- .../domain/game/service/GameService.java | 11 ++++++---- .../review/controller/ReviewController.java | 20 ++++--------------- .../dto/ReviewFindByAllResponseDto.java | 4 +++- .../domain/review/service/ReviewService.java | 14 ++++--------- 6 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 8a97e98..bcf9f42 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.game.dto.*; import com.example.gamemate.domain.game.service.GameService; +import com.example.gamemate.global.config.auth.CustomUserDetails; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -10,6 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -84,9 +86,10 @@ public ResponseEntity> findAllGame( */ @GetMapping("/{id}") public ResponseEntity findGameById( - @PathVariable Long id) { + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails userDetails) { - GameFindByIdResponseDto gameById = gameService.findGameById(id); + GameFindByIdResponseDto gameById = gameService.findGameById(userDetails.getUser(), id); return new ResponseEntity<>(gameById, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java index 517a707..5e285dc 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java @@ -21,9 +21,10 @@ public class GameFindByIdResponseDto { private final String fileName; private final String imageUrl; private final Page reviews; + private final String nickname; // private final List reviews; - public GameFindByIdResponseDto(Game game, Page reviews) { + public GameFindByIdResponseDto(Game game, Page reviews, String nickname) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 this.id = game.getId(); this.title = game.getTitle(); @@ -37,6 +38,7 @@ public GameFindByIdResponseDto(Game game, Page revie this.imageUrl = game.getImages().isEmpty() ? null : game.getImages().get(0).getFilePath(); this.reviews = reviews; + this.nickname = nickname; // this.reviews = game.getReviews().stream() // .map(ReviewFindByAllResponseDto::new) // .collect(Collectors.toList()); diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index c15b84f..7472e84 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -9,6 +9,7 @@ import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.s3.S3Service; import lombok.RequiredArgsConstructor; @@ -74,7 +75,9 @@ public Page findAllGame(int page, int size) { } @Transactional - public GameFindByIdResponseDto findGameById(Long id) { + public GameFindByIdResponseDto findGameById(User longinUser, Long id) { + + String nickName = longinUser.getNickname(); Game game = gameRepository.findGameById(id) .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); @@ -82,12 +85,12 @@ public GameFindByIdResponseDto findGameById(Long id) { Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); Page reviewPage = reviewRepository.findAllByGame(game, pageable); - // Review를 ReviewFindByAllResponseDto로 변환 + // Review를 ReviewFindByAllResponseDto로 변환하면서 닉네임 추가 Page reviews = reviewPage.map(review -> - new ReviewFindByAllResponseDto(review) + new ReviewFindByAllResponseDto(review, longinUser.getNickname()) ); - return new GameFindByIdResponseDto(game, reviews); + return new GameFindByIdResponseDto(game, reviews, nickName); } @Transactional diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 97a2ea2..8ca1fa0 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -45,14 +45,9 @@ public class ReviewController { public ResponseEntity createReview( @PathVariable Long gameId, @RequestBody ReviewCreateRequestDto requestDto, - @RequestHeader("Authorization") String token) { - - // "Bearer " 접두사 제거 - token = token.substring(7); - // 토큰에서 이메일 추출 - String email = jwtTokenProvider.getEmailFromToken(token); + @AuthenticationPrincipal CustomUserDetails userDetails) { - ReviewCreateResponseDto responseDto = reviewService.createReview(email, gameId, requestDto); + ReviewCreateResponseDto responseDto = reviewService.createReview(userDetails.getUser(), gameId, requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -69,14 +64,9 @@ public ResponseEntity updateReview( @PathVariable Long gameId, @PathVariable Long id, @RequestBody ReviewUpdateRequestDto requestDto, - @RequestHeader("Authorization") String token) { - - // "Bearer " 접두사 제거 - token = token.substring(7); - // 토큰에서 이메일 추출 - String email = jwtTokenProvider.getEmailFromToken(token); + @AuthenticationPrincipal CustomUserDetails userDetails) { - reviewService.updateReview(email, gameId, id, requestDto); + reviewService.updateReview(userDetails.getUser(), gameId, id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -93,8 +83,6 @@ public ResponseEntity deleteReview( @PathVariable Long id, @AuthenticationPrincipal CustomUserDetails userDetails) { - - reviewService.deleteReview(userDetails.getUser(), id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java index 237b7ae..80f18bf 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java @@ -13,13 +13,15 @@ public class ReviewFindByAllResponseDto { private Long gameId; private Long userId; private LocalDateTime createdAt; + private String nickName; - public ReviewFindByAllResponseDto(Review review) { + public ReviewFindByAllResponseDto(Review review, String nickName) { this.id = review.getId(); this.content = review.getContent(); this.star = review.getStar(); this.gameId = review.getGame().getId(); this.userId = review.getUserId(); this.createdAt = review.getCreatedAt(); + this.nickName = nickName; } } diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index a27f4fc..f436dbe 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -27,12 +27,9 @@ public class ReviewService { private final UserRepository userRepository; @Transactional - public ReviewCreateResponseDto createReview(String email, Long gameId, ReviewCreateRequestDto requestDto) { + public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewCreateRequestDto requestDto) { - // 이메일로 유저 조회 - User user = userRepository.findByEmail(email). - orElseThrow(() -> new ApiException(USER_NOT_FOUND)); - Long userId = user.getId(); + Long userId = loginUser.getId(); // 사용자가 이미 해당 게임에 대한 리뷰를 작성했는지 확인 boolean hasReview = reviewRepository.existsByUserIdAndGameId(userId, gameId); @@ -53,12 +50,9 @@ public ReviewCreateResponseDto createReview(String email, Long gameId, ReviewCre return new ReviewCreateResponseDto(saveReview); } - public void updateReview(String email, Long gameId, Long id, ReviewUpdateRequestDto requestDto) { + public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateRequestDto requestDto) { - // 이메일로 유저 조회 - User user = userRepository.findByEmail(email). - orElseThrow(() -> new ApiException(USER_NOT_FOUND)); - Long userId = user.getId(); + Long userId = loginUser.getId(); Review review = reviewRepository.findById(id) .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); From efc6a5a711939333fb89381319c868fac43a435f Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 13 Jan 2025 20:38:48 +0900 Subject: [PATCH 069/215] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?follow=20CRUD=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 팔로우 하기, 팔로우 취소, 팔로우 여부 확인에서 로그인한 유저객체를 받도록 리팩토링 --- .../follow/controller/FollowController.java | 24 +++++---- .../domain/follow/service/FollowService.java | 51 ++++++++----------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index 1803f21..4bd9af2 100644 --- a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -2,9 +2,11 @@ import com.example.gamemate.domain.follow.service.FollowService; import com.example.gamemate.domain.follow.dto.*; +import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -23,10 +25,11 @@ public class FollowController { */ @PostMapping public ResponseEntity createFollow( - @RequestBody FollowCreateRequestDto dto + @RequestBody FollowCreateRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - FollowResponseDto followResponseDto = followService.createFollow(dto); + FollowResponseDto followResponseDto = followService.createFollow(dto, customUserDetails.getUser()); return new ResponseEntity<>(followResponseDto, HttpStatus.CREATED); } @@ -37,26 +40,27 @@ public ResponseEntity createFollow( */ @DeleteMapping("/{id}") public ResponseEntity deleteFollow( - @PathVariable Long id + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - followService.deleteFollow(id); + followService.deleteFollow(id, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** - * 팔로우 상태 확인 (follower 가 followee 를 팔로우 했는지 확인) - * @param followerEmail - * @param followeeEmail + * 팔로우 상태 확인 (loginUser 가 followee 를 팔로우 했는지 확인) + * @param customUserDetails 로그인한 유저 + * @param email 팔로우 상태를 확인할 상대방 이메일 * @return followBooleanResponseDto */ @GetMapping("/status") public ResponseEntity findFollow( - @RequestParam String followerEmail, - @RequestParam String followeeEmail + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam String email ) { - FollowBooleanResponseDto followBooleanResponseDto = followService.findFollow(followerEmail, followeeEmail); + FollowBooleanResponseDto followBooleanResponseDto = followService.findFollow(customUserDetails.getUser(), email); return new ResponseEntity<>(followBooleanResponseDto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index edac065..ad85def 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -10,42 +10,39 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Slf4j public class FollowService { private final UserRepository userRepository; private final FollowRepository followRepository; // 팔로우하기 - // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public FollowResponseDto createFollow(FollowCreateRequestDto dto) { + public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser) { - User follower = userRepository.findById(1L) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User followee = userRepository.findByEmail(dto.getEmail()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } + } // 탈퇴한 유저일때 예외처리 - if (followRepository.existsByFollowerAndFollowee(follower, followee)) { + if (followRepository.existsByFollowerAndFollowee(loginUser, followee)) { throw new ApiException(ErrorCode.IS_ALREADY_FOLLOWED); - } + } // 이미 팔로우를 했을때 예외처리 - if (Objects.equals(follower.getEmail(), dto.getEmail())) { + if (Objects.equals(loginUser.getEmail(), dto.getEmail())) { throw new ApiException(ErrorCode.INVALID_INPUT); - } + } // 자기 자신을 팔로우 할때 예외처리 - Follow follow = new Follow(follower, followee); + Follow follow = new Follow(loginUser, followee); followRepository.save(follow); return new FollowResponseDto( @@ -57,46 +54,42 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto) { } // 팔로우 취소하기 - // todo: 현재 로그인이 구현되지 않아 1번유저가 팔로우를 취소 하는것으로 구현했으니 추후 로그인이 구현되면 follower 는 로그인한 유저로 설정 @Transactional - public void deleteFollow(Long id) { + public void deleteFollow(Long id, User loginUser) { Follow findFollow = followRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.FOLLOW_NOT_FOUND)); - User follower = userRepository.findById(1L) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - if (findFollow.getFollower() != follower) { + log.info("사용자 email : {}" , loginUser.getEmail()); + + if (findFollow.getFollower() != loginUser) { throw new ApiException(ErrorCode.INVALID_INPUT); - } + } // 본인의 팔로우가 아닐때 예외처리 followRepository.delete(findFollow); } // 팔로우 상태 확인 - // todo : 로그인한 유저(follower) 기준으로 상대 유저(followee)가 팔로우 되어 있는지 확인이 필요한 것이므로, 로그인 구현시 코드 수정해야함. - public FollowBooleanResponseDto findFollow(String followerEmail, String followeeEmail) { + public FollowBooleanResponseDto findFollow(User loginUser, String email) { - User follower = userRepository.findByEmail(followerEmail) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - User followee = userRepository.findByEmail(followeeEmail) + User followee = userRepository.findByEmail(email) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } + } // 확인할 상대방이 탈퇴한 회원일때 예외처리 - if (!followRepository.existsByFollowerAndFollowee(follower, followee)) { + if (!followRepository.existsByFollowerAndFollowee(loginUser, followee)) { return new FollowBooleanResponseDto( false, - follower.getId(), + loginUser.getId(), followee.getId() ); } return new FollowBooleanResponseDto( true, - follower.getId(), + loginUser.getId(), followee.getId() ); } @@ -109,7 +102,7 @@ public List findFollowers(String email) { if (followee.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } + } // 확인할 상대방이 탈퇴한 회원일때 예외처리 List followListByFollowee = followRepository.findByFollowee(followee); @@ -132,7 +125,7 @@ public List findFollowing(String email) { if (follower.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } + } // 확인할 상대방이 탈퇴한 회원일때 예외처리 List followListByFollower = followRepository.findByFollower(follower); From c44012e26af959b57f5a4834f8ad5ac7c3bf1038 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 13 Jan 2025 22:53:28 +0900 Subject: [PATCH 070/215] =?UTF-8?q?fix:=20ErrorCode=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. ErrorCode.형식으로 수정 --- .../domain/board/service/BoardService.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index f60d46b..30fcbad 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -12,23 +12,19 @@ import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.global.config.auth.CustomUserDetails; +import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; 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.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; -import static com.example.gamemate.global.constant.ErrorCode.BOARD_NOT_FOUND; -import static com.example.gamemate.global.constant.ErrorCode.FORBIDDEN; @Service @RequiredArgsConstructor @@ -93,7 +89,7 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { Pageable pageable = PageRequest.of(page, ListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.asc("createdAt"))); // 게시글 조회 Board findBoard = boardRepository.findById(id) - .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); // 댓글 조회 Page comments = commentRepository.findByBoard(findBoard,pageable); @@ -128,11 +124,11 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { // 게시글 조회 Board findBoard = boardRepository.findById(id) - .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); // 게시글 작성자와 로그인한 사용자 확인 if(!findBoard.getUser().getId().equals(loginUser.getId())) { - throw new ApiException(FORBIDDEN); + throw new ApiException(ErrorCode.FORBIDDEN); } findBoard.updateBoard(dto.getCategory(),dto.getTitle(),dto.getContent()); @@ -147,11 +143,11 @@ public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { public void deleteBoard(User loginUser, Long id) { //게시글 조회 Board findBoard = boardRepository.findById(id) - .orElseThrow(()->new ApiException(BOARD_NOT_FOUND)); + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); // 게시글 작성자와 로그인한 사용자 확인 if(!findBoard.getUser().getId().equals(loginUser.getId())) { - throw new ApiException(FORBIDDEN); + throw new ApiException(ErrorCode.FORBIDDEN); } boardRepository.delete(findBoard); From 7e0b09f5d7c96c961248a2426f2f46127053f285 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 13 Jan 2025 22:58:50 +0900 Subject: [PATCH 071/215] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 댓글 생성 시에 유저 추가 2. 댓글 수정, 삭제 시에 로그인한 유저 확인 추가 --- .../comment/controller/CommentController.java | 17 ++++++++++------ .../comment/dto/CommentResponseDto.java | 4 +++- .../domain/comment/entity/Comment.java | 3 ++- .../comment/service/CommentService.java | 20 +++++++++++++++---- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index 75906bf..027074f 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -3,9 +3,11 @@ import com.example.gamemate.domain.comment.dto.CommentRequestDto; import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.service.CommentService; +import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -24,9 +26,10 @@ public class CommentController { @PostMapping public ResponseEntity createComment( @PathVariable Long boardId, - @RequestBody CommentRequestDto requestDto + @RequestBody CommentRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - CommentResponseDto dto = commentService.createComment(boardId, requestDto); + CommentResponseDto dto = commentService.createComment(customUserDetails.getUser(),boardId, requestDto); return new ResponseEntity<>(dto, HttpStatus.CREATED); } @@ -39,17 +42,19 @@ public ResponseEntity createComment( @PatchMapping("/{id}") public ResponseEntity updateComment( @PathVariable Long id, - @RequestBody CommentRequestDto requestDto + @RequestBody CommentRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - commentService.updateComment(id, requestDto); + commentService.updateComment(customUserDetails.getUser(), id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") public ResponseEntity deleteComment( - @PathVariable Long id + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - commentService.deleteComment(id); + commentService.deleteComment(customUserDetails.getUser(), id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java b/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java index d893b96..a30a11b 100644 --- a/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/comment/dto/CommentResponseDto.java @@ -8,12 +8,14 @@ public class CommentResponseDto { private final Long id; private final String content; + private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime updatedAt; - public CommentResponseDto(Long id, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + public CommentResponseDto(Long id, String content, String nickname, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.content = content; + this.nickname = nickname; this.createdAt = createdAt; this.updatedAt = updatedAt; } diff --git a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java index 733513a..fd04489 100644 --- a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java +++ b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java @@ -27,9 +27,10 @@ public class Comment extends BaseEntity { @JoinColumn(name = "board_id") private Board board; - public Comment(String content, Board board) { + public Comment(String content, Board board, User user) { this.content = content; this.board = board; + this.user = user; } public void updateComment(String content) { diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index 17cc6ea..dc2b125 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -6,6 +6,7 @@ import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; @@ -26,17 +27,18 @@ public class CommentService { * @return */ @Transactional - public CommentResponseDto createComment(Long boardId, CommentRequestDto requestDto) { + public CommentResponseDto createComment(User loginUser, Long boardId, CommentRequestDto requestDto) { // 게시글 조회 Board findBoard = boardRepository.findById(boardId) .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)); - Comment comment = new Comment(requestDto.getContent(), findBoard); + Comment comment = new Comment(requestDto.getContent(), findBoard, loginUser); Comment createComment = commentRepository.save(comment); return new CommentResponseDto( createComment.getCommentId(), createComment.getContent(), + createComment.getUser().getNickname(), createComment.getCreatedAt(), createComment.getModifiedAt() ); @@ -48,11 +50,16 @@ public CommentResponseDto createComment(Long boardId, CommentRequestDto requestD * @param requestDto */ @Transactional - public void updateComment(Long id, CommentRequestDto requestDto) { + public void updateComment(User loginUser, Long id, CommentRequestDto requestDto) { // 댓글 조회 Comment findComment = commentRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + // 댓글 작성자와 로그인한 유저 확인 + if(!findComment.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + findComment.updateComment(requestDto.getContent()); commentRepository.save(findComment); } @@ -62,11 +69,16 @@ public void updateComment(Long id, CommentRequestDto requestDto) { * @param id */ @Transactional - public void deleteComment(Long id) { + public void deleteComment(User loginUser, Long id) { // 댓글 조회 Comment findComment = commentRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + // 댓글 작성자와 로그인한 유저 확인 + if(!findComment.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + commentRepository.delete(findComment); } } From 5e6a877b2a5eb683e9730e5a2afdf674df605591 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:08:38 +0900 Subject: [PATCH 072/215] =?UTF-8?q?fix:=20review=20=EC=A1=B0=ED=9A=8C=20AP?= =?UTF-8?q?I=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EA=B0=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B6=8C=ED=95=9C=20=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. review 조회 API 분리 2. 리뷰 작성 1회만 3. 리뷰 수정/삭제 권한부여 --- .../review/controller/ReviewController.java | 56 +++++++++++-------- .../review/dto/ReviewCreateResponseDto.java | 2 +- .../dto/ReviewFindByAllResponseDto.java | 2 +- .../review/dto/ReviewUpdateResponseDto.java | 2 +- .../gamemate/domain/review/entity/Review.java | 20 +++---- .../domain/review/service/ReviewService.java | 50 ++++++++++++----- 6 files changed, 83 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 8ca1fa0..94cc70a 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -1,9 +1,6 @@ package com.example.gamemate.domain.review.controller; -import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; -import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; -import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; -import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; +import com.example.gamemate.domain.review.dto.*; import com.example.gamemate.domain.review.service.ReviewService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; @@ -11,12 +8,18 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +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.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.*; import static com.example.gamemate.global.constant.ErrorCode.USER_NOT_FOUND; @@ -28,62 +31,71 @@ public class ReviewController { private final ReviewService reviewService; - private final JwtTokenProvider jwtTokenProvider; /** - * 게임에 대한 리뷰를 등록합니다. + * 리뷰등록 * - * @param gameId 리뷰를 등록할 게임의 고유 식별자 - * @param requestDto 리뷰 정보를 담고 있는 DTO 객체 - * @param token 사용자 인증 토큰 - * @return 등록된 리뷰의 정보 - * @throws UnauthorizedException 유효하지 않은 토큰일 경우 - * @throws GameNotFoundException 해당 gameId의 게임이 존재하지 않을 경우 + * @param gameId + * @param requestDto + * @param customUserDetails + * @return */ @PostMapping public ResponseEntity createReview( @PathVariable Long gameId, - @RequestBody ReviewCreateRequestDto requestDto, - @AuthenticationPrincipal CustomUserDetails userDetails) { + @Valid @RequestBody ReviewCreateRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - ReviewCreateResponseDto responseDto = reviewService.createReview(userDetails.getUser(), gameId, requestDto); + ReviewCreateResponseDto responseDto = reviewService.createReview(customUserDetails.getUser(), gameId, requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } /** - * 리뷰 수정 + * 리뷰수정 * * @param gameId * @param id * @param requestDto + * @param customUserDetails * @return */ @PatchMapping("/{id}") public ResponseEntity updateReview( @PathVariable Long gameId, @PathVariable Long id, - @RequestBody ReviewUpdateRequestDto requestDto, - @AuthenticationPrincipal CustomUserDetails userDetails) { + @Valid @RequestBody ReviewUpdateRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - reviewService.updateReview(userDetails.getUser(), gameId, id, requestDto); + reviewService.updateReview(customUserDetails.getUser(), gameId, id, requestDto); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } /** - * 리뷰 삭제 + * 리뷰삭제 * * @param gameId * @param id + * @param customUserDetails * @return */ @DeleteMapping("/{id}") public ResponseEntity deleteReview( @PathVariable Long gameId, @PathVariable Long id, - @AuthenticationPrincipal CustomUserDetails userDetails) { + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - reviewService.deleteReview(userDetails.getUser(), id); + reviewService.deleteReview(customUserDetails.getUser(), id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + @GetMapping + public ResponseEntity> ReviewFindAllByGameId( + @PathVariable Long gameId, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + Page responseDto = reviewService.ReviewFindAllByGameId(gameId, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + + } } diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java index d2d4334..85fac17 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java @@ -19,7 +19,7 @@ public ReviewCreateResponseDto(Review review) { this.content = review.getContent(); this.star = review.getStar(); this.gameId = review.getGame().getId(); - this.userId = review.getUserId(); + this.userId = review.getUser().getId(); this.createdAt = review.getCreatedAt(); } } diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java index 80f18bf..374ae7f 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java @@ -20,7 +20,7 @@ public ReviewFindByAllResponseDto(Review review, String nickName) { this.content = review.getContent(); this.star = review.getStar(); this.gameId = review.getGame().getId(); - this.userId = review.getUserId(); + this.userId = review.getUser().getId(); this.createdAt = review.getCreatedAt(); this.nickName = nickName; } diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java index 27dc113..4684bb3 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java @@ -19,7 +19,7 @@ public ReviewUpdateResponseDto(Review review) { this.content = review.getContent(); this.star = review.getStar(); this.gameId = review.getGame().getId(); - this.userId = review.getUserId(); + this.userId = review.getUser().getId(); this.modifiedAt = review.getModifiedAt(); } } diff --git a/src/main/java/com/example/gamemate/domain/review/entity/Review.java b/src/main/java/com/example/gamemate/domain/review/entity/Review.java index 3d71be7..fd8cba3 100644 --- a/src/main/java/com/example/gamemate/domain/review/entity/Review.java +++ b/src/main/java/com/example/gamemate/domain/review/entity/Review.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.review.entity; import com.example.gamemate.domain.game.entity.Game; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; @@ -21,26 +22,25 @@ public class Review extends BaseEntity { @Column(name = "star", nullable = false) private Integer star; - // @ManyToOne(fetch = FetchType.LAZY) -// @JoinColumn(name = "user_id") -// private User user; - @Column(name = "user_id", nullable = false) - private Long userId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "game_id") private Game game; - public Review(String content, Integer star, Game game, Long userId) { + public Review(String content, Integer star, Game gameId, User userId) { this.content = content; this.star = star; - this.game = game; - this.userId = userId; + this.game = gameId; + this.user = userId; } - public void updateReview(String content, Integer star){ + public void updateReview(String content, Integer star) { this.content = content; - this.star =star; + this.star = star; } } diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index f436dbe..0e44882 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -4,20 +4,23 @@ import com.example.gamemate.domain.game.repository.GameRepository; import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; +import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; -import com.example.gamemate.domain.review.dto.ReviewUpdateResponseDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.Role; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static com.example.gamemate.global.constant.ErrorCode.*; - @Service @RequiredArgsConstructor @@ -31,21 +34,25 @@ public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewC Long userId = loginUser.getId(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + // 사용자가 이미 해당 게임에 대한 리뷰를 작성했는지 확인 boolean hasReview = reviewRepository.existsByUserIdAndGameId(userId, gameId); if (hasReview) { - throw new ApiException(REVIEW_ALREADY_EXISTS); + throw new ApiException(ErrorCode.REVIEW_ALREADY_EXISTS); } Game game = gameRepository.findById(gameId) - .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); Review review = new Review( requestDto.getContent(), requestDto.getStar(), game, - userId + user ); + Review saveReview = reviewRepository.save(review); return new ReviewCreateResponseDto(saveReview); } @@ -55,11 +62,11 @@ public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateReque Long userId = loginUser.getId(); Review review = reviewRepository.findById(id) - .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); // 리뷰 작성자와 현재 사용자가 같은지 확인 - if (!review.getUserId().equals(userId)) { - throw new ApiException(FORBIDDEN); + if (!review.getUser().getId().equals(userId)) { + throw new ApiException(ErrorCode.FORBIDDEN); } review.updateReview( @@ -75,16 +82,31 @@ public void deleteReview(User loginUser, Long id) { Long userId = loginUser.getId(); Review review = reviewRepository.findById(id) - .orElseThrow(() -> new ApiException(REVIEW_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); - // 리뷰 작성자와 현재 사용자가 같은지 확인 - if (!review.getUserId().equals(userId)) { - throw new ApiException(FORBIDDEN); + if (userId.equals(review.getUser().getId()) || loginUser.getRole() == Role.ADMIN) { + reviewRepository.delete(review); + } else { + throw new ApiException(ErrorCode.FORBIDDEN); } - reviewRepository.delete(review); } + public Page ReviewFindAllByGameId(Long gameId, User loginUser ){ + + Game game = gameRepository.findGameById(gameId) + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); + + Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); + Page reviewPage = reviewRepository.findAllByGame(game, pageable); + + // Review를 ReviewFindByAllResponseDto로 변환하면서 닉네임 추가 + Page reviews = reviewPage.map(review -> + new ReviewFindByAllResponseDto(review, loginUser.getNickname()) + ); + + return reviewPage.map(review -> new ReviewFindByAllResponseDto(review, loginUser.getNickname())); + } } From c90efbd8747123432ccd3b00433c9539c6649593 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 14 Jan 2025 11:30:10 +0900 Subject: [PATCH 073/215] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?match=20CRUD=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매칭 요청, 매칭 수락/거절, 받은 매칭 조회, 보낸 매칭 조회, 매칭 취소(삭제) 에서 로그인한 유저객체를 받도록 리팩토링 --- .../match/controller/MatchController.java | 31 +++++++----- .../domain/match/service/MatchService.java | 47 ++++++++++--------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 80f4c4e..9c6bb1b 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -5,9 +5,11 @@ import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; +import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -25,10 +27,11 @@ public class MatchController { */ @PostMapping public ResponseEntity createMatch( - @RequestBody MatchCreateRequestDto dto + @RequestBody MatchCreateRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto); + MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto, customUserDetails.getUser()); return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); } @@ -41,10 +44,11 @@ public ResponseEntity createMatch( @PatchMapping("/{id}") public ResponseEntity updateMatch( @PathVariable Long id, - @RequestBody MatchUpdateRequestDto dto + @RequestBody MatchUpdateRequestDto dto, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - matchService.updateMatch(id, dto); + matchService.updateMatch(id, dto, userDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -53,20 +57,24 @@ public ResponseEntity updateMatch( * @return matchFindResponseDtoList */ @GetMapping("/received-match") - public ResponseEntity> findAllReceivedMatch() { + public ResponseEntity> findAllReceivedMatch( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { - List matchFindResponseDtoList = matchService.findAllReceivedMatch(); + List matchFindResponseDtoList = matchService.findAllReceivedMatch(userDetails.getUser()); return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); } /** - * 받은 매칭 전체 조회 + * 보낸 매칭 전체 조회 * @return matchFindResponseDtoList */ @GetMapping("/sent-match") - public ResponseEntity> findAllSentMatch() { + public ResponseEntity> findAllSentMatch( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { - List matchFindResponseDtoList = matchService.findAllSentMatch(); + List matchFindResponseDtoList = matchService.findAllSentMatch(userDetails.getUser()); return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); } @@ -77,10 +85,11 @@ public ResponseEntity> findAllSentMatch() { */ @DeleteMapping("/{id}") public ResponseEntity deleteMatch( - @PathVariable Long id + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - matchService.deleteMatch(id); + matchService.deleteMatch(id, userDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 643de21..85e0836 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -26,17 +27,14 @@ public class MatchService { private final MatchRepository matchRepository; // 매칭 요청 생성 - // todo : 현재 로그인이 구현되어 있지 않아, 로그인 유저를 1번 유저로 설정 @Transactional - public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { + public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { - User loginUser = userRepository.findById(1L) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); User receiver = userRepository.findById(dto.getUserId()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); if (receiver.getUserStatus() == UserStatus.WITHDRAW) { - throw new ApiException(ErrorCode.IS_WITHDRAW_USER); + throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } if (matchRepository.existsBySenderAndReceiverAndStatus(loginUser, receiver, MatchStatus.PENDING)) { @@ -50,9 +48,8 @@ public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto) { } // 매칭 수락/거절 - // todo : 현재 로그인이 구현되어 있지 않아, receiver 를 1번 유저로 설정. 로그인 구현시 수정필요 @Transactional - public void updateMatch(Long id, MatchUpdateRequestDto dto) { + public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { Match findMatch = matchRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); @@ -61,42 +58,46 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto) { throw new ApiException(ErrorCode.IS_ALREADY_PROCESSED); } - User loginUser = userRepository.findById(1L) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - - if (loginUser != findMatch.getReceiver()) { + if (!Objects.equals(loginUser.getId(), findMatch.getReceiver().getId())) { throw new ApiException(ErrorCode.FORBIDDEN); } findMatch.updateStatus(dto.getStatus()); } - // 보낸 매칭 전체 조회 - // todo : 현재 로그인이 구현 되어 있지 않아, 1번 유저의 목록을 불러오도록 설정. 로그인 구현시 수정 필요 - public List findAllReceivedMatch() { + // 받은 매칭 전체 조회 + public List findAllReceivedMatch(User loginUser) { - List matchList = matchRepository.findAllByReceiverId(1L); + List matchList = matchRepository.findAllByReceiverId(loginUser.getId()); - return matchList.stream().map(MatchFindResponseDto::toDto).toList(); + return matchList + .stream() + .map(MatchFindResponseDto::toDto) + .toList(); } - // 받은 매칭 전체 조회 - // todo : 현재 로그인이 구현 되어 있지 않아, 1번 유저의 목록을 불러오도록 설정. 로그인 구현시 수정 필요 - public List findAllSentMatch() { + // 보낸 매칭 전체 조회 + public List findAllSentMatch(User loginUser) { - List matchList = matchRepository.findAllBySenderId(1L); + List matchList = matchRepository.findAllBySenderId(loginUser.getId()); - return matchList.stream().map(MatchFindResponseDto::toDto).toList(); + return matchList + .stream() + .map(MatchFindResponseDto::toDto) + .toList(); } // 매치 삭제 (취소) - // todo : 로그인 구현시 로그인한유저가 sender 일때만 삭제 가능하도록 수정 @Transactional - public void deleteMatch(Long id) { + public void deleteMatch(Long id, User loginUser) { Match findMatch = matchRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + if (!Objects.equals(findMatch.getSender().getId(), loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + matchRepository.delete(findMatch); } } From 1290d58a52247b6d4cc3d5567a614875c6b911af Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 14 Jan 2025 11:33:39 +0900 Subject: [PATCH 074/215] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=82=B4=EC=9A=A9=EC=9D=98=20Dto=20?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20MatchResponseDto=20=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 19 ++++++----- .../match/dto/MatchCreateResponseDto.java | 12 ------- .../match/dto/MatchFindResponseDto.java | 24 -------------- .../domain/match/dto/MatchResponseDto.java | 32 +++++++++++++++++++ .../domain/match/service/MatchService.java | 15 ++++----- 5 files changed, 48 insertions(+), 54 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java delete mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 9c6bb1b..ff8bc79 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,10 +1,9 @@ package com.example.gamemate.domain.match.controller; -import com.example.gamemate.domain.match.dto.MatchFindResponseDto; +import com.example.gamemate.domain.match.dto.MatchResponseDto; import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; -import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -26,12 +25,12 @@ public class MatchController { * @return message = "매칭이 요청되었습니다." */ @PostMapping - public ResponseEntity createMatch( + public ResponseEntity createMatch( @RequestBody MatchCreateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - MatchCreateResponseDto matchCreateResponseDto = matchService.createMatch(dto, customUserDetails.getUser()); + MatchResponseDto matchCreateResponseDto = matchService.createMatch(dto, customUserDetails.getUser()); return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); } @@ -57,12 +56,12 @@ public ResponseEntity updateMatch( * @return matchFindResponseDtoList */ @GetMapping("/received-match") - public ResponseEntity> findAllReceivedMatch( + public ResponseEntity> findAllReceivedMatch( @AuthenticationPrincipal CustomUserDetails userDetails ) { - List matchFindResponseDtoList = matchService.findAllReceivedMatch(userDetails.getUser()); - return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); + List matchResponseDtoList = matchService.findAllReceivedMatch(userDetails.getUser()); + return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } /** @@ -70,12 +69,12 @@ public ResponseEntity> findAllReceivedMatch( * @return matchFindResponseDtoList */ @GetMapping("/sent-match") - public ResponseEntity> findAllSentMatch( + public ResponseEntity> findAllSentMatch( @AuthenticationPrincipal CustomUserDetails userDetails ) { - List matchFindResponseDtoList = matchService.findAllSentMatch(userDetails.getUser()); - return new ResponseEntity<>(matchFindResponseDtoList, HttpStatus.OK); + List matchResponseDtoList = matchService.findAllSentMatch(userDetails.getUser()); + return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } /** diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java deleted file mode 100644 index 6dcb140..0000000 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateResponseDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.gamemate.domain.match.dto; - -import lombok.Getter; - -@Getter -public class MatchCreateResponseDto { - private String message; - - public MatchCreateResponseDto(String message) { - this.message = message; - } -} diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java deleted file mode 100644 index 1655541..0000000 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchFindResponseDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.gamemate.domain.match.dto; - -import com.example.gamemate.domain.match.entity.Match; -import com.example.gamemate.domain.match.enums.MatchStatus; -import lombok.Getter; - -@Getter -public class MatchFindResponseDto { - private Long id; - private MatchStatus status; - private String nickname; - private String message; - - public MatchFindResponseDto(Long id, MatchStatus status, String nickname, String message) { - this.id = id; - this.status = status; - this.nickname = nickname; - this.message = message; - } - - public static MatchFindResponseDto toDto(Match match) { - return new MatchFindResponseDto(match.getId(), match.getStatus(), match.getSender().getNickname() , match.getMessage()); - } -} diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchResponseDto.java new file mode 100644 index 0000000..0b4e62e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchResponseDto.java @@ -0,0 +1,32 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.match.enums.MatchStatus; +import lombok.Getter; + +@Getter +public class MatchResponseDto { + private Long id; + private MatchStatus status; + private String senderNickname; + private String receiverNickname; + private String message; + + public MatchResponseDto(Long id, MatchStatus status, String senderNickname, String receiverNickname, String message) { + this.id = id; + this.status = status; + this.senderNickname = senderNickname; + this.receiverNickname = receiverNickname; + this.message = message; + } + + public static MatchResponseDto toDto(Match match) { + return new MatchResponseDto( + match.getId(), + match.getStatus(), + match.getSender().getNickname(), + match.getReceiver().getNickname(), + match.getMessage() + ); + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 85e0836..131ea18 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -1,11 +1,10 @@ package com.example.gamemate.domain.match.service; -import com.example.gamemate.domain.match.dto.MatchFindResponseDto; +import com.example.gamemate.domain.match.dto.MatchResponseDto; import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; import com.example.gamemate.domain.match.entity.Match; import com.example.gamemate.domain.match.enums.MatchStatus; import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; -import com.example.gamemate.domain.match.dto.MatchCreateResponseDto; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; @@ -28,7 +27,7 @@ public class MatchService { // 매칭 요청 생성 @Transactional - public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { + public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { User receiver = userRepository.findById(dto.getUserId()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -44,7 +43,7 @@ public MatchCreateResponseDto createMatch(MatchCreateRequestDto dto, User loginU Match match = new Match(dto.getMessage(), loginUser, receiver); matchRepository.save(match); - return new MatchCreateResponseDto("매칭이 요청되었습니다."); + return MatchResponseDto.toDto(match); } // 매칭 수락/거절 @@ -66,24 +65,24 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { } // 받은 매칭 전체 조회 - public List findAllReceivedMatch(User loginUser) { + public List findAllReceivedMatch(User loginUser) { List matchList = matchRepository.findAllByReceiverId(loginUser.getId()); return matchList .stream() - .map(MatchFindResponseDto::toDto) + .map(MatchResponseDto::toDto) .toList(); } // 보낸 매칭 전체 조회 - public List findAllSentMatch(User loginUser) { + public List findAllSentMatch(User loginUser) { List matchList = matchRepository.findAllBySenderId(loginUser.getId()); return matchList .stream() - .map(MatchFindResponseDto::toDto) + .map(MatchResponseDto::toDto) .toList(); } From dc3a50724d35e7333cee9c6f9158ef7a0806121a Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 14 Jan 2025 11:40:44 +0900 Subject: [PATCH 075/215] =?UTF-8?q?refactor=20:=20=ED=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20MatchController=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MatchController 에 팀 코드컨벤션에 맞지 않는 부분 수정 --- .../domain/match/controller/MatchController.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index ff8bc79..fc292ad 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -44,10 +44,10 @@ public ResponseEntity createMatch( public ResponseEntity updateMatch( @PathVariable Long id, @RequestBody MatchUpdateRequestDto dto, - @AuthenticationPrincipal CustomUserDetails userDetails + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - matchService.updateMatch(id, dto, userDetails.getUser()); + matchService.updateMatch(id, dto,customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -57,10 +57,10 @@ public ResponseEntity updateMatch( */ @GetMapping("/received-match") public ResponseEntity> findAllReceivedMatch( - @AuthenticationPrincipal CustomUserDetails userDetails + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List matchResponseDtoList = matchService.findAllReceivedMatch(userDetails.getUser()); + List matchResponseDtoList = matchService.findAllReceivedMatch(customUserDetails.getUser()); return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } @@ -70,10 +70,10 @@ public ResponseEntity> findAllReceivedMatch( */ @GetMapping("/sent-match") public ResponseEntity> findAllSentMatch( - @AuthenticationPrincipal CustomUserDetails userDetails + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List matchResponseDtoList = matchService.findAllSentMatch(userDetails.getUser()); + List matchResponseDtoList = matchService.findAllSentMatch(customUserDetails.getUser()); return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } @@ -85,10 +85,10 @@ public ResponseEntity> findAllSentMatch( @DeleteMapping("/{id}") public ResponseEntity deleteMatch( @PathVariable Long id, - @AuthenticationPrincipal CustomUserDetails userDetails + @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - matchService.deleteMatch(id, userDetails.getUser()); + matchService.deleteMatch(id, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } From 05b083d32137b67e9bc5d6adf3e5e7cec2f757f2 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:29:03 +0900 Subject: [PATCH 076/215] =?UTF-8?q?fix:=20PR=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. RequiredArgsConstructor 수정 --- .../gamemate/domain/review/controller/ReviewController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 94cc70a..3cd4907 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -10,6 +10,7 @@ import com.example.gamemate.global.provider.JwtTokenProvider; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -26,8 +27,8 @@ @RestController @RequestMapping("/games/{gameId}/reviews") -@AllArgsConstructor @Slf4j +@RequiredArgsConstructor public class ReviewController { private final ReviewService reviewService; From 73e1fe4654973efde999d78ac83ad0e19f2eb532 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 14 Jan 2025 13:23:07 +0900 Subject: [PATCH 077/215] =?UTF-8?q?feat=20:=20HttpMessageNotReadableExcept?= =?UTF-8?q?ion=20=EC=A0=84=EC=97=AD=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enum 을 Dto 로 받는 매칭 수락/거절에서 발생하는 HttpMessageNotReadableException 을 BadRequest 로 예외처리 --- .../gamemate/domain/match/controller/MatchController.java | 2 +- .../gamemate/global/exception/GlobalExceptionHandler.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index fc292ad..0c5eab5 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -47,7 +47,7 @@ public ResponseEntity updateMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - matchService.updateMatch(id, dto,customUserDetails.getUser()); + matchService.updateMatch(id, dto, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 2ffdffc..8b528e7 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import org.apache.coyote.Response; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -92,6 +93,12 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedException return handleExceptionInternal(errorCode, errorCode.getMessage()); } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn("handleHttpMessageNotReadableException", e); + ErrorCode errorCode = ErrorCode.INVALID_INPUT; + return handleExceptionInternal(errorCode); + } private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getStatus()) From 039b1924a870e97f1b684ece20330cbce760099f Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:24:02 +0900 Subject: [PATCH 078/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84/=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=EC=9A=94=EC=B2=AD=20=EA=B6=8C=ED=95=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5(=EA=B6=8C=EB=A6=AC=EC=9E=90=EB=A7=8C=20CRUD=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.게임/게임요청 권한 기능(권리자만 CRUD 가능) 추가 --- .../game/controller/GameController.java | 34 +++++------ .../GameEnrollRequestController.java | 38 ++++++------ .../dto/GameEnrollRequestResponseDto.java | 2 + .../game/dto/GameFindByIdResponseDto.java | 11 +--- .../domain/game/entity/GamaEnrollRequest.java | 9 ++- .../service/GameEnrollRequestService.java | 50 +++++++++++----- .../domain/game/service/GameService.java | 60 ++++++++++--------- 7 files changed, 117 insertions(+), 87 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index bcf9f42..2e426df 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -5,6 +5,8 @@ import com.example.gamemate.global.config.auth.CustomUserDetails; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -18,25 +20,22 @@ @RestController @RequestMapping("/games") @Slf4j +@RequiredArgsConstructor public class GameController { private final GameService gameService; - @Autowired - public GameController(GameService gameService) { - - this.gameService = gameService; - } - /** * * @param gameDataString * @param file + * @param customUserDetails * @return */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createGame( - @RequestPart(value = "gameData") String gameDataString, - @RequestPart(value = "file", required = false) MultipartFile file) { + @Valid @RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile file, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { ObjectMapper mapper = new ObjectMapper(); GameCreateRequestDto requestDto; @@ -46,13 +45,12 @@ public ResponseEntity createGame( throw new RuntimeException("Invalid JSON format", e); } - GameCreateResponseDto responseDto = gameService.createGame(requestDto, file); + GameCreateResponseDto responseDto = gameService.createGame(customUserDetails.getUser(), requestDto, file); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } /** * 게임 전체 조회 - * * @param page * @param size * @return @@ -87,9 +85,9 @@ public ResponseEntity> findAllGame( @GetMapping("/{id}") public ResponseEntity findGameById( @PathVariable Long id, - @AuthenticationPrincipal CustomUserDetails userDetails) { + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - GameFindByIdResponseDto gameById = gameService.findGameById(userDetails.getUser(), id); + GameFindByIdResponseDto gameById = gameService.findGameById(customUserDetails.getUser(), id); return new ResponseEntity<>(gameById, HttpStatus.OK); } @@ -104,8 +102,9 @@ public ResponseEntity findGameById( @PatchMapping("/{id}") public ResponseEntity updateGame( @PathVariable Long id, - @RequestPart(value = "gameData") String gameDataString, - @RequestPart(value = "file", required = false) MultipartFile newFile) { + @Valid @RequestPart(value = "gameData") String gameDataString, + @RequestPart(value = "file", required = false) MultipartFile newFile, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { ObjectMapper mapper = new ObjectMapper(); GameUpdateRequestDto requestDto; @@ -115,15 +114,16 @@ public ResponseEntity updateGame( throw new RuntimeException("Invalid JSON format", e); } - gameService.updateGame(id, requestDto, newFile); + gameService.updateGame(id, requestDto, newFile, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") public ResponseEntity deleteGame( - @PathVariable Long id) { + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - gameService.deleteGame(id); + gameService.deleteGame(id, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 47634d4..1e77186 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -4,23 +4,22 @@ import com.example.gamemate.domain.game.dto.GameEnrollRequestResponseDto; import com.example.gamemate.domain.game.dto.GameEnrollRequestUpdateRequestDto; import com.example.gamemate.domain.game.service.GameEnrollRequestService; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/games/requests") +@RequiredArgsConstructor public class GameEnrollRequestController { private final GameEnrollRequestService gameEnrollRequestService; - @Autowired - public GameEnrollRequestController( - GameEnrollRequestService gameEnrollRequestService) { - this.gameEnrollRequestService = gameEnrollRequestService; - } - /** * 게임등록 요청 * @@ -29,9 +28,10 @@ public GameEnrollRequestController( */ @PostMapping public ResponseEntity CreateGameEnrollRequest( - @RequestBody GameEnrollRequestCreateRequestDto requestDto) { + @RequestBody GameEnrollRequestCreateRequestDto requestDto, + @Valid @AuthenticationPrincipal CustomUserDetails customUserDetails) { - GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.createGameEnrollRequest(requestDto); + GameEnrollRequestResponseDto responseDto = gameEnrollRequestService.createGameEnrollRequest(requestDto, customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } @@ -41,9 +41,10 @@ public ResponseEntity CreateGameEnrollRequest( * @return */ @GetMapping - public ResponseEntity> findAllGameEnrollRequest() { + public ResponseEntity> findAllGameEnrollRequest( + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - Page gameEnrollRequestAll = gameEnrollRequestService.findAllGameEnrollRequest(); + Page gameEnrollRequestAll = gameEnrollRequestService.findAllGameEnrollRequest(customUserDetails.getUser()); return new ResponseEntity<>(gameEnrollRequestAll, HttpStatus.OK); } @@ -56,9 +57,10 @@ public ResponseEntity> findAllGameEnrollReque */ @GetMapping("/{id}") public ResponseEntity findGameEnrollRequestById( - @PathVariable Long id) { + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - GameEnrollRequestResponseDto gameEnrollRequestById = gameEnrollRequestService.findGameEnrollRequestById(id); + GameEnrollRequestResponseDto gameEnrollRequestById = gameEnrollRequestService.findGameEnrollRequestById(id, customUserDetails.getUser()); return new ResponseEntity<>(gameEnrollRequestById, HttpStatus.OK); } @@ -72,16 +74,18 @@ public ResponseEntity findGameEnrollRequestById( @PatchMapping("/{id}") public ResponseEntity updateGameEnroll( @PathVariable Long id, - @RequestBody GameEnrollRequestUpdateRequestDto requestDto) { + @Valid @RequestBody GameEnrollRequestUpdateRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { - gameEnrollRequestService.updateGameEnroll(id, requestDto); + gameEnrollRequestService.updateGameEnroll(id, requestDto, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/{id}") - public ResponseEntity deleteGame( - @PathVariable Long id) { - gameEnrollRequestService.deleteGame(id); + public ResponseEntity deleteGameEnroll( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + gameEnrollRequestService.deleteGameEnroll(id,customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java index 29f8308..8aa9d84 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java @@ -16,6 +16,7 @@ public class GameEnrollRequestResponseDto { private LocalDateTime createdAt; private LocalDateTime modifiedAt; private boolean isAccepted; + private Long userId; public GameEnrollRequestResponseDto(GamaEnrollRequest gameEnrollRequest ) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 @@ -28,6 +29,7 @@ public GameEnrollRequestResponseDto(GamaEnrollRequest gameEnrollRequest ) { this.createdAt = gameEnrollRequest.getCreatedAt(); this.modifiedAt = gameEnrollRequest.getModifiedAt(); this.isAccepted = gameEnrollRequest.getIsAccepted(); + this.userId = gameEnrollRequest.getUser().getId(); } } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java index 5e285dc..7213e8a 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Getter -@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt","fileName","imageUrl", "modifiedAt", "reviews" }) +@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt","fileName","imageUrl", "modifiedAt" }) public class GameFindByIdResponseDto { private final Long id; private final String title; @@ -20,11 +20,9 @@ public class GameFindByIdResponseDto { private final LocalDateTime modifiedAt; private final String fileName; private final String imageUrl; - private final Page reviews; - private final String nickname; // private final List reviews; - public GameFindByIdResponseDto(Game game, Page reviews, String nickname) { + public GameFindByIdResponseDto(Game game ) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 this.id = game.getId(); this.title = game.getTitle(); @@ -37,10 +35,5 @@ public GameFindByIdResponseDto(Game game, Page revie game.getImages().get(0).getFileName(); this.imageUrl = game.getImages().isEmpty() ? null : game.getImages().get(0).getFilePath(); - this.reviews = reviews; - this.nickname = nickname; -// this.reviews = game.getReviews().stream() -// .map(ReviewFindByAllResponseDto::new) -// .collect(Collectors.toList()); } } diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java index 0c79760..d353f51 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GamaEnrollRequest.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.game.entity; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; @@ -28,17 +29,19 @@ public class GamaEnrollRequest extends BaseEntity { @Column(name = "platform", length = 20) private String platform; -// @OneToMany(mappedBy = "game", cascade = CascadeType.ALL) -// private List gameImages = new ArrayList<>(); + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; @Column(name = "is_accepted", columnDefinition = "BOOLEAN DEFAULT false") private Boolean isAccepted = false; - public GamaEnrollRequest(String title, String genre, String platform, String description ) { + public GamaEnrollRequest(String title, String genre, String platform, String description, User userId ) { this.title = title; this.genre = genre; this.platform = platform; this.description = description; + this.user = userId; } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index d6f5acc..6834562 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -8,11 +8,12 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.repository.GameEnrollRequestRepository; import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.Role; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -20,9 +21,6 @@ import org.springframework.transaction.annotation.Transactional; -import static com.example.gamemate.global.constant.ErrorCode.GAME_NOT_FOUND; - - @Service @Slf4j @RequiredArgsConstructor @@ -30,18 +28,24 @@ public class GameEnrollRequestService { private final GameRepository gameRepository; private final GameEnrollRequestRepository gameEnrollRequestRepository; - public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto) { + public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto, User userId) { GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( requestDto.getTitle(), requestDto.getGenre(), requestDto.getPlatform(), - requestDto.getDescription() + requestDto.getDescription(), + userId ); GamaEnrollRequest saveEnrollRequest = gameEnrollRequestRepository.save(gameEnrollRequest); return new GameEnrollRequestResponseDto(saveEnrollRequest); } - public Page findAllGameEnrollRequest() { + public Page findAllGameEnrollRequest(User loginUser) { + + //관리자만 게임등록요청 조회 가능함(조회) + if (!loginUser.getRole().equals(Role.ADMIN)){ + throw new ApiException(ErrorCode.FORBIDDEN); + } Pageable pageable = PageRequest.of(0, 10); @@ -49,18 +53,29 @@ public Page findAllGameEnrollRequest() { } @Transactional - public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id) { + public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User loginUser) { + + //관리자만 게임등록요청 조회 가능함(조회) + if (!loginUser.getRole().equals(Role.ADMIN)){ + throw new ApiException(ErrorCode.FORBIDDEN); + } GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository.findById(id) - .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); return new GameEnrollRequestResponseDto(gamaEnrollRequest); } @Transactional - public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto) { + public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto, User loginUser) { + + //관리자만 게임등록요청 수정 가능함(수정) + if (!loginUser.getRole().equals(Role.ADMIN)){ + throw new ApiException(ErrorCode.FORBIDDEN); + } + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository - .findById(id).orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .findById(id).orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); gamaEnrollRequest.updateGameEnroll( requestDto.getTitle(), @@ -69,7 +84,8 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD requestDto.getDescription(), requestDto.getIsAccepted() ); - GamaEnrollRequest updateGameEnroll = gameEnrollRequestRepository.save(gamaEnrollRequest); + + gameEnrollRequestRepository.save(gamaEnrollRequest); // 만약에 관리자가 true로 바꾸면 게임등록도 함께 진행됨 Boolean accepted = requestDto.getIsAccepted(); @@ -84,9 +100,15 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD } } - public void deleteGame(Long id) { + public void deleteGameEnroll(Long id, User loginUser) { + + //관리자만 게임등록요청 삭제 가능함(삭제) + if (!loginUser.getRole().equals(Role.ADMIN)){ + throw new ApiException(ErrorCode.FORBIDDEN); + } + GamaEnrollRequest gamaEnrollRequest = gameEnrollRequestRepository - .findById(id).orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .findById(id).orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); gameEnrollRequestRepository.delete(gamaEnrollRequest); } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 7472e84..2209e29 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -10,6 +10,8 @@ import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.s3.S3Service; import lombok.RequiredArgsConstructor; @@ -26,8 +28,6 @@ import java.io.IOException; import java.util.List; -import static com.example.gamemate.global.constant.ErrorCode.GAME_NOT_FOUND; - @Service @Slf4j @@ -39,8 +39,12 @@ public class GameService { private final GameImageRepository gameImageRepository; @Transactional - public GameCreateResponseDto createGame(GameCreateRequestDto gameCreateRequestDto , MultipartFile file) { + public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gameCreateRequestDto, MultipartFile file) { + //관리자만 가능함(생성) + if (!loginUser.getRole().equals(Role.ADMIN)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } // 게임 엔티티 생성 Game game = new Game( gameCreateRequestDto.getTitle(), @@ -75,43 +79,39 @@ public Page findAllGame(int page, int size) { } @Transactional - public GameFindByIdResponseDto findGameById(User longinUser, Long id) { - - String nickName = longinUser.getNickname(); + public GameFindByIdResponseDto findGameById(User loginUser, Long id) { Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); - Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); - Page reviewPage = reviewRepository.findAllByGame(game, pageable); - - // Review를 ReviewFindByAllResponseDto로 변환하면서 닉네임 추가 - Page reviews = reviewPage.map(review -> - new ReviewFindByAllResponseDto(review, longinUser.getNickname()) - ); - - return new GameFindByIdResponseDto(game, reviews, nickName); + return new GameFindByIdResponseDto(game); } @Transactional - public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile) { + public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile, User loginUser) { + + //관리자만 가능함(수정) + if (!loginUser.getRole().equals(Role.ADMIN)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); // 기존 파일이 있고 새 파일이 업로드된 경우 - // 1. 기존 S3 파일 삭제 -// if (!game.getImages().isEmpty()) { -// for (GameImage image : game.getImages()) { -// s3Service.deleteFile(image.getFilePath()); -// } -// } + // 1. 기존 S3 파일 삭제 + if (!game.getImages().isEmpty()) { + for (GameImage image : game.getImages()) { + s3Service.deleteFile(image.getFilePath()); + } + } List gameImages = gameImageRepository.findGameImagesByGameId(id); if (!gameImages.isEmpty()) { gameImageRepository.deleteAll(gameImages); } - // 2. 새 파일 업로드 + // 2. 새 파일 업로드 if (newFile != null && !newFile.isEmpty()) { try { String fileUrl = s3Service.uploadFile(newFile); @@ -137,9 +137,15 @@ public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile n } @Transactional - public void deleteGame(Long id) { + public void deleteGame(Long id, User loginUser) { + + //관리자만 가능함(삭제) + if (!loginUser.getRole().equals(Role.ADMIN)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + Game game = gameRepository.findGameById(id) - .orElseThrow(() -> new ApiException(GAME_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); // 게임에 연결된 모든 이미지 삭제 if (!game.getImages().isEmpty()) { From 960e64f78ec880cb281342d5ddd3a9aac87142af Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:24:18 +0900 Subject: [PATCH 079/215] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0/=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.리뷰/보드 좋아요 / 싫어요/ 2, 좋아요 카운트 기능 --- .../like/controller/LikeController.java | 58 +++++++++++++++ .../like/dto/BoardLikeCountResponseDto.java | 15 ++++ .../like/dto/ReviewLikeCountResponseDto.java | 15 ++++ .../domain/like/entity/BoardLike.java | 38 ++++++++++ .../domain/like/entity/ReviewLike.java | 38 ++++++++++ .../like/repository/BoardLikeRepository.java | 18 +++++ .../like/repository/ReviewLikeRepository.java | 15 ++++ .../domain/like/service/LikeService.java | 70 +++++++++++++++++++ .../dto/ReviewFindByAllResponseDto.java | 4 +- .../domain/review/service/ReviewService.java | 16 +++-- 10 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/like/controller/LikeController.java create mode 100644 src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java create mode 100644 src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java create mode 100644 src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/like/service/LikeService.java diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java new file mode 100644 index 0000000..97ecff5 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -0,0 +1,58 @@ +package com.example.gamemate.domain.like.controller; + +import com.example.gamemate.domain.like.dto.BoardLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; +import com.example.gamemate.domain.like.service.LikeService; +import com.example.gamemate.global.config.auth.CustomUserDetails; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/likes") +@RequiredArgsConstructor +public class LikeController { + + private final LikeService likeService; + + @PostMapping("/reviews/{reviewId}") + public ResponseEntity reviewLikeUp( + @PathVariable Long reviewId, + @RequestBody Integer status, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + likeService.reviewLikeUp(reviewId, status, customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @PostMapping("/boards/{boardId}") + public ResponseEntity boardLikeUp( + @PathVariable Long boardId, + @RequestBody Integer status, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + likeService.boardLikeUp(boardId, status, customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping("/reviews/{reviewId}") + public ResponseEntity reviewLikeCount( + @PathVariable Long reviewId){ + + Long likeCount = likeService.getReivewLikeCount(reviewId); + ReviewLikeCountResponseDto responseDto = new ReviewLikeCountResponseDto(reviewId, likeCount); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @GetMapping("/boards/{boardId}") + public ResponseEntity boardLikeCount( + @PathVariable Long boardId){ + + Long likeCount = likeService.getBoardLikeCount(boardId); + BoardLikeCountResponseDto responseDto = new BoardLikeCountResponseDto(boardId, likeCount); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java new file mode 100644 index 0000000..8ef30e2 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java @@ -0,0 +1,15 @@ +package com.example.gamemate.domain.like.dto; + +import lombok.Getter; + +@Getter +public class BoardLikeCountResponseDto { + private Long boardId; + private Long likeCount; + + public BoardLikeCountResponseDto(Long boardId, Long likeCount){ + this.boardId = boardId; + this.likeCount = likeCount; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java new file mode 100644 index 0000000..9d08405 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java @@ -0,0 +1,15 @@ +package com.example.gamemate.domain.like.dto; + +import lombok.Getter; + +@Getter +public class ReviewLikeCountResponseDto { + private Long reviewId; + private Long likeCount; + + public ReviewLikeCountResponseDto(Long reviewId, Long likeCount){ + this.reviewId = reviewId; + this.likeCount = likeCount; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java b/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java new file mode 100644 index 0000000..e745b37 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java @@ -0,0 +1,38 @@ +package com.example.gamemate.domain.like.entity; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BoardLike { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Integer status; // 1: 좋아요, -1: 싫어요, 0: 무반응 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id") + private Board board; + + public BoardLike(Integer status, User user, Board board) { + this.status = status; + this.user = user; + this.board = board; + } + + // 좋아요 상태 변경을 위한 메서드 + public void changeStatus(Integer status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java b/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java new file mode 100644 index 0000000..8f0027d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java @@ -0,0 +1,38 @@ +package com.example.gamemate.domain.like.entity; + +import com.example.gamemate.domain.review.entity.Review; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ReviewLike { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Integer status; // 1: 좋아요, -1: 싫어요, 0: 무반응 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + + public ReviewLike(Integer status, User user, Review review) { + this.status = status; + this.user = user; + this.review = review; + } + + // 좋아요 상태 변경을 위한 메서드 + public void changeStatus(Integer status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java new file mode 100644 index 0000000..a7515f7 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java @@ -0,0 +1,18 @@ +package com.example.gamemate.domain.like.repository; + +import com.example.gamemate.domain.like.entity.BoardLike; +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 BoardLikeRepository extends JpaRepository { + + @Query("SELECT bl FROM BoardLike bl WHERE bl.board.boardId = :boardId AND bl.user.id = :userId") + Optional findByBoardIdAndUserId(@Param("boardId") Long boardId, @Param("userId") Long userId); + + Long countByBoardBoardIdAndStatus(Long boardId, Integer status); + + +} diff --git a/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java b/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java new file mode 100644 index 0000000..fb486cd --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java @@ -0,0 +1,15 @@ +package com.example.gamemate.domain.like.repository; + +import com.example.gamemate.domain.like.entity.ReviewLike; +import org.springframework.data.jpa.repository.JpaRepository; + + +import java.util.Optional; + +public interface ReviewLikeRepository extends JpaRepository { + + Optional findByReviewIdAndUserId(Long reviewId, Long userId); + + Long countByReviewIdAndStatus(Long reviewId, Integer status); + +} diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java new file mode 100644 index 0000000..58bc595 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -0,0 +1,70 @@ +package com.example.gamemate.domain.like.service; + +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.like.entity.BoardLike; +import com.example.gamemate.domain.like.entity.ReviewLike; +import com.example.gamemate.domain.like.repository.BoardLikeRepository; +import com.example.gamemate.domain.like.repository.ReviewLikeRepository; +import com.example.gamemate.domain.review.repository.ReviewRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class LikeService { + private final ReviewLikeRepository reviewLikeRepository; + private final UserRepository userRepository; + private final ReviewRepository reviewRepository; + private final BoardLikeRepository boardLikeRepository; + private final BoardRepository boardRepository; + + @Transactional + public void reviewLikeUp(Long reviewId, Integer status, User loginUser) { + + ReviewLike reviewLike = reviewLikeRepository.findByReviewIdAndUserId(reviewId, loginUser.getId()). + orElse(new ReviewLike( + status, + userRepository.findById(loginUser.getId()) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)), + reviewRepository.findById(reviewId) + .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)) + )); + + if (reviewLike.getId() == null) { + reviewLikeRepository.save(reviewLike); + } else { + reviewLike.changeStatus(status); + } + } + + @Transactional + public void boardLikeUp(Long boardId, Integer status, User loginUser) { + + BoardLike boardLike = boardLikeRepository.findByBoardIdAndUserId(boardId, loginUser.getId()). + orElse(new BoardLike( + status, + userRepository.findById(loginUser.getId()) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)), + boardRepository.findById(boardId) + .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)) + )); + + if (boardLike.getId() == null) { + boardLikeRepository.save(boardLike); + } else { + boardLike.changeStatus(status); + } + } + + public Long getBoardLikeCount(Long boardId) { + return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, 1); + } + public Long getReivewLikeCount(Long reviewId) { + return reviewLikeRepository.countByReviewIdAndStatus(reviewId, 1); + } +} diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java index 374ae7f..19aac73 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java @@ -14,8 +14,9 @@ public class ReviewFindByAllResponseDto { private Long userId; private LocalDateTime createdAt; private String nickName; + private Long likeCount; - public ReviewFindByAllResponseDto(Review review, String nickName) { + public ReviewFindByAllResponseDto(Review review, String nickName, Long likeCount) { this.id = review.getId(); this.content = review.getContent(); this.star = review.getStar(); @@ -23,5 +24,6 @@ public ReviewFindByAllResponseDto(Review review, String nickName) { this.userId = review.getUser().getId(); this.createdAt = review.getCreatedAt(); this.nickName = nickName; + this.likeCount = likeCount; } } diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 0e44882..86cf2d0 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.domain.like.repository.ReviewLikeRepository; import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; @@ -28,6 +29,7 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final GameRepository gameRepository; private final UserRepository userRepository; + private final ReviewLikeRepository reviewLikeRepository; @Transactional public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewCreateRequestDto requestDto) { @@ -101,11 +103,15 @@ public Page ReviewFindAllByGameId(Long gameId, User Page reviewPage = reviewRepository.findAllByGame(game, pageable); // Review를 ReviewFindByAllResponseDto로 변환하면서 닉네임 추가 - Page reviews = reviewPage.map(review -> - new ReviewFindByAllResponseDto(review, loginUser.getNickname()) - ); - - return reviewPage.map(review -> new ReviewFindByAllResponseDto(review, loginUser.getNickname())); +// Page reviews = reviewPage.map(review -> +// new ReviewFindByAllResponseDto(review, loginUser.getNickname()) +// ); +// +// return reviewPage.map(review -> new ReviewFindByAllResponseDto(review, loginUser.getNickname())); + return reviewPage.map(review -> { + Long likeCount = reviewLikeRepository.countByReviewIdAndStatus(review.getId(), 1); + return new ReviewFindByAllResponseDto(review, loginUser.getNickname(), likeCount); + }); } From c89ecf35b96f31fa46b5affec986c4fc9edc0d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 14 Jan 2025 19:42:09 +0900 Subject: [PATCH 080/215] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 토큰 방식에서 @AuthenticationPrincipal 방식으로 변경 --- .../user/controller/UserController.java | 29 +++++------ .../domain/user/service/UserService.java | 51 +++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index 64badb1..c49e22a 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -5,12 +5,14 @@ import com.example.gamemate.domain.user.dto.ProfileResponseDto; import com.example.gamemate.domain.user.dto.ProfileUpdateRequestDto; import com.example.gamemate.domain.user.service.UserService; +import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -23,10 +25,9 @@ public class UserController { @GetMapping("/{id}") public ResponseEntity findProfile( @PathVariable Long id, - @RequestHeader("Authorization") String token) { - - String jwtToken = token.substring(7); - ProfileResponseDto responseDto = userService.findProfile(id, jwtToken); + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + ProfileResponseDto responseDto = userService.findProfile(id, customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } @@ -34,9 +35,9 @@ public ResponseEntity findProfile( public ResponseEntity updateProfile( @PathVariable Long id, @Valid @RequestBody ProfileUpdateRequestDto requestDto, - @RequestHeader("Authorization") String token) { - String jwtToken = token.substring(7); - userService.updateProfile(id, requestDto.getNewNickname(), jwtToken); + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + userService.updateProfile(id, requestDto.getNewNickname(), customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -44,25 +45,21 @@ public ResponseEntity updateProfile( public ResponseEntity updatePassword( @PathVariable Long id, @Valid @RequestBody PasswordUpdateRequestDto requestDto, - @RequestHeader("Authorization") String token) { - - String jwtToken = token.substring(7); - userService.updatePassword(id, requestDto.getOldPassword(), requestDto.getNewPassword(), jwtToken); + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + userService.updatePassword(id, requestDto.getOldPassword(), requestDto.getNewPassword(), customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/withdraw") public ResponseEntity withdraw( - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails customUserDetails, HttpServletRequest request, HttpServletResponse response ) { - String jwtToken = token.substring(7); - userService.withdrawUser(jwtToken); - + userService.withdrawUser(customUserDetails.getUser()); authService.logout(request, response); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } - } diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index d37c79a..afc4934 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -24,9 +24,7 @@ public class UserService { private final AuthService authService; @Transactional(readOnly = true) - public ProfileResponseDto findProfile(Long id, String token) { - - validateToken(token); + public ProfileResponseDto findProfile(Long id, User loginUser) { User findUser = userRepository.findById(id) .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -37,28 +35,24 @@ public ProfileResponseDto findProfile(Long id, String token) { return new ProfileResponseDto(findUser); } - public void updateProfile(Long id, String newNickname, String token) { - - validateToken(token); + public void updateProfile(Long id, String newNickname, User loginUser) { User findUser = userRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - validateOwner(findUser, token); + validateOwner(findUser, loginUser); findUser.updateProfile(newNickname); User savedUser = userRepository.save(findUser); } - public void updatePassword(Long id, String oldPassword, String newPassword, String token) { - - validateToken(token); + public void updatePassword(Long id, String oldPassword, String newPassword, User loginUser) { User findUser = userRepository.findById(id) .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); - validateOwner(findUser, token); + validateOwner(findUser, loginUser); if(!passwordEncoder.matches(oldPassword, findUser.getPassword())) { throw new ApiException(ErrorCode.INVALID_PASSWORD); @@ -69,35 +63,24 @@ public void updatePassword(Long id, String oldPassword, String newPassword, Stri userRepository.save(findUser); } - public void withdrawUser(String token) { + public void withdrawUser(User loginUser) { - validateToken(token); + loginUser.deleteSoftly(); + loginUser.updateUserStatus(UserStatus.WITHDRAW); + loginUser.removeRefreshToken(); - String email = jwtTokenProvider.getEmailFromToken(token); - User findUser = userRepository.findByEmail(email) - .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); - - if(UserStatus.WITHDRAW.equals(findUser.getUserStatus())) { - throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } - - findUser.deleteSoftly(); - findUser.updateUserStatus(UserStatus.WITHDRAW); - findUser.removeRefreshToken(); - - userRepository.save(findUser); + userRepository.save(loginUser); } - private void validateToken(String token) { - if(!jwtTokenProvider.validateToken(token)) { - throw new ApiException(ErrorCode.INVALID_TOKEN); - } - } +// private void validateToken(String token) { +// if(!jwtTokenProvider.validateToken(token)) { +// throw new ApiException(ErrorCode.INVALID_TOKEN); +// } +// } - private void validateOwner(User user, String token) { - String emailFromToken = jwtTokenProvider.getEmailFromToken(token); - if(!user.getEmail().equals(emailFromToken)) { + private void validateOwner(User user, User loginUser) { + if(!user.getEmail().equals(loginUser.getEmail())) { throw new ApiException(ErrorCode.FORBIDDEN); } } From b4de20f8fc87578dcb38e643a905040d56127a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 14 Jan 2025 19:53:25 +0900 Subject: [PATCH 081/215] =?UTF-8?q?feat:=20OAuth2.0=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구글 로그인 - 카카오 로그인 --- build.gradle | 1 + .../auth/dto/OAuth2LoginResponseDto.java | 16 ++++ .../domain/auth/service/AuthService.java | 72 +++++++++++++++++ .../domain/auth/service/TokenService.java | 4 + .../gamemate/domain/user/entity/User.java | 22 ++++- .../domain/user/enums/AuthProvider.java | 18 +++++ .../global/config/SecurityConfig.java | 12 ++- .../config/auth/CustomOAuth2UserService.java | 42 ++++++++++ .../global/config/auth/CustomUserDetails.java | 18 ++++- .../config/auth/CustomUserDetailsService.java | 2 +- .../config/auth/OAuth2FailureHandler.java | 35 ++++++++ .../config/auth/OAuth2SuccessHandler.java | 80 +++++++++++++++++++ .../gamemate/global/constant/ErrorCode.java | 4 +- .../global/provider/JwtTokenProvider.java | 4 +- src/main/resources/application.properties | 24 +++++- 15 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/OAuth2LoginResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/TokenService.java create mode 100644 src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java diff --git a/build.gradle b/build.gradle index 907e424..5dce570 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'at.favre.lib:bcrypt:0.10.2' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2LoginResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2LoginResponseDto.java new file mode 100644 index 0000000..aa79043 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2LoginResponseDto.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.auth.dto; + +import com.example.gamemate.domain.user.enums.AuthProvider; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OAuth2LoginResponseDto { + + private final String providerId; + private final String email; + private final String name; + private final AuthProvider provider; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index fd53a7b..8826b62 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; @@ -15,6 +16,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; import java.util.Optional; @Service @@ -85,6 +87,36 @@ public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { return new TokenRefreshResponseDto(newAccessToken); } + public OAuth2LoginResponseDto extractOAuth2Attributes(AuthProvider provider, Map attributes) { + if(provider == AuthProvider.GOOGLE) { + return extractGoogleAttributes(attributes); + } else if(provider == AuthProvider.KAKAO) { + return extractKakaoAttributes(attributes); + } + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + + public User registerOAuth2User(OAuth2LoginResponseDto responseDto) { + User findUser = userRepository.findByEmail(responseDto.getEmail()) + .orElseGet(()-> { + User newUser = new User( + responseDto.getEmail(), + responseDto.getName(), + responseDto.getName(), + responseDto.getProvider(), + responseDto.getProviderId() + ); + return userRepository.save(newUser); + }); + if (findUser.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); + } + if (!findUser.getProvider().equals(responseDto.getProvider())) { + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + return findUser; + } + public void logout(HttpServletRequest request, HttpServletResponse response) { String refreshToken = extractRefreshTokenFromCookie(request); if(refreshToken != null) { @@ -121,4 +153,44 @@ private String extractRefreshTokenFromCookie(HttpServletRequest request) { } return null; } + + private OAuth2LoginResponseDto extractGoogleAttributes(Map attributes) { + return new OAuth2LoginResponseDto( + getSafeString(attributes.get("sub")), + getSafeString(attributes.get("email")), + getSafeString(attributes.get("name")), + AuthProvider.GOOGLE + ); + } + + private OAuth2LoginResponseDto extractKakaoAttributes(Map attributes) { + String providerId = getSafeString(attributes.get("id")); + + Map kakaoAccount = getSafeMap(attributes, "kakao_account"); + Map profile = getSafeMap(kakaoAccount, "profile"); + + return new OAuth2LoginResponseDto( + providerId, + getSafeString(kakaoAccount.get("email")), + getSafeString(profile.get("nickname")), + AuthProvider.KAKAO + ); + } + + private String getSafeString(Object obj) { + if (obj == null) { + throw new ApiException(ErrorCode.INVALID_OAUTH2_ATTRIBUTE); + } + return obj.toString(); + } + + private Map getSafeMap(Map attributes, String attributeName) { + Object attributeValue = attributes.get(attributeName); + // instanceof 검사를 통해 타입 안정성 확보, null 체크 포함 + if (!(attributeValue instanceof Map)) { + throw new ApiException(ErrorCode.INVALID_OAUTH2_ATTRIBUTE); + } + return (Map) attributeValue; + } + } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java new file mode 100644 index 0000000..198b246 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -0,0 +1,4 @@ +package com.example.gamemate.domain.auth.service; + +public class TokenService { +} diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index 2abeba0..f1a1270 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.user.entity; import com.example.gamemate.domain.follow.entity.Follow; +import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.global.common.BaseEntity; import com.example.gamemate.domain.user.enums.Role; import com.example.gamemate.domain.user.enums.UserStatus; @@ -30,7 +31,6 @@ public class User extends BaseEntity { @Column(nullable = false) private String nickname; - @Column(nullable = false) private String password; @Enumerated(EnumType.STRING) @@ -43,23 +43,43 @@ public class User extends BaseEntity { private String refreshToken; + @Enumerated(EnumType.STRING) + private AuthProvider provider; + + private String providerId; + @OneToMany(mappedBy = "follower") private List followingList; @OneToMany(mappedBy = "followee") private List followerList; + // 이메일 로그인용 생성자 public User(String email, String name, String nickname, String password) { this.email = email; this.name = name; this.nickname = nickname; this.password = password; + this.provider = AuthProvider.EMAIL; + this.providerId = null; this.role = Role.USER; this.isPremium = false; this.userStatus = UserStatus.ACTIVE; this.refreshToken = null; } + // OAuth용 생성자 + public User(String email, String name, String nickname, AuthProvider provider, String providerId) { + this.email = email; + this.name = name; + this.nickname = nickname; + this.provider = provider; + this.providerId = providerId; + this.role = Role.USER; + this.isPremium = false; + this.userStatus = UserStatus.ACTIVE; + } + public void updatePassword(String newPassword) { this.password = newPassword; } diff --git a/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java new file mode 100644 index 0000000..c0b338e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java @@ -0,0 +1,18 @@ +package com.example.gamemate.domain.user.enums; + +import lombok.Getter; + +@Getter +public enum AuthProvider { + + EMAIL("email"), + GOOGLE("user"), + KAKAO("admin"); + + private String name; + + AuthProvider(String name) { + this.name = name; + } + +} diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index 715d3c1..c9731ea 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -1,7 +1,6 @@ package com.example.gamemate.global.config; -import com.example.gamemate.global.config.auth.DelegatedAccessDeniedHandler; -import com.example.gamemate.global.config.auth.DelegatedAuthenticationEntryPoint; +import com.example.gamemate.global.config.auth.*; import com.example.gamemate.global.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -33,6 +32,9 @@ public class SecurityConfig { private final DelegatedAccessDeniedHandler accessDeniedHandler; private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; + private final CustomOAuth2UserService customOAuth2UserService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -43,10 +45,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/v3/api-docs/**", "/swagger-resources/**" ,"/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/auth/signup", "/auth/login", "auth/refresh").permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/code/**").permitAll() //Todo 관리자 접근 가능 url 수정 .requestMatchers("/관리자관련url").hasRole("admin") .anyRequest().authenticated() ) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler)) .exceptionHandling(hanling-> hanling .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)) diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java new file mode 100644 index 0000000..6f8b0e8 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java @@ -0,0 +1,42 @@ +package com.example.gamemate.global.config.auth; + +import com.example.gamemate.domain.auth.dto.OAuth2LoginResponseDto; +import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final AuthService authService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oauth2User = super.loadUser(userRequest); + + try { + AuthProvider provider = AuthProvider.valueOf( + userRequest.getClientRegistration().getRegistrationId().toUpperCase() + ); + + OAuth2LoginResponseDto attributes = authService.extractOAuth2Attributes( + provider, + oauth2User.getAttributes() + ); + + User user = authService.registerOAuth2User(attributes); + + return new CustomUserDetails(user, oauth2User.getAttributes()); + + } catch (Exception ex) { + throw new OAuth2AuthenticationException("소셜 로그인 처리 중 오류가 발생했습니다."); + } + } +} diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java index 7d303ed..118b830 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetails.java @@ -7,18 +7,33 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; @Getter @RequiredArgsConstructor @Slf4j -public class CustomUserDetails implements UserDetails { +public class CustomUserDetails implements UserDetails, OAuth2User { private final User user; + private final Map attributes; + // OAuth2User 구현 + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return user.getEmail(); + } + + // UserDetails 구현 @Override public Collection getAuthorities() { Collection authorities = new ArrayList<>(); @@ -59,4 +74,5 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + } diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java index f942848..42566e2 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomUserDetailsService.java @@ -20,6 +20,6 @@ public class CustomUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByEmail(username) .orElseThrow(()-> new UsernameNotFoundException("회원을 찾을 수 없습니다.")); - return new CustomUserDetails(user); + return new CustomUserDetails(user, null); } } diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java new file mode 100644 index 0000000..cf2bcda --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java @@ -0,0 +1,35 @@ +package com.example.gamemate.global.config.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Value("${oauth2.redirect-uri}") + private String redirectUri; + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException { + + String targetUrl = UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("error", exception.getMessage()) + .build().toUriString(); + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java new file mode 100644 index 0000000..25b90c1 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -0,0 +1,80 @@ +package com.example.gamemate.global.config.auth; + +import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.domain.user.service.UserService; +import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + private final AuthService authService; // UserRepository 대신 AuthService 사용 + private final UserRepository userRepository; + private int refreshTokenMaxAge = 1000 * 60 * 60 * 24 * 3; //3일 + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token"; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException { + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + User user = userDetails.getUser(); + + try { + String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); + + // AuthService에 토큰 저장 처리 위임 + user.updateRefreshToken(refreshToken); + userRepository.save(user); + + // 쿠키에 Refresh 토큰 저장 + addRefreshTokenCookie(response, refreshToken); + + // Access 토큰과 함께 리다이렉트 + getRedirectStrategy().sendRedirect( + request, + response, + determineTargetUrl(accessToken) + ); + } catch (Exception e) { + throw new IOException("OAuth2 인증 처리 중 오류가 발생했습니다.", e); + } + } + + private void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(refreshTokenMaxAge); + response.addCookie(cookie); + } + + @Value("${oauth2.redirect-uri}") + private String redirectUri; + + private String determineTargetUrl(String token) { + return UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("token", token) + .build().toUriString(); + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index a99a082..1704bd1 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -11,9 +11,11 @@ public enum ErrorCode { INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 회원입니다."), - IS_WITHDRAWN_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 회원입니다."), + IS_WITHDRAWN_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "비활성화된 회원입니다."), DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "DUPLICATE_USER", "이미 사용 중인 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "비밀번호가 일치하지 않습니다."), + INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST,"INVALID_PROVIDER_TYPE", "지원하지 않는 서비스 제공자입니다."), + INVALID_OAUTH2_ATTRIBUTE(HttpStatus.BAD_REQUEST, "INVALID_OAUTH2_ATTRIBUTE", "인증 정보가 유효하지 않습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java index bc416a4..6aa557d 100644 --- a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -21,8 +21,8 @@ public class JwtTokenProvider { @Value("${spring.jwt.secret}") private String secretKey; - private final long accessTokenExpirationMs = 1000 * 60 * 60; //60분 - private final long refreshTokenExpirationMs = 1000 * 60 * 60 * 24 * 3; //3일 + private final int accessTokenExpirationMs = 1000 * 60 * 60; //60분 + private final int refreshTokenExpirationMs = 1000 * 60 * 60 * 24 * 3; //3일 public String createAccessToken(String email, Role role) { Claims claims = Jwts.claims().setSubject(email); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 711ad87..1bd2b58 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,4 +17,26 @@ spring.jpa.generate-ddl=false spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true -spring.jwt.secret=${JWT_SECRET} \ No newline at end of file +spring.jwt.secret=${JWT_SECRET} + +# Google OAuth2 +spring.security.oauth2.client.registration.google.client-id=${OAUTH2_GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${OAUTH2_GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.scope=email,profile + +# Kakao OAuth2 +spring.security.oauth2.client.registration.kakao.client-id=${OAUTH2_KAKAO_CLIENT_ID} +spring.security.oauth2.client.registration.kakao.client-secret=${OAUTH2_KAKAO_CLIENT_SECRET} +spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/oauth/code +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=POST + +# Kakao Provider +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id + +oauth2.redirect.uri=http://localhost:8080/oauth2/redirect \ No newline at end of file From 403bc61bf997f923d07db10cbe6bcc736a205a62 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:07:00 +0900 Subject: [PATCH 082/215] =?UTF-8?q?fix:=20PR=20#40=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 좋아요 생성 시에 dto 리턴 2. 게임 단건 조회 로그인 없이 가능하도록 수정 3. DTO 줄 정리 --- .../game/controller/GameController.java | 10 ++-- .../domain/game/dto/ChatRequestDto.java | 50 +++++++++++++++++ .../domain/game/dto/ChatResponseDto.java | 56 +++++++++++++++++++ .../domain/game/dto/GameCreateRequestDto.java | 3 +- .../game/dto/GameCreateResponseDto.java | 2 +- .../GameEnrollRequestCreateRequestDto.java | 3 +- .../dto/GameEnrollRequestResponseDto.java | 2 +- .../GameEnrollRequestUpdateRequestDto.java | 2 +- .../game/dto/GameFindByIdResponseDto.java | 4 +- .../domain/game/service/GameService.java | 2 +- .../like/controller/LikeController.java | 22 ++++---- .../domain/like/dto/BoardLikeResponseDto.java | 20 +++++++ .../like/dto/ReviewLikeResponseDto.java | 19 +++++++ .../domain/like/service/LikeService.java | 12 +++- .../review/dto/ReviewCreateResponseDto.java | 12 ++-- .../dto/ReviewFindByAllResponseDto.java | 12 ++-- .../domain/review/service/ReviewService.java | 2 +- 17 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 2e426df..5d1460f 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -25,7 +25,6 @@ public class GameController { private final GameService gameService; /** - * * @param gameDataString * @param file * @param customUserDetails @@ -51,6 +50,7 @@ public ResponseEntity createGame( /** * 게임 전체 조회 + * * @param page * @param size * @return @@ -84,16 +84,14 @@ public ResponseEntity> findAllGame( */ @GetMapping("/{id}") public ResponseEntity findGameById( - @PathVariable Long id, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { + @PathVariable Long id) { - GameFindByIdResponseDto gameById = gameService.findGameById(customUserDetails.getUser(), id); + GameFindByIdResponseDto gameById = gameService.findGameById(id); return new ResponseEntity<>(gameById, HttpStatus.OK); } /** - * * @param id * @param gameDataString * @param newFile @@ -102,7 +100,7 @@ public ResponseEntity findGameById( @PatchMapping("/{id}") public ResponseEntity updateGame( @PathVariable Long id, - @Valid @RequestPart(value = "gameData") String gameDataString, + @Valid @RequestPart(value = "gameData") String gameDataString, @RequestPart(value = "file", required = false) MultipartFile newFile, @AuthenticationPrincipal CustomUserDetails customUserDetails) { diff --git a/src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java new file mode 100644 index 0000000..8ca1248 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java @@ -0,0 +1,50 @@ +package com.example.gamemate.domain.game.dto; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ChatRequestDto { + private List contents; + private GenerationConfig generationConfig; + + @Getter + @Setter + public static class Content { + private Parts parts; + } + + @Getter @Setter + public static class Parts { + private String text; + + } + + @Getter @Setter + public static class GenerationConfig { + private int candidate_count; + private int max_output_tokens; + private double temperature; + + } + + public ChatRequestDto(String prompt) { + this.contents = new ArrayList<>(); + Content content = new Content(); + Parts parts = new Parts(); + + parts.setText(prompt); + content.setParts(parts); + + this.contents.add(content); + this.generationConfig = new GenerationConfig(); + this.generationConfig.setCandidate_count(1); + this.generationConfig.setMax_output_tokens(1000); + this.generationConfig.setTemperature(0.7); + } +} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java new file mode 100644 index 0000000..3824370 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java @@ -0,0 +1,56 @@ +package com.example.gamemate.domain.game.dto; + +import lombok.*; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatResponseDto { + + private List candidates; + private PromptFeedback promptFeedback; + + @Getter + @Setter + public static class Candidate { + private Content content; + private String finishReason; + private int index; + private List safetyRatings; + + } + + @Getter + @Setter + @ToString + public static class Content { + private List parts; + private String role; + + } + + @Getter + @Setter + @ToString + public static class Parts { + private String text; + + } + + @Getter + @Setter + public static class SafetyRating { + private String category; + private String probability; + } + + @Getter + @Setter + public static class PromptFeedback { + private List safetyRatings; + + } +} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java index b385cac..4921c52 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java @@ -13,8 +13,7 @@ public class GameCreateRequestDto { private String description; - - public GameCreateRequestDto(String title, String genre, String platform , String description ) { + public GameCreateRequestDto(String title, String genre, String platform, String description) { this.title = title; this.genre = genre; this.platform = platform; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java index 4414f31..c9cca19 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java @@ -25,7 +25,7 @@ public GameCreateResponseDto(Game game) { this.platform = game.getPlatform(); this.description = game.getDescription(); this.createdAt = game.getCreatedAt(); - this.modifiedAt =game.getModifiedAt(); + this.modifiedAt = game.getModifiedAt(); this.fileName = game.getImages().isEmpty() ? null : game.getImages().get(0).getFileName(); this.imageUrl = game.getImages().isEmpty() ? null : diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java index 9e7c76d..0431070 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java @@ -11,8 +11,7 @@ public class GameEnrollRequestCreateRequestDto { private String description; - - public GameEnrollRequestCreateRequestDto(String title, String genre, String platform , String description ) { + public GameEnrollRequestCreateRequestDto(String title, String genre, String platform, String description) { this.title = title; this.genre = genre; this.platform = platform; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java index 8aa9d84..c0da8b1 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java @@ -18,7 +18,7 @@ public class GameEnrollRequestResponseDto { private boolean isAccepted; private Long userId; - public GameEnrollRequestResponseDto(GamaEnrollRequest gameEnrollRequest ) { + public GameEnrollRequestResponseDto(GamaEnrollRequest gameEnrollRequest) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 this.message = "게임등록 요청이 완료되었습니다."; this.id = gameEnrollRequest.getId(); diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java index 5887a8f..24f75e8 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java @@ -12,7 +12,7 @@ public class GameEnrollRequestUpdateRequestDto { private Boolean isAccepted; - public GameEnrollRequestUpdateRequestDto(String title, String genre, String platform , String description,Boolean isAccepted ) { + public GameEnrollRequestUpdateRequestDto(String title, String genre, String platform, String description, Boolean isAccepted) { this.title = title; this.genre = genre; this.platform = platform; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java index 7213e8a..ec1e63d 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Getter -@JsonPropertyOrder({ "id", "title", "genre", "platform", "description", "createdAt","fileName","imageUrl", "modifiedAt" }) +@JsonPropertyOrder({"id", "title", "genre", "platform", "description", "createdAt", "fileName", "imageUrl", "modifiedAt"}) public class GameFindByIdResponseDto { private final Long id; private final String title; @@ -22,7 +22,7 @@ public class GameFindByIdResponseDto { private final String imageUrl; // private final List reviews; - public GameFindByIdResponseDto(Game game ) { + public GameFindByIdResponseDto(Game game) { // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 this.id = game.getId(); this.title = game.getTitle(); diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 2209e29..7dfb6d0 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -79,7 +79,7 @@ public Page findAllGame(int page, int size) { } @Transactional - public GameFindByIdResponseDto findGameById(User loginUser, Long id) { + public GameFindByIdResponseDto findGameById(Long id) { Game game = gameRepository.findGameById(id) .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java index 97ecff5..1b3f24b 100644 --- a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -1,7 +1,9 @@ package com.example.gamemate.domain.like.controller; import com.example.gamemate.domain.like.dto.BoardLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.BoardLikeResponseDto; import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.ReviewLikeResponseDto; import com.example.gamemate.domain.like.service.LikeService; import com.example.gamemate.global.config.auth.CustomUserDetails; @@ -19,40 +21,40 @@ public class LikeController { private final LikeService likeService; @PostMapping("/reviews/{reviewId}") - public ResponseEntity reviewLikeUp( + public ResponseEntity reviewLikeUp( @PathVariable Long reviewId, @RequestBody Integer status, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - likeService.reviewLikeUp(reviewId, status, customUserDetails.getUser()); - return new ResponseEntity<>(HttpStatus.OK); + ReviewLikeResponseDto responseDto = likeService.reviewLikeUp(reviewId, status, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); } @PostMapping("/boards/{boardId}") - public ResponseEntity boardLikeUp( + public ResponseEntity boardLikeUp( @PathVariable Long boardId, @RequestBody Integer status, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - likeService.boardLikeUp(boardId, status, customUserDetails.getUser()); - return new ResponseEntity<>(HttpStatus.OK); + BoardLikeResponseDto responseDto = likeService.boardLikeUp(boardId, status, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); } @GetMapping("/reviews/{reviewId}") public ResponseEntity reviewLikeCount( - @PathVariable Long reviewId){ + @PathVariable Long reviewId) { Long likeCount = likeService.getReivewLikeCount(reviewId); ReviewLikeCountResponseDto responseDto = new ReviewLikeCountResponseDto(reviewId, likeCount); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return new ResponseEntity<>(responseDto, HttpStatus.OK); } @GetMapping("/boards/{boardId}") public ResponseEntity boardLikeCount( - @PathVariable Long boardId){ + @PathVariable Long boardId) { Long likeCount = likeService.getBoardLikeCount(boardId); BoardLikeCountResponseDto responseDto = new BoardLikeCountResponseDto(boardId, likeCount); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return new ResponseEntity<>(responseDto, HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java new file mode 100644 index 0000000..2b0a652 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.like.dto; + +import com.example.gamemate.domain.like.entity.BoardLike; +import com.example.gamemate.domain.like.entity.ReviewLike; +import lombok.Getter; + +@Getter +public class BoardLikeResponseDto { + private Long boardId; + private Long userId; + private Integer status; + + + public BoardLikeResponseDto(BoardLike boardLike){ + this.boardId = boardLike.getBoard().getBoardId(); + this.status = boardLike.getStatus(); + this.userId = boardLike.getUser().getId(); + } + +} diff --git a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java new file mode 100644 index 0000000..3341229 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.like.dto; + +import com.example.gamemate.domain.like.entity.ReviewLike; +import lombok.Getter; + +@Getter +public class ReviewLikeResponseDto { + private Long reviewId; + private Long userId; + private Integer status; + + + public ReviewLikeResponseDto(ReviewLike reviewLike){ + this.reviewId = reviewLike.getReview().getId(); + this.status = reviewLike.getStatus(); + this.userId = reviewLike.getUser().getId(); + } + +} diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 58bc595..05effbd 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -1,6 +1,9 @@ package com.example.gamemate.domain.like.service; import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.like.dto.BoardLikeResponseDto; +import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.ReviewLikeResponseDto; import com.example.gamemate.domain.like.entity.BoardLike; import com.example.gamemate.domain.like.entity.ReviewLike; import com.example.gamemate.domain.like.repository.BoardLikeRepository; @@ -24,7 +27,7 @@ public class LikeService { private final BoardRepository boardRepository; @Transactional - public void reviewLikeUp(Long reviewId, Integer status, User loginUser) { + public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User loginUser) { ReviewLike reviewLike = reviewLikeRepository.findByReviewIdAndUserId(reviewId, loginUser.getId()). orElse(new ReviewLike( @@ -40,10 +43,12 @@ public void reviewLikeUp(Long reviewId, Integer status, User loginUser) { } else { reviewLike.changeStatus(status); } + + return new ReviewLikeResponseDto(reviewLike); } @Transactional - public void boardLikeUp(Long boardId, Integer status, User loginUser) { + public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User loginUser) { BoardLike boardLike = boardLikeRepository.findByBoardIdAndUserId(boardId, loginUser.getId()). orElse(new BoardLike( @@ -59,11 +64,14 @@ public void boardLikeUp(Long boardId, Integer status, User loginUser) { } else { boardLike.changeStatus(status); } + + return new BoardLikeResponseDto(boardLike); } public Long getBoardLikeCount(Long boardId) { return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, 1); } + public Long getReivewLikeCount(Long reviewId) { return reviewLikeRepository.countByReviewIdAndStatus(reviewId, 1); } diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java index 85fac17..3dd8dad 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java @@ -7,12 +7,12 @@ @Getter public class ReviewCreateResponseDto { - private Long id; - private String content; - private Integer star; - private Long gameId; - private Long userId; - private LocalDateTime createdAt; + private Long id; + private String content; + private Integer star; + private Long gameId; + private Long userId; + private LocalDateTime createdAt; public ReviewCreateResponseDto(Review review) { this.id = review.getId(); diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java index 19aac73..f45de45 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java @@ -7,12 +7,12 @@ @Getter public class ReviewFindByAllResponseDto { - private Long id; - private String content; - private Integer star; - private Long gameId; - private Long userId; - private LocalDateTime createdAt; + private Long id; + private String content; + private Integer star; + private Long gameId; + private Long userId; + private LocalDateTime createdAt; private String nickName; private Long likeCount; diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index 86cf2d0..e3f8572 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -94,7 +94,7 @@ public void deleteReview(User loginUser, Long id) { } - public Page ReviewFindAllByGameId(Long gameId, User loginUser ){ + public Page ReviewFindAllByGameId(Long gameId, User loginUser) { Game game = gameRepository.findGameById(gameId) .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); From bd0840b925127c153d68efc5fff1f90c7ec6f287 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 15 Jan 2025 14:20:52 +0900 Subject: [PATCH 083/215] =?UTF-8?q?feat:=20=EB=8C=80=EB=8C=93=EA=B8=80=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 1. 대댓글 생성 2. 대댓글 수정 3. 대댓글 삭제 4. 게시판 단건 조회 시 대댓글 출력 5. 게시판 단건 조회 시에 게시판, 댓글에 작성자 이름 출력하도록 수정 --- .../board/dto/BoardFindOneResponseDto.java | 6 +- .../domain/board/service/BoardService.java | 43 +++++-- .../comment/dto/CommentFindResponseDto.java | 9 +- .../domain/comment/entity/Comment.java | 3 + .../reply/controller/ReplyController.java | 67 +++++++++++ .../reply/dto/ReplyFindResponseDto.java | 25 +++++ .../domain/reply/dto/ReplyRequestDto.java | 21 ++++ .../domain/reply/dto/ReplyResponseDto.java | 32 ++++++ .../gamemate/domain/reply/entity/Reply.java | 52 +++++++++ .../reply/repository/ReplyRepository.java | 13 +++ .../domain/reply/service/ReplyService.java | 105 ++++++++++++++++++ 11 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/dto/ReplyFindResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/dto/ReplyRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/dto/ReplyResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/entity/Reply.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/repository/ReplyRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java index 77bf9ab..02afae6 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java @@ -2,7 +2,6 @@ import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; -import com.example.gamemate.domain.comment.entity.Comment; import lombok.Getter; import java.time.LocalDate; @@ -15,16 +14,17 @@ public class BoardFindOneResponseDto { private final BoardCategory category; private final String title; private final String content; - //private final String nickname; + private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; private final List comments; - public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, LocalDateTime createdAt, LocalDateTime modifiedAt, List comments) { + public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime modifiedAt, List comments) { this.id = id; this.category = category; this.title = title; this.content = content; + this.nickname = nickname; this.createdAt = createdAt; this.modifiedAt = modifiedAt; this.comments = comments; diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 30fcbad..560b28c 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -11,6 +11,9 @@ import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; +import com.example.gamemate.domain.reply.entity.Reply; +import com.example.gamemate.domain.reply.repository.ReplyRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; @@ -22,7 +25,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @@ -32,6 +37,7 @@ public class BoardService { private final BoardRepository boardRepository; private final CommentRepository commentRepository; + private final ReplyRepository replyRepository; /** * 게시글 생성 메서드 @@ -95,19 +101,15 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { Page comments = commentRepository.findByBoard(findBoard,pageable); List commentDtos = comments.stream() - .map(comment-> new CommentFindResponseDto( - comment.getCommentId(), - comment.getContent(), - comment.getCreatedAt(), - comment.getModifiedAt() - )) - .collect(Collectors.toList()); + .map(this::convertCommentDto) + .collect(Collectors.toList()); return new BoardFindOneResponseDto( findBoard.getBoardId(), findBoard.getCategory(), findBoard.getTitle(), findBoard.getContent(), + findBoard.getUser().getNickname(), findBoard.getCreatedAt(), findBoard.getModifiedAt(), commentDtos @@ -152,4 +154,31 @@ public void deleteBoard(User loginUser, Long id) { boardRepository.delete(findBoard); } + + private CommentFindResponseDto convertCommentDto(Comment comment) { + List replyDtos = Optional.ofNullable(replyRepository.findByComment(comment)) + .orElse(Collections.emptyList()) + .stream() + .map(this::convertReplyDto) + .collect(Collectors.toList()); + return new CommentFindResponseDto( + comment.getCommentId(), + comment.getContent(), + comment.getUser().getNickname(), + comment.getCreatedAt(), + comment.getModifiedAt(), + replyDtos + ); + } + + private ReplyFindResponseDto convertReplyDto(Reply reply) { + String findUserName = reply.getParentReply() == null ? null : reply.getParentReply().getUser().getNickname(); + return new ReplyFindResponseDto( + reply.getReplyId(), + findUserName, + reply.getContent(), + reply.getCreatedAt(), + reply.getModifiedAt() + ); + } } diff --git a/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java b/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java index a825982..a45c897 100644 --- a/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/comment/dto/CommentFindResponseDto.java @@ -1,20 +1,27 @@ package com.example.gamemate.domain.comment.dto; +import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; import lombok.Getter; import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; @Getter public class CommentFindResponseDto { private final Long commentId; private final String content; + private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime updatedAt; + private final List replies; - public CommentFindResponseDto(Long commentId, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + public CommentFindResponseDto(Long commentId, String content, String nickname, LocalDateTime createdAt, LocalDateTime updatedAt, List replies) { this.commentId = commentId; this.content = content; + this.nickname = nickname; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.replies = replies; } } diff --git a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java index fd04489..bece5f7 100644 --- a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java +++ b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java @@ -1,12 +1,15 @@ package com.example.gamemate.domain.comment.entity; import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.reply.entity.Reply; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.List; + @Entity @Getter @NoArgsConstructor diff --git a/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java new file mode 100644 index 0000000..ef1ea52 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java @@ -0,0 +1,67 @@ +package com.example.gamemate.domain.reply.controller; + +import com.example.gamemate.domain.reply.dto.ReplyRequestDto; +import com.example.gamemate.domain.reply.dto.ReplyResponseDto; +import com.example.gamemate.domain.reply.service.ReplyService; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/boards/{boardId}/comments/{commentId}/replies") +public class ReplyController { + + private final ReplyService replyService; + + /** + * 대댓글 생성 API + * @param commentId + * @param requestDto + * @return + */ + @PostMapping + public ResponseEntity createReply( + @PathVariable Long commentId, + @Valid @RequestBody ReplyRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ){ + ReplyResponseDto dto = replyService.createReply(customUserDetails.getUser(), commentId, requestDto); + return new ResponseEntity<>(dto, HttpStatus.CREATED); + } + + /** + * 대댓글 수정 API + * @param id + * @param requestDto + * @return + */ + @PatchMapping("/{id}") + public ResponseEntity updateReply( + @PathVariable Long id, + @Valid @RequestBody ReplyRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ){ + replyService.updateReply(customUserDetails.getUser(), id, requestDto); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + /** + * 대댓글 삭제 API + * @param id + * @return + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteReply( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ){ + replyService.deleteReply(customUserDetails.getUser(), id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} + diff --git a/src/main/java/com/example/gamemate/domain/reply/dto/ReplyFindResponseDto.java b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyFindResponseDto.java new file mode 100644 index 0000000..2806e74 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyFindResponseDto.java @@ -0,0 +1,25 @@ +package com.example.gamemate.domain.reply.dto; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +@Getter +public class ReplyFindResponseDto { + private final Long replyId; + private final String parentReplyName; + private final String content; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + + public ReplyFindResponseDto(Long replyId, String parentReplyName, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.replyId = replyId; + this.parentReplyName = parentReplyName; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/example/gamemate/domain/reply/dto/ReplyRequestDto.java b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyRequestDto.java new file mode 100644 index 0000000..6c038a3 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyRequestDto.java @@ -0,0 +1,21 @@ +package com.example.gamemate.domain.reply.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class ReplyRequestDto { + @NotBlank(message="댓글 내용을 입력하세요.") + private String content; + + private Long parentReplyId; + + public ReplyRequestDto(String content, Long parentReplyId) { + this.content = content; + this.parentReplyId = parentReplyId; + } + + public ReplyRequestDto() { + } +} diff --git a/src/main/java/com/example/gamemate/domain/reply/dto/ReplyResponseDto.java b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyResponseDto.java new file mode 100644 index 0000000..809a1c3 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/dto/ReplyResponseDto.java @@ -0,0 +1,32 @@ +package com.example.gamemate.domain.reply.dto; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReplyResponseDto { + private final Long id; + private final Long commentId; + private Long parentReplyId; + private final String content; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public ReplyResponseDto(Long id, Long commentId, Long parentReplyId, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.commentId = commentId; + this.parentReplyId = parentReplyId; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public ReplyResponseDto(Long id, Long commentId, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.commentId = commentId; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java b/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java new file mode 100644 index 0000000..a542e6f --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java @@ -0,0 +1,52 @@ +package com.example.gamemate.domain.reply.entity; + +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "reply") +public class Reply extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long replyId; + + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "comment_id") + private Comment comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_reply_id") + private Reply parentReply; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + public Reply(String content, Comment comment, User user) { + this.content = content; + this.comment = comment; + this.user = user; + } + + public Reply(String content, Comment comment, User user, Reply parentReply) { + this.content = content; + this.comment = comment; + this.user = user; + this.parentReply = parentReply; + } + + public void updateReply(String content){ + this.content = content; + } +} diff --git a/src/main/java/com/example/gamemate/domain/reply/repository/ReplyRepository.java b/src/main/java/com/example/gamemate/domain/reply/repository/ReplyRepository.java new file mode 100644 index 0000000..69bf6fe --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/repository/ReplyRepository.java @@ -0,0 +1,13 @@ +package com.example.gamemate.domain.reply.repository; + +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.reply.entity.Reply; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReplyRepository extends JpaRepository { + List findByComment(Comment comment); + + List findByParentReply(Reply reply); +} diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java new file mode 100644 index 0000000..1a95bf8 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -0,0 +1,105 @@ +package com.example.gamemate.domain.reply.service; + +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.reply.dto.ReplyRequestDto; +import com.example.gamemate.domain.reply.dto.ReplyResponseDto; +import com.example.gamemate.domain.reply.entity.Reply; +import com.example.gamemate.domain.reply.repository.ReplyRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReplyService { + + private final ReplyRepository replyRepository; + private final CommentRepository commentRepository; + + /** + * 대댓글 생성 메서드 + * @param commentId + * @param requestDto + * @return + */ + @Transactional + public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequestDto requestDto) { + //댓글 조회 + Comment findComment = commentRepository.findById(commentId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + Reply newReply; + // 부모 대댓글 null 일 경우 + if(requestDto.getParentReplyId()==null){ + newReply = new Reply(requestDto.getContent(), findComment, loginUser); + Reply createReply = replyRepository.save(newReply); + + return new ReplyResponseDto( + createReply.getReplyId(), + createReply.getComment().getCommentId(), + createReply.getContent(), + createReply.getCreatedAt(), + createReply.getModifiedAt() + ); + }else{ + //대댓글 조회 + Reply findParentReply = replyRepository.findById(requestDto.getParentReplyId()) + .orElseThrow(()-> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + newReply = new Reply(requestDto.getContent(), findComment,loginUser, findParentReply); + Reply createReply = replyRepository.save(newReply); + + return new ReplyResponseDto( + createReply.getReplyId(), + createReply.getComment().getCommentId(), + createReply.getParentReply().getReplyId(), + createReply.getContent(), + createReply.getCreatedAt(), + createReply.getModifiedAt() + ); + } + } + + /** + * 대댓글 업데이트 메서드 + * @param id + * @param requestDto + */ + @Transactional + public void updateReply(User loginUser, Long id, ReplyRequestDto requestDto) { + // 대댓글 조회 + Reply findReply = replyRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 대댓글 작성자와 로그인 유저 확인 + if(!findReply.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + findReply.updateReply(requestDto.getContent()); + replyRepository.save(findReply); + } + + /** + * 대댓글 삭제 메서드 + * @param id + */ + @Transactional + public void deleteReply(User loginUser, Long id) { + // 대댓글 조회 + Reply findReply = replyRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 대댓글 작성자와 로그인 유저 확인 + if(!findReply.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + replyRepository.delete(findReply); + } +} From fc9488875e2b9fb6e6822eb02968bd1c713a3c31 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 15 Jan 2025 15:22:26 +0900 Subject: [PATCH 084/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=EC=97=90?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 25 ++++-- .../match/dto/CreateMyInfoRequestDto.java | 63 +++++++++++++++ .../match/dto/CreateMyInfoResponseDto.java | 55 ++++++++++++++ .../domain/match/entity/MatchDesiredInfo.java | 71 +++++++++++++++++ .../domain/match/entity/MatchUserInfo.java | 76 +++++++++++++++++++ .../gamemate/domain/match/enums/GameRank.java | 24 ++++++ .../gamemate/domain/match/enums/Gender.java | 15 ++++ .../gamemate/domain/match/enums/Lane.java | 20 +++++ .../domain/match/enums/PlayTimeRange.java | 19 +++++ .../gamemate/domain/match/enums/Purpose.java | 24 ++++++ .../MatchDesiredInfoRepository.java | 7 ++ .../repository/MatchUserInfoRepository.java | 7 ++ .../domain/match/service/MatchService.java | 30 +++++++- 13 files changed, 427 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java create mode 100644 src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/GameRank.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/Gender.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/Lane.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/PlayTimeRange.java create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/Purpose.java create mode 100644 src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 0c5eab5..445f1da 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,9 +1,7 @@ package com.example.gamemate.domain.match.controller; -import com.example.gamemate.domain.match.dto.MatchResponseDto; -import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; +import com.example.gamemate.domain.match.dto.*; import com.example.gamemate.domain.match.service.MatchService; -import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -22,7 +20,7 @@ public class MatchController { /** * 매칭 요청 생성 * @param dto MatchCreateRequestDto 상대 유저 id, 상대방에게 전할 메세지 - * @return message = "매칭이 요청되었습니다." + * @return matchCreateResponseDto */ @PostMapping public ResponseEntity createMatch( @@ -55,7 +53,7 @@ public ResponseEntity updateMatch( * 받은 매칭 전체 조회 * @return matchFindResponseDtoList */ - @GetMapping("/received-match") + @GetMapping("/received-matches") public ResponseEntity> findAllReceivedMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { @@ -68,7 +66,7 @@ public ResponseEntity> findAllReceivedMatch( * 보낸 매칭 전체 조회 * @return matchFindResponseDtoList */ - @GetMapping("/sent-match") + @GetMapping("/sent-matches") public ResponseEntity> findAllSentMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { @@ -91,4 +89,19 @@ public ResponseEntity deleteMatch( matchService.deleteMatch(id, customUserDetails.getUser()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + /** + * 내 매칭 정보 입력 + * @param dto CreateMyInfoRequestDto + * @return createMyInfoRequestDto + */ + @PostMapping("/my-info") + public ResponseEntity createMyInfo( + CreateMyInfoRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + CreateMyInfoResponseDto createMyInfoResponseDto = matchService.createMyInfo(dto, customUserDetails.getUser()); + return new ResponseEntity<>(createMyInfoResponseDto, HttpStatus.CREATED); + } } diff --git a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java new file mode 100644 index 0000000..8310bfd --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java @@ -0,0 +1,63 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.entity.MatchUserInfo; +import com.example.gamemate.domain.match.enums.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +import java.util.Set; + +@Getter +public class CreateMyInfoRequestDto { + @NotNull(message = "성별은 필수 입력값입니다.") + private Gender gender; + + @NotNull(message = "라인은 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "라인은 1-2개 선택 가능합니다.") + private Set lanes; + + @NotNull(message = "목적은 필수 입력값입니다.") + @Size(min = 1, max = 3, message = "목적은 1-3개 선택 가능합니다.") + private Set purposes; + + @NotNull(message = "게임 랭크는 필수 입력값입니다.") + private GameRank gameRank; + + @NotNull(message = "플레이 시간대는 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "플레이 시간대는 1-2개 선택 가능합니다.") + private Set playTimeRanges; + + @NotNull(message = "스킬 레벨은 필수 입력값입니다.") + @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") + @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + private Integer skillLevel; + + @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") + private Boolean micUsage; + + @Size(max = 200, message = "메시지는 200자를 초과할 수 없습니다.") + private String message; + + public CreateMyInfoRequestDto( + Gender gender, + Set lanes, + Set purposes, + GameRank gameRank, + Set playTimeRanges, + Integer skillLevel, + Boolean micUsage, + String message + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.gameRank = gameRank; + this.playTimeRanges = playTimeRanges; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.message = message; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java new file mode 100644 index 0000000..3e31ccf --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java @@ -0,0 +1,55 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.entity.MatchUserInfo; +import com.example.gamemate.domain.match.enums.*; +import lombok.Getter; + +import java.util.Set; + +@Getter +public class CreateMyInfoResponseDto { + private Long id; + private Gender gender; + private Set lanes; + private Set purposes; + private GameRank gameRank; + private Set playTimeRanges; + private Integer skillLevel; + private Boolean micUsage; + private String message; + + public CreateMyInfoResponseDto( + Long id, Gender gender, + Set lanes, + Set purposes, + GameRank gameRank, + Set playTimeRanges, + Integer skillLevel, + Boolean micUsage, + String message + ) { + this.id = id; + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.gameRank = gameRank; + this.playTimeRanges = playTimeRanges; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.message = message; + } + + public static CreateMyInfoResponseDto toDto(MatchUserInfo matchUserInfo) { + return new CreateMyInfoResponseDto( + matchUserInfo.getId(), + matchUserInfo.getGender(), + matchUserInfo.getLanes(), + matchUserInfo.getPurposes(), + matchUserInfo.getGameRank(), + matchUserInfo.getPlayTimeRanges(), + matchUserInfo.getSkillLevel(), + matchUserInfo.getMicUsage(), + matchUserInfo.getMessage() + ); + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java b/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java new file mode 100644 index 0000000..ac07d41 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java @@ -0,0 +1,71 @@ +package com.example.gamemate.domain.match.entity; + +import com.example.gamemate.domain.match.enums.*; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Entity +public class MatchDesiredInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private Gender gender; + + @ElementCollection + @CollectionTable(name = "desired_lanes") + @Enumerated(EnumType.STRING) + private Set lanes = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "desired_purposes") + @Enumerated(EnumType.STRING) + private Set purposes = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "desired_play_times") + @Enumerated(EnumType.STRING) + private Set playTimeRanges = new HashSet<>(); + + @Enumerated(EnumType.STRING) + private GameRank gameRank; + + @Column + private Integer skillLevel; + + @Column + private Boolean micUsage; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + public MatchDesiredInfo() { + } + + public MatchDesiredInfo( + Gender gender, + Set lanes, + Set purposes, + Set playTimeRanges, + GameRank gameRank, + Integer skillLevel, + Boolean micUsage, + User user + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.playTimeRanges = playTimeRanges; + this.gameRank = gameRank; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.user = user; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java new file mode 100644 index 0000000..e0da7ef --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java @@ -0,0 +1,76 @@ +package com.example.gamemate.domain.match.entity; + +import com.example.gamemate.domain.match.enums.*; +import com.example.gamemate.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Entity +public class MatchUserInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private Gender gender; + + @ElementCollection + @CollectionTable(name = "user_lanes") + @Enumerated(EnumType.STRING) + private Set lanes = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "user_purposes") + @Enumerated(EnumType.STRING) + private Set purposes = new HashSet<>(); + + @ElementCollection + @CollectionTable(name = "user_play_times") + @Enumerated(EnumType.STRING) + private Set playTimeRanges = new HashSet<>(); + + @Enumerated(EnumType.STRING) + private GameRank gameRank; + + @Column + private Integer skillLevel; + + @Column + private Boolean micUsage; + + @Column + private String message; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + public MatchUserInfo() { + } + + public MatchUserInfo( + Gender gender, + Set lanes, + Set purposes, + Set playTimeRanges, + GameRank gameRank, + Integer skillLevel, + Boolean micUsage, + String message, + User user + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.playTimeRanges = playTimeRanges; + this.gameRank = gameRank; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.message = message; + this.user = user; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java b/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java new file mode 100644 index 0000000..b488553 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java @@ -0,0 +1,24 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum GameRank { + IRON("iron", "아이언"), + BRONZE("bronze", "브론즈"), + SILVER("silver", "실버"), + GOLD("gold", "골드"), + PLATINUM("platinum", "플래티넘"), + DIAMOND("diamond", "다이아"), + MASTER("master", "마스터"), + GRANDMASTER("grandmaster", "그랜드마스터"), + CHALLENGER("challenger", "챌린저"); + + private final String name; + private final String koreanName; + + GameRank(String name, String koreanName) { + this.name = name; + this.koreanName = koreanName; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/Gender.java b/src/main/java/com/example/gamemate/domain/match/enums/Gender.java new file mode 100644 index 0000000..176d03a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/Gender.java @@ -0,0 +1,15 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum Gender { + MALE("male"), + FEMALE("female"); + + private final String name; + + Gender(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/Lane.java b/src/main/java/com/example/gamemate/domain/match/enums/Lane.java new file mode 100644 index 0000000..856276a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/Lane.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum Lane { + TOP("top", "탑"), + JUNGLE("jungle", "정글"), + MID("mid","정글"), + BOTTOM_AD("bottom_ad", "원딜"), + BOTTOM_SUPPORTER("bottom_supporter", "서포터"); + + private final String name; + private final String koreanName; + + Lane(String name, String koreanName) { + this.name = name; + this.koreanName = koreanName; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/PlayTimeRange.java b/src/main/java/com/example/gamemate/domain/match/enums/PlayTimeRange.java new file mode 100644 index 0000000..14cab4e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/PlayTimeRange.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum PlayTimeRange { + ZERO_TO_SIX("zero_to_six", "0~6시"), + SIX_TO_TWELVE("six_to_twelve", "6~12시"), + TWELVE_TO_EIGHTEEN("twelve_to_eighteen", "12~18시"), + EIGHTEEN_TO_TWENTY_FOUR("eighteen_to_twenty_four", "18시~24시"); + + private final String name; + private final String koreanName; + + PlayTimeRange(String name, String koreanName) { + this.name = name; + this.koreanName = koreanName; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/enums/Purpose.java b/src/main/java/com/example/gamemate/domain/match/enums/Purpose.java new file mode 100644 index 0000000..6bc7d7c --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/Purpose.java @@ -0,0 +1,24 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum Purpose { + JUST_FOR_FUN("just_for_fun", "즐겜"), + TRY_HARD("try_hard", "빡겜"), + RANK_GAME("rank_game", "랭겜"), + NORMAL_GAME("normal_game", "일반겜"), + TEAMWORK("teamwork", "팀워크"), + WANT_FRIEND("want_friend", "친구 구함"), + MENTORING("mentoring", "멘토/멘티 구함"), + DUO_PLAY("duo_play", "듀오할 사람 구함"), + BEGINNER_FRIENDLY("beginner_friendly","뉴비 구함"); + + private final String name; + private final String koreanName; + + Purpose(String name, String koreanName) { + this.name = name; + this.koreanName = koreanName; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java new file mode 100644 index 0000000..71406c6 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java @@ -0,0 +1,7 @@ +package com.example.gamemate.domain.match.repository; + +import com.example.gamemate.domain.match.entity.MatchDesiredInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MatchDesiredInfoRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java new file mode 100644 index 0000000..4467798 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java @@ -0,0 +1,7 @@ +package com.example.gamemate.domain.match.repository; + +import com.example.gamemate.domain.match.entity.MatchUserInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MatchUserInfoRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 131ea18..b50b4b3 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -1,11 +1,12 @@ package com.example.gamemate.domain.match.service; -import com.example.gamemate.domain.match.dto.MatchResponseDto; -import com.example.gamemate.domain.match.dto.MatchUpdateRequestDto; +import com.example.gamemate.domain.match.dto.*; import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.match.entity.MatchUserInfo; import com.example.gamemate.domain.match.enums.MatchStatus; -import com.example.gamemate.domain.match.dto.MatchCreateRequestDto; +import com.example.gamemate.domain.match.repository.MatchDesiredInfoRepository; import com.example.gamemate.domain.match.repository.MatchRepository; +import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; @@ -24,6 +25,8 @@ public class MatchService { private final UserRepository userRepository; private final MatchRepository matchRepository; + private final MatchUserInfoRepository matchUserInfoRepository; + private final MatchDesiredInfoRepository matchDesiredInfoRepository; // 매칭 요청 생성 @Transactional @@ -99,4 +102,25 @@ public void deleteMatch(Long id, User loginUser) { matchRepository.delete(findMatch); } + + // 내 정보 입력 + @Transactional + public CreateMyInfoResponseDto createMyInfo(CreateMyInfoRequestDto dto, User loginUser) { + + MatchUserInfo matchUserInfo = new MatchUserInfo( + dto.getGender(), + dto.getLanes(), + dto.getPurposes(), + dto.getPlayTimeRanges(), + dto.getGameRank(), + dto.getSkillLevel(), + dto.getMicUsage(), + dto.getMessage(), + loginUser + ); + + matchUserInfoRepository.save(matchUserInfo); + + return CreateMyInfoResponseDto.toDto(matchUserInfo); + } } From 394bd8b6c099bdac987393e4eff08b888b8560eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 14 Jan 2025 22:23:45 +0900 Subject: [PATCH 085/215] =?UTF-8?q?refactor:=20AuthService=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthService - TokenService - OAuth2Service --- .../domain/auth/service/AuthService.java | 41 +------- .../domain/auth/service/OAuth2Service.java | 93 +++++++++++++++++++ .../domain/auth/service/TokenService.java | 61 ++++++++++++ .../config/auth/CustomOAuth2UserService.java | 43 +++++++++ .../config/auth/OAuth2SuccessHandler.java | 78 ++++++++++++++++ 5 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/TokenService.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index fd53a7b..0a8415b 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -7,14 +7,12 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.provider.JwtTokenProvider; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - import java.util.Optional; @Service @@ -24,6 +22,7 @@ public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final TokenService tokenService; private final JwtTokenProvider jwtTokenProvider; public SignupResponseDto signup(SignupRequestDto requestDto) { @@ -56,16 +55,7 @@ public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto, HttpSer if(!passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) { throw new ApiException(ErrorCode.INVALID_PASSWORD); } - - String accessToken = jwtTokenProvider.createAccessToken(findUser.getEmail(), findUser.getRole()); - String refreshToken = jwtTokenProvider.createRefreshToken(findUser.getEmail()); - - findUser.updateRefreshToken(refreshToken); - userRepository.save(findUser); - - addRefreshTokenToCookie(response, refreshToken); - - return new EmailLoginResponseDto(accessToken); + return tokenService.generateLoginTokens(findUser, response); } public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { @@ -86,7 +76,7 @@ public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { } public void logout(HttpServletRequest request, HttpServletResponse response) { - String refreshToken = extractRefreshTokenFromCookie(request); + String refreshToken = tokenService.extractRefreshTokenFromCookie(request); if(refreshToken != null) { String email = jwtTokenProvider.getEmailFromToken(refreshToken); userRepository.findByEmail(email).ifPresent(user -> { @@ -94,31 +84,8 @@ public void logout(HttpServletRequest request, HttpServletResponse response) { userRepository.save(user); }); - Cookie cookie = new Cookie("refresh_token", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - response.addCookie(cookie); + tokenService.removeRefreshTokenCookie(response); } } - private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { - Cookie cookie = new Cookie("refresh_token", refreshToken); - cookie.setHttpOnly(true); - cookie.setSecure(true); // HTTPS에서만 전송 - cookie.setPath("/"); - cookie.setMaxAge(3 * 24 * 60 * 60); // 3일 - response.addCookie(cookie); - } - - private String extractRefreshTokenFromCookie(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if ("refresh_token".equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - return null; - } } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java new file mode 100644 index 0000000..688597e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java @@ -0,0 +1,93 @@ +package com.example.gamemate.domain.auth.service; + +import com.example.gamemate.domain.auth.dto.OAuth2LoginResponseDto; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; +import com.example.gamemate.domain.user.enums.UserStatus; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional +public class OAuth2Service { + + private final UserRepository userRepository; + + public OAuth2LoginResponseDto extractOAuth2Attributes(AuthProvider provider, Map attributes) { + if(provider == AuthProvider.GOOGLE) { + return extractGoogleAttributes(attributes); + } else if(provider == AuthProvider.KAKAO) { + return extractKakaoAttributes(attributes); + } + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + + public User registerOAuth2User(OAuth2LoginResponseDto responseDto) { + User findUser = userRepository.findByEmail(responseDto.getEmail()) + .orElseGet(() -> { + User newUser = new User( + responseDto.getEmail(), + responseDto.getName(), + responseDto.getName(), + responseDto.getProvider(), + responseDto.getProviderId() + ); + return userRepository.save(newUser); + }); + + if (findUser.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); + } + if (!findUser.getProvider().equals(responseDto.getProvider())) { + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + return findUser; + } + + private OAuth2LoginResponseDto extractGoogleAttributes(Map attributes) { + return new OAuth2LoginResponseDto( + getSafeString(attributes.get("sub")), + getSafeString(attributes.get("email")), + getSafeString(attributes.get("name")), + AuthProvider.GOOGLE + ); + } + + private OAuth2LoginResponseDto extractKakaoAttributes(Map attributes) { + String providerId = getSafeString(attributes.get("id")); + + Map kakaoAccount = getSafeMap(attributes, "kakao_account"); + Map profile = getSafeMap(kakaoAccount, "profile"); + + return new OAuth2LoginResponseDto( + providerId, + getSafeString(kakaoAccount.get("email")), + getSafeString(profile.get("nickname")), + AuthProvider.KAKAO + ); + } + + private String getSafeString(Object obj) { + if (obj == null) { + throw new ApiException(ErrorCode.INVALID_OAUTH2_ATTRIBUTE); + } + return obj.toString(); + } + + private Map getSafeMap(Map attributes, String attributeName) { + Object attributeValue = attributes.get(attributeName); + // instanceof 검사를 통해 타입 안정성 확보, null 체크 포함 + if (!(attributeValue instanceof Map)) { + throw new ApiException(ErrorCode.INVALID_OAUTH2_ATTRIBUTE); + } + return (Map) attributeValue; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java new file mode 100644 index 0000000..1ac17d1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -0,0 +1,61 @@ +package com.example.gamemate.domain.auth.service; + +import com.example.gamemate.domain.auth.dto.EmailLoginResponseDto; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class TokenService { + + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + + public EmailLoginResponseDto generateLoginTokens(User user, HttpServletResponse response) { + String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); + + user.updateRefreshToken(refreshToken); + userRepository.save(user); + + addRefreshTokenToCookie(response, refreshToken); + return new EmailLoginResponseDto(accessToken); + } + + public String extractRefreshTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("refresh_token".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } + + private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie("refresh_token", refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(3 * 24 * 60 * 60); // 3일 + response.addCookie(cookie); + } + + public void removeRefreshTokenCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("refresh_token", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + +} diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java new file mode 100644 index 0000000..78d5f66 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java @@ -0,0 +1,43 @@ +package com.example.gamemate.global.config.auth; + +import com.example.gamemate.domain.auth.dto.OAuth2LoginResponseDto; +import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.domain.auth.service.OAuth2Service; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final OAuth2Service oAuth2Service; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oauth2User = super.loadUser(userRequest); + + try { + AuthProvider provider = AuthProvider.valueOf( + userRequest.getClientRegistration().getRegistrationId().toUpperCase() + ); + + OAuth2LoginResponseDto attributes = oAuth2Service.extractOAuth2Attributes( + provider, + oauth2User.getAttributes() + ); + + User user = oAuth2Service.registerOAuth2User(attributes); + + return new CustomUserDetails(user, oauth2User.getAttributes()); + + } catch (Exception ex) { + throw new OAuth2AuthenticationException("소셜 로그인 처리 중 오류가 발생했습니다."); + } + } +} diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java new file mode 100644 index 0000000..4e9f113 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -0,0 +1,78 @@ +package com.example.gamemate.global.config.auth; + +import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.domain.user.service.UserService; +import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private int refreshTokenMaxAge = 1000 * 60 * 60 * 24 * 3; //3일 + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token"; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException { + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + User user = userDetails.getUser(); + + try { + String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); + + user.updateRefreshToken(refreshToken); + userRepository.save(user); + + // 쿠키에 Refresh 토큰 저장 + addRefreshTokenCookie(response, refreshToken); + + // Access 토큰과 함께 리다이렉트 + getRedirectStrategy().sendRedirect( + request, + response, + determineTargetUrl(accessToken) + ); + } catch (Exception e) { + throw new IOException("OAuth2 인증 처리 중 오류가 발생했습니다.", e); + } + } + + private void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(refreshTokenMaxAge); + response.addCookie(cookie); + } + + @Value("${oauth2.redirect-uri}") + private String redirectUri; + + private String determineTargetUrl(String token) { + return UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("token", token) + .build().toUriString(); + } +} From fd7cd2b32d23a5829acb128a309e3749cb2452e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 14 Jan 2025 22:27:49 +0900 Subject: [PATCH 086/215] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=EC=97=90=20INTERNAL=5FSERVER=5FE?= =?UTF-8?q?RROR=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/global/exception/GlobalExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 2ffdffc..e256739 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -93,6 +93,14 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedException } + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Unhandled exception occurred", e); + ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; + return handleExceptionInternal(errorCode); + } + + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getStatus()) .body(makeErrorResponse(errorCode)); From c5eb3ce98eb15299c70b50b141f9c4d46644b9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 14 Jan 2025 22:28:39 +0900 Subject: [PATCH 087/215] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @AuthenticationPrincipal을 사용한 인증 방식으로 변경 --- .../domain/auth/controller/AuthController.java | 5 ++++- .../gamemate/domain/auth/service/AuthService.java | 10 +++------- .../domain/user/controller/UserController.java | 2 +- .../example/gamemate/global/config/SecurityConfig.java | 2 +- .../global/filter/JwtAuthenticationFilter.java | 3 ++- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index 60edaad..8aff6d4 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -2,12 +2,14 @@ import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -35,10 +37,11 @@ public ResponseEntity emailLogin( @PostMapping("/logout") public ResponseEntity logout( + @AuthenticationPrincipal CustomUserDetails customUserDetails, HttpServletRequest request, HttpServletResponse response ) { - authService.logout(request, response); + authService.logout(customUserDetails.getUser(), request, response); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index 0a8415b..4fed1d3 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -75,15 +75,11 @@ public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { return new TokenRefreshResponseDto(newAccessToken); } - public void logout(HttpServletRequest request, HttpServletResponse response) { + public void logout(User user, HttpServletRequest request, HttpServletResponse response) { String refreshToken = tokenService.extractRefreshTokenFromCookie(request); if(refreshToken != null) { - String email = jwtTokenProvider.getEmailFromToken(refreshToken); - userRepository.findByEmail(email).ifPresent(user -> { - user.removeRefreshToken(); - userRepository.save(user); - }); - + user.removeRefreshToken(); + userRepository.save(user); tokenService.removeRefreshTokenCookie(response); } } diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index c49e22a..45256c8 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -58,7 +58,7 @@ public ResponseEntity withdraw( HttpServletResponse response ) { userService.withdrawUser(customUserDetails.getUser()); - authService.logout(request, response); + authService.logout(customUserDetails.getUser(), request, response); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index 715d3c1..ece159b 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -67,7 +67,7 @@ public PasswordEncoder passwordEncoder() { @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService); + authProvider.setUserDetailsService(userDetailsService); // 여기서 CustomUserDetailsService가 주입됨 authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } diff --git a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java index d55ea4b..57abed7 100644 --- a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.example.gamemate.global.filter; import ch.qos.logback.core.util.StringUtil; +import com.example.gamemate.global.config.auth.CustomUserDetails; import com.example.gamemate.global.provider.JwtTokenProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -62,7 +63,7 @@ protected void doFilterInternal(HttpServletRequest request, } private Authentication createAuthentication(String email) { - UserDetails userDetails = userDetailsService.loadUserByUsername(email); + CustomUserDetails userDetails = (CustomUserDetails) userDetailsService.loadUserByUsername(email); return new UsernamePasswordAuthenticationToken( userDetails, From 7bee8f78fd9e0fea540e8b5c3e11847a087765d4 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:15:12 +0900 Subject: [PATCH 088/215] =?UTF-8?q?fix:=20=20@Transactional=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. @Transactional 누락 수정 --- .../com/example/gamemate/domain/game/entity/Game.java | 2 -- .../domain/game/service/GameEnrollRequestService.java | 4 +++- .../example/gamemate/domain/game/service/GameService.java | 2 +- .../gamemate/domain/review/service/ReviewService.java | 8 ++------ 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/entity/Game.java b/src/main/java/com/example/gamemate/domain/game/entity/Game.java index 76bedd1..a695b0e 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/Game.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/Game.java @@ -3,8 +3,6 @@ import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index 6834562..f66d7fa 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -28,6 +28,7 @@ public class GameEnrollRequestService { private final GameRepository gameRepository; private final GameEnrollRequestRepository gameEnrollRequestRepository; + @Transactional public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto, User userId) { GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( requestDto.getTitle(), @@ -52,7 +53,7 @@ public Page findAllGameEnrollRequest(User loginUse return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); } - @Transactional + public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User loginUser) { //관리자만 게임등록요청 조회 가능함(조회) @@ -100,6 +101,7 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD } } + @Transactional public void deleteGameEnroll(Long id, User loginUser) { //관리자만 게임등록요청 삭제 가능함(삭제) diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 7dfb6d0..f277b27 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -78,7 +78,7 @@ public Page findAllGame(int page, int size) { return gameRepository.findAll(pageable).map(GameFindAllResponseDto::new); } - @Transactional + public GameFindByIdResponseDto findGameById(Long id) { Game game = gameRepository.findGameById(id) diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index e3f8572..a8a3b96 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -59,6 +59,7 @@ public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewC return new ReviewCreateResponseDto(saveReview); } + @Transactional public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateRequestDto requestDto) { Long userId = loginUser.getId(); @@ -79,6 +80,7 @@ public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateReque reviewRepository.save(review); } + @Transactional public void deleteReview(User loginUser, Long id) { Long userId = loginUser.getId(); @@ -102,12 +104,6 @@ public Page ReviewFindAllByGameId(Long gameId, User Pageable pageable = PageRequest.of(0, 5, Sort.by("createdAt").descending()); Page reviewPage = reviewRepository.findAllByGame(game, pageable); - // Review를 ReviewFindByAllResponseDto로 변환하면서 닉네임 추가 -// Page reviews = reviewPage.map(review -> -// new ReviewFindByAllResponseDto(review, loginUser.getNickname()) -// ); -// -// return reviewPage.map(review -> new ReviewFindByAllResponseDto(review, loginUser.getNickname())); return reviewPage.map(review -> { Long likeCount = reviewLikeRepository.countByReviewIdAndStatus(review.getId(), 1); return new ReviewFindByAllResponseDto(review, loginUser.getNickname(), likeCount); From 3166da3e2952e591ba36282d65df85239de8dd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 15 Jan 2025 14:07:51 +0900 Subject: [PATCH 089/215] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../auth/controller/AuthController.java | 24 ++++ .../auth/dto/EamilVerifyRequestDto.java | 19 +++ .../dto/EmailVerificationCodeRequestDto.java | 16 +++ .../domain/auth/service/EmailService.java | 122 ++++++++++++++++++ .../global/config/SecurityConfig.java | 14 +- .../global/config/auth/MailConfig.java | 41 ++++++ .../gamemate/global/constant/ErrorCode.java | 16 ++- .../global/provider/JwtTokenProvider.java | 7 +- src/main/resources/application.properties | 33 ++++- 10 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/EamilVerifyRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/EmailVerificationCodeRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/EmailService.java create mode 100644 src/main/java/com/example/gamemate/global/config/auth/MailConfig.java diff --git a/build.gradle b/build.gradle index 907e424..bb4877e 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'at.favre.lib:bcrypt:0.10.2' + implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index 8aff6d4..e0c4b7b 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -2,6 +2,11 @@ import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.auth.service.AuthService; +import com.example.gamemate.domain.auth.service.EmailService; +import com.example.gamemate.domain.auth.service.OAuth2Service; +import com.example.gamemate.domain.auth.service.TokenService; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -18,6 +23,7 @@ public class AuthController { private final AuthService authService; + private final EmailService emailService; @PostMapping("/signup") public ResponseEntity signup( @@ -26,6 +32,24 @@ public ResponseEntity signup( SignupResponseDto responseDto = authService.signup(requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } + + @PostMapping("/email/verification-request") + public ResponseEntity sendVerificationEmail( + @Valid @RequestBody EmailVerificationCodeRequestDto requestDto + ) { + emailService.sendVerificationEmail(requestDto.getEmail()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @PostMapping("/email/verify") + public ResponseEntity verifyEmail( + @Valid @RequestBody EamilVerifyRequestDto requestDto + ) { + emailService.verifyEmail(requestDto.getEmail(), requestDto.getCode()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @PostMapping("/login") public ResponseEntity emailLogin( @Valid @RequestBody EmailLoginRequestDto requestDto, diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EamilVerifyRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/EamilVerifyRequestDto.java new file mode 100644 index 0000000..2149b02 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/EamilVerifyRequestDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class EamilVerifyRequestDto { + + @NotBlank(message = "이메일을 입력해주세요.") + @Email(message = "이메일 형식을 확인해주세요.") + private final String email; + + @NotBlank(message = "인증 코드를 입력해주세요.") + private final String code; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailVerificationCodeRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/EmailVerificationCodeRequestDto.java new file mode 100644 index 0000000..9993ec7 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/EmailVerificationCodeRequestDto.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class EmailVerificationCodeRequestDto { + + @NotBlank(message = "이메일을 입력해주세요.") + @Email(message = "이메일 형식을 확인해주세요.") + private final String email; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java new file mode 100644 index 0000000..e884f21 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java @@ -0,0 +1,122 @@ +package com.example.gamemate.domain.auth.service; + +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@RequiredArgsConstructor +@Transactional +public class EmailService { + + private static final long VERIFICATION_TIME_LIMIT = 5; //5분 + private final JavaMailSender emailSender; + private final UserRepository userRepository; + private final Map verificationMap = new ConcurrentHashMap<>(); + + public void sendVerificationEmail(String email) { + // 이미 가입된 이메일인지 확인 + if (userRepository.findByEmail(email).isPresent()) { + throw new ApiException(ErrorCode.DUPLICATE_EMAIL); + } + + String verificationCode = generateVerificationCode(); + + try { + MimeMessage message = createEmailMessage(email, verificationCode); + emailSender.send(message); + + // 메모리에 인증 정보 저장 + // Todo: 추후 Redis로 수정 예정 + verificationMap.put(email, new VerificationInfo( + verificationCode, + LocalDateTime.now().plusMinutes(VERIFICATION_TIME_LIMIT) + )); + } catch (MailException e) { + throw new ApiException(ErrorCode.EMAIL_SEND_ERROR); + } + } + + public void verifyEmail(String email, String code) { + VerificationInfo verificationInfo = getVerificationInfo(email); + if (verificationInfo == null) { + throw new ApiException(ErrorCode.VERIFICATION_TIME_EXPIRED); + } + + if (LocalDateTime.now().isAfter(verificationInfo.getExpiryTime())) { + verificationMap.remove(email); + throw new ApiException(ErrorCode.VERIFICATION_TIME_EXPIRED); + } + + if (!verificationInfo.getCode().equals(code)) { + throw new ApiException(ErrorCode.INVALID_VERIFICATION_CODE); + } + + verificationMap.remove(email); + } + + // Todo: 추후 Redis로 수정 예정 + private VerificationInfo getVerificationInfo(String email) { + VerificationInfo info = verificationMap.get(email); + + if (info != null && LocalDateTime.now().isAfter(info.getExpiryTime())) { + verificationMap.remove(email); + return null; + } + + return info; + } + + private String generateVerificationCode() { + return UUID.randomUUID().toString().substring(0, 8); + } + + private MimeMessage createEmailMessage(String email, String code) { + try { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom("lee24pm@gmail.com"); + helper.setTo(email); + helper.setSubject("[GameMate] 이메일 인증"); + helper.setText(createEmailContent(code), true); + + return message; + } catch (MessagingException e) { + throw new ApiException(ErrorCode.EMAIL_SEND_ERROR); + } + } + + private String createEmailContent(String code) { + return String.format( + "
" + + "

GameMate 이메일 인증

" + + "
" + + "

아래 인증 코드를 입력해주세요.

" + + "
" + + "
인증 코드 : %s
" + + "
", code); + } + + @Getter + @RequiredArgsConstructor + private static class VerificationInfo { + private final String code; + private final LocalDateTime expiryTime; + } + +} diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index ece159b..e924722 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -1,7 +1,6 @@ package com.example.gamemate.global.config; -import com.example.gamemate.global.config.auth.DelegatedAccessDeniedHandler; -import com.example.gamemate.global.config.auth.DelegatedAuthenticationEntryPoint; +import com.example.gamemate.global.config.auth.*; import com.example.gamemate.global.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -33,6 +32,9 @@ public class SecurityConfig { private final DelegatedAccessDeniedHandler accessDeniedHandler; private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; + private final CustomOAuth2UserService customOAuth2UserService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -42,11 +44,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/v3/api-docs/**", "/swagger-resources/**" ,"/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/auth/signup", "/auth/login", "auth/refresh").permitAll() + .requestMatchers("/auth/signup", "/auth/login", "auth/refresh", "auth/email/**").permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/code/**").permitAll() //Todo 관리자 접근 가능 url 수정 .requestMatchers("/관리자관련url").hasRole("admin") .anyRequest().authenticated() ) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler)) .exceptionHandling(hanling-> hanling .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)) diff --git a/src/main/java/com/example/gamemate/global/config/auth/MailConfig.java b/src/main/java/com/example/gamemate/global/config/auth/MailConfig.java new file mode 100644 index 0000000..9ea465c --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/auth/MailConfig.java @@ -0,0 +1,41 @@ +package com.example.gamemate.global.config.auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig { + @Value("${spring.mail.host}") + private String mailHost; + + @Value("${spring.mail.port}") + private int mailPort; + + @Value("${spring.mail.username}") + private String mailUsername; + + @Value("${spring.mail.password}") + private String mailPassword; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(mailHost); + mailSender.setPort(mailPort); + mailSender.setUsername(mailUsername); + mailSender.setPassword(mailPassword); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "true"); + + return mailSender; + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index a99a082..0c786a1 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -11,9 +11,16 @@ public enum ErrorCode { INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "잘못된 요청입니다."), IS_ALREADY_FOLLOWED(HttpStatus.BAD_REQUEST, "IS_ALREADY_FOLLOWED", "이미 팔로우 한 회원입니다."), - IS_WITHDRAWN_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "탈퇴한 회원입니다."), + IS_WITHDRAWN_USER(HttpStatus.BAD_REQUEST, "IS_WITHDRAW_USER", "비활성화된 회원입니다."), DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "DUPLICATE_USER", "이미 사용 중인 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "비밀번호가 일치하지 않습니다."), + REVIEW_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"REVIEW_ALREADY_EXISTS","이미 리뷰를 작성한 회원입니다."), + IS_ALREADY_PENDING(HttpStatus.BAD_REQUEST, "IS_ALREADY_PENDING", "이미 대기중인 요청이 있습니다."), + IS_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "IS_ALREADY_PROCESSED", "이미 처리된 요청입니다."), + INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST,"INVALID_PROVIDER_TYPE", "지원하지 않는 서비스 제공자입니다."), + INVALID_OAUTH2_ATTRIBUTE(HttpStatus.BAD_REQUEST, "INVALID_OAUTH2_ATTRIBUTE", "인증 정보가 유효하지 않습니다."), + VERIFICATION_TIME_EXPIRED(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 시간이 만료되었습니다."), + INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE", "인증 코드가 일치하지 않습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), @@ -27,11 +34,14 @@ public enum ErrorCode { USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "회원을 찾을 수 없습니다."), FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND,"FOLLOW_NOT_FOUND", "팔로우를 찾을 수 없습니다."), BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD_NOT_FOUND", "게시글을 찾을 수 없습니다."), + GAME_NOT_FOUND(HttpStatus.NOT_FOUND,"GAME_NOT_FOUND","게임을 찾을 수 없습니다."), + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), + MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), /* 500 서버 오류 */ - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; - + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."), + EMAIL_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "EMAIL_SEND_ERROR", "이메일 전송에 문제가 발생했습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java index bc416a4..26b3a86 100644 --- a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -21,8 +21,11 @@ public class JwtTokenProvider { @Value("${spring.jwt.secret}") private String secretKey; - private final long accessTokenExpirationMs = 1000 * 60 * 60; //60분 - private final long refreshTokenExpirationMs = 1000 * 60 * 60 * 24 * 3; //3일 + @Value("${jwt.access-token.expiration:3600000}") + private int accessTokenExpirationMs; //60분 + + @Value("${jwt.refresh-token.expiration:259200000}") + private int refreshTokenExpirationMs; //3일 public String createAccessToken(String email, Role role) { Claims claims = Jwts.claims().setSubject(email); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 711ad87..ace2608 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,4 +17,35 @@ spring.jpa.generate-ddl=false spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true -spring.jwt.secret=${JWT_SECRET} \ No newline at end of file +spring.jwt.secret=${JWT_SECRET} + +# Google OAuth2 +spring.security.oauth2.client.registration.google.client-id=${OAUTH2_GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${OAUTH2_GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.scope=email,profile + +# Kakao OAuth2 +spring.security.oauth2.client.registration.kakao.client-id=${OAUTH2_KAKAO_CLIENT_ID} +spring.security.oauth2.client.registration.kakao.client-secret=${OAUTH2_KAKAO_CLIENT_SECRET} +spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=POST + +# Kakao Provider +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id + +oauth2.redirect.uri=http://localhost:8080/oauth2/redirect + +# EMAIL +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${EMAIL_USERNAME} +spring.mail.password=${EMAIL_APP_PASSWORD} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.timeout=5000 +spring.mail.properties.mail.smtp.starttls.enable=true \ No newline at end of file From 864ae28483cb46e4fb928ecc73a90815ada5e6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 15 Jan 2025 16:35:22 +0900 Subject: [PATCH 090/215] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=95=84=EC=88=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/SignupRequestDto.java | 4 +++ .../domain/auth/service/AuthService.java | 13 ++++++-- .../domain/auth/service/EmailService.java | 31 +++++++++++++++++-- .../gamemate/global/constant/ErrorCode.java | 1 + 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java index 0205ff2..6b6c242 100644 --- a/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/auth/dto/SignupRequestDto.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -24,4 +25,7 @@ public class SignupRequestDto { // @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자 최소 1글자씩 입력해주세요.") private final String password; + @NotNull(message = "이메일 인증이 필요합니다.") + private final Boolean isEmailVerified; + } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index 4fed1d3..e3ff377 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -24,8 +24,10 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final TokenService tokenService; private final JwtTokenProvider jwtTokenProvider; + private final EmailService emailService; public SignupResponseDto signup(SignupRequestDto requestDto) { + // 기존 사용자 중복 체크 Optional findUser = userRepository.findByEmail(requestDto.getEmail()); if(findUser.isPresent()) { if(findUser.get().getUserStatus() == UserStatus.WITHDRAW) { @@ -34,11 +36,18 @@ public SignupResponseDto signup(SignupRequestDto requestDto) { throw new ApiException(ErrorCode.DUPLICATE_EMAIL); } + // 이메일 인증 여부 확인 + if (!emailService.isEmailVerified(requestDto.getEmail())) { + throw new ApiException(ErrorCode.EMAIL_NOT_VERIFIED); + } + + // 비밀번호 암호화 String rawPassword = requestDto.getPassword(); String encodedPassword = passwordEncoder.encode(rawPassword); - User user = new User(requestDto.getEmail(), requestDto.getName(), requestDto.getNickname(), encodedPassword); - User savedUser = userRepository.save(user); + // 새로운 사용자 생성 및 저장 + User newUser = new User(requestDto.getEmail(), requestDto.getName(), requestDto.getNickname(), encodedPassword); + User savedUser = userRepository.save(newUser); return new SignupResponseDto(savedUser); } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java index e884f21..d48a2cd 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java @@ -51,22 +51,44 @@ public void sendVerificationEmail(String email) { } } - public void verifyEmail(String email, String code) { + public boolean verifyEmail(String email, String code) { + // 인증 정보 확인 VerificationInfo verificationInfo = getVerificationInfo(email); + + //인증 정보가 없는 경우 if (verificationInfo == null) { throw new ApiException(ErrorCode.VERIFICATION_TIME_EXPIRED); } + // 인증 정보가 만료된 경우 if (LocalDateTime.now().isAfter(verificationInfo.getExpiryTime())) { verificationMap.remove(email); throw new ApiException(ErrorCode.VERIFICATION_TIME_EXPIRED); } + // 인증 코드 불일치 if (!verificationInfo.getCode().equals(code)) { throw new ApiException(ErrorCode.INVALID_VERIFICATION_CODE); } - verificationMap.remove(email); + // 인증 성공 시 상태 변경 + verificationInfo.markAsVerified(); + verificationMap.put(email, verificationInfo); + + // 만료된 모든 인증 정보 제거 + verificationMap.entrySet().removeIf(entry -> + LocalDateTime.now().isAfter(entry.getValue().getExpiryTime()) + ); + + return true; + } + + public boolean isEmailVerified(String email) { + VerificationInfo info = verificationMap.get(email); + + return info != null + && !LocalDateTime.now().isAfter(info.getExpiryTime()) + && info.isVerified(); } // Todo: 추후 Redis로 수정 예정 @@ -117,6 +139,11 @@ private String createEmailContent(String code) { private static class VerificationInfo { private final String code; private final LocalDateTime expiryTime; + private boolean verified = false; + + public void markAsVerified() { + this.verified = true; + } } } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 0c786a1..e84eee8 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -21,6 +21,7 @@ public enum ErrorCode { INVALID_OAUTH2_ATTRIBUTE(HttpStatus.BAD_REQUEST, "INVALID_OAUTH2_ATTRIBUTE", "인증 정보가 유효하지 않습니다."), VERIFICATION_TIME_EXPIRED(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 시간이 만료되었습니다."), INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE", "인증 코드가 일치하지 않습니다."), + EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "EMAIL_NOT_VERIFIED", "이메일 인증이 필요합니다"), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From c98e449a890cddf3b39ffb997fdc5eee3a2e2d17 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:17:48 +0900 Subject: [PATCH 091/215] =?UTF-8?q?feat:=20=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=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 1. 게임 주천 2. 게임 추천 조회( 본인 유저만) 3. 게임 선호, 추천 데이터 저장 --- .../controller/GameRecommendContorller.java | 44 +++++++ .../dto/GameRecommendHistorysResponseDto.java | 27 ++++ .../dto/GameRecommendationResponseDto.java | 17 +++ .../dto/UserGamePreferenceRequestDto.java | 19 +++ .../dto/UserGamePreferenceResponseDto.java | 31 +++++ .../game/entity/GameRecommendHistory.java | 51 ++++++++ .../game/entity/UserGamePreference.java | 51 ++++++++ .../GameRecommendHistoryRepository.java | 13 ++ .../UserGamePreferenceRepository.java | 7 ++ .../game/service/GameRecommendService.java | 118 ++++++++++++++++++ .../domain/game/service/GeminiService.java | 39 ++++++ .../aiapi/GeminiRestTemplateConfig.java | 21 ++++ .../gamemate/global/constant/ErrorCode.java | 1 + 13 files changed, 439 insertions(+) create mode 100644 src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java create mode 100644 src/main/java/com/example/gamemate/domain/game/entity/UserGamePreference.java create mode 100644 src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/game/repository/UserGamePreferenceRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java create mode 100644 src/main/java/com/example/gamemate/domain/game/service/GeminiService.java create mode 100644 src/main/java/com/example/gamemate/global/config/aiapi/GeminiRestTemplateConfig.java diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java new file mode 100644 index 0000000..4a47ed5 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java @@ -0,0 +1,44 @@ +package com.example.gamemate.domain.game.controller; + +import com.example.gamemate.domain.game.dto.GameRecommendHistorysResponseDto; +import com.example.gamemate.domain.game.dto.GameRecommendationResponseDto; +import com.example.gamemate.domain.game.dto.UserGamePreferenceRequestDto; +import com.example.gamemate.domain.game.dto.UserGamePreferenceResponseDto; +import com.example.gamemate.domain.game.service.GameRecommendService; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/games/recommendations") +@RequiredArgsConstructor +public class GameRecommendContorller { + + private final GameRecommendService gameRecommendService; + + @PostMapping + public ResponseEntity createUserGamePreference( + @RequestBody UserGamePreferenceRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + UserGamePreferenceResponseDto responseDto = gameRecommendService.createUserGamePreference(requestDto, customUserDetails.getUser()); + + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + @GetMapping("/{userId}") + public ResponseEntity> getGameRecommendHistories( + @PathVariable Long userId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + Page responseDto = gameRecommendService.getGameRecommendHistories(userId, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + +} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java new file mode 100644 index 0000000..09d0b39 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java @@ -0,0 +1,27 @@ +package com.example.gamemate.domain.game.dto; + +import com.example.gamemate.domain.game.entity.GameRecommendHistory; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class GameRecommendHistorysResponseDto { + private Long userId; + private String title; + private String description; + private Double star; + private Double matchingScore; + private String reasonForRecommendation; + + public GameRecommendHistorysResponseDto(GameRecommendHistory gameRecommendHistory) { + + this.userId = gameRecommendHistory.getUser().getId(); + this.title = gameRecommendHistory.getTitle(); + this.description = gameRecommendHistory.getDescription(); + this.star = gameRecommendHistory.getStar(); + this.matchingScore = gameRecommendHistory.getMatchingScore(); + this.reasonForRecommendation = gameRecommendHistory.getReasonForRecommendation(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java new file mode 100644 index 0000000..12e3373 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java @@ -0,0 +1,17 @@ +package com.example.gamemate.domain.game.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GameRecommendationResponseDto { + private String title; + private String description; + private Double star; + private Double matchingScore; + private String reasonForRecommendation; +} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java new file mode 100644 index 0000000..7e07b1d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java @@ -0,0 +1,19 @@ +package com.example.gamemate.domain.game.dto; + +import com.example.gamemate.domain.user.entity.User; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserGamePreferenceRequestDto { + + private User user; + private String preferredGenres; + private String playStyle; + private String playTime; + private String difficulty; + private String platform; + private String extraRequest; + +} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java new file mode 100644 index 0000000..4cf769f --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java @@ -0,0 +1,31 @@ +package com.example.gamemate.domain.game.dto; + +import com.example.gamemate.domain.game.entity.UserGamePreference; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@Getter +public class UserGamePreferenceResponseDto { + + private Long user; + private String preferredGenres; + private String playStyle; + private String playTime; + private String difficulty; + private String platform; + private List recommendations; + + public UserGamePreferenceResponseDto(UserGamePreference userGamePreference,List recommendations) { + this.user = userGamePreference.getUser().getId(); + this.preferredGenres = userGamePreference.getPreferredGenres(); + this.playStyle = userGamePreference.getPlayStyle(); + this.playTime = userGamePreference.getPlayTime(); + this.difficulty = userGamePreference.getDifficulty(); + this.platform = userGamePreference.getPlatform(); + this.recommendations =recommendations; + } + +} diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java new file mode 100644 index 0000000..eb7f24b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java @@ -0,0 +1,51 @@ +package com.example.gamemate.domain.game.entity; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "game_recommend_history") +public class GameRecommendHistory extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(length = 255) + private String title; + + @Column(length = 255) + private String description; + + private Double matchingScore; + + @Column(length = 255) + private String reasonForRecommendation; + + private Double star; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "preferences_id") + private UserGamePreference userGamePreference; + + // 새로운 생성자 추가 + public GameRecommendHistory(User user, String title, String description, Double matchingScore, String reasonForRecommendation, Double star, UserGamePreference userGamePreference) { + this.user = user; + this.title = title; + this.description = description; + this.matchingScore = matchingScore; + this.reasonForRecommendation = reasonForRecommendation; + this.star = star; + this.userGamePreference = userGamePreference; + } +} + diff --git a/src/main/java/com/example/gamemate/domain/game/entity/UserGamePreference.java b/src/main/java/com/example/gamemate/domain/game/entity/UserGamePreference.java new file mode 100644 index 0000000..11526e7 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/entity/UserGamePreference.java @@ -0,0 +1,51 @@ +package com.example.gamemate.domain.game.entity; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "user_game_preference") +public class UserGamePreference extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(length = 10) + private String preferredGenres; + + @Column(length = 255) + private String playStyle; + + @Column(length = 10) + private String playTime; + + @Column(length = 10) + private String difficulty; + + @Column(length = 20) + private String platform; + + @Column(length = 255) + private String extraRequest; + + public UserGamePreference(User user, String preferredGenres, String playStyle, String playTime, String difficulty, String platform, String extraRequest){ + this.user =user; + this.preferredGenres = preferredGenres; + this.playStyle = playStyle; + this.playTime = playTime; + this.difficulty = difficulty; + this.platform = platform; + this.extraRequest =extraRequest; + } +} + diff --git a/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java new file mode 100644 index 0000000..e6eef2e --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java @@ -0,0 +1,13 @@ +package com.example.gamemate.domain.game.repository; + +import com.example.gamemate.domain.game.dto.GameRecommendHistorysResponseDto; +import com.example.gamemate.domain.game.entity.GameRecommendHistory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GameRecommendHistoryRepository extends JpaRepository { + Page findByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/com/example/gamemate/domain/game/repository/UserGamePreferenceRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/UserGamePreferenceRepository.java new file mode 100644 index 0000000..d66a245 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/repository/UserGamePreferenceRepository.java @@ -0,0 +1,7 @@ +package com.example.gamemate.domain.game.repository; + +import com.example.gamemate.domain.game.entity.UserGamePreference; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserGamePreferenceRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java new file mode 100644 index 0000000..1bbfa63 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java @@ -0,0 +1,118 @@ +package com.example.gamemate.domain.game.service; + +import com.example.gamemate.domain.game.dto.*; +import com.example.gamemate.domain.game.entity.GameRecommendHistory; +import com.example.gamemate.domain.game.entity.UserGamePreference; +import com.example.gamemate.domain.game.repository.GameRecommendHistoryRepository; +import com.example.gamemate.domain.game.repository.UserGamePreferenceRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GameRecommendService { + private final UserGamePreferenceRepository userGamePreferenceRepository; + private final UserRepository userRepository; + private final GameRecommendHistoryRepository gameRecommendHistoryRepository; + private final GeminiService geminiService; + + @Transactional + public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreferenceRequestDto requestDto, User loginUser) { + + User user = userRepository.findById(loginUser.getId()) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + UserGamePreference userGamePreference = new UserGamePreference( + user, + requestDto.getPreferredGenres(), + requestDto.getPlayStyle(), + requestDto.getPlayTime(), + requestDto.getDifficulty(), + requestDto.getPlatform(), + requestDto.getExtraRequest() + ); + + UserGamePreference saveData = userGamePreferenceRepository.save(userGamePreference); + + // 저장된 선호도를 기반으로 Gemini API 호출 + String prompt = String.format( + "나에게 맞는 게임 3개 추천해줘 선호하는 장르는 %s이고 플레이 스타일은 %s 정도고 플레이 타임은 %s 정도고 난이도는 %s 그리고 플랫폼은 %s이고 추가적인 요청은 %s 야 " + + "응답은 제목(title), 간단한 내용(description), 평점(star), 나와의 매칭점수(matchingScore), 추천 이유(reasonForRecommendation)를 적어주고 " + + "응답은 순수 JSON 배열로 알려", + userGamePreference.getPreferredGenres(), + userGamePreference.getPlayTime(), + userGamePreference.getPlayStyle(), + userGamePreference.getDifficulty(), + userGamePreference.getPlatform(), + userGamePreference.getExtraRequest() + ); + + String recommendation = geminiService.getContents(prompt); + log.info(recommendation); + // 추천 JSON 문자열을 구조화된 객체로 파싱 + + ObjectMapper objectMapper = new ObjectMapper(); + List gameRecommendations; + try { + // 응답 문자열에서 JSON 배열 추출 + String jsonArray = recommendation + .replaceAll("(?s)^.*?```json\\s*", "") // 시작 부분의 ``` + .replaceAll("\\s*```\\s*$", "") // 끝 부분의 ``` + .trim(); + + gameRecommendations = objectMapper.readValue(jsonArray, new TypeReference>() { + }); + } catch (Exception e) { + throw new ApiException(ErrorCode.RECOMMENDATION_NOT_FOUND); + } + + // GameRecommendHistory 엔티티 생성 및 저장 + List gameRecommendHistories = new ArrayList<>(); + for (GameRecommendationResponseDto responseDto : gameRecommendations) { + GameRecommendHistory history = new GameRecommendHistory( + loginUser, + responseDto.getTitle(), + responseDto.getDescription(), + responseDto.getMatchingScore(), + responseDto.getReasonForRecommendation(), + responseDto.getStar(), + saveData + ); + gameRecommendHistories.add(history); + } + // 저장 + gameRecommendHistoryRepository.saveAll(gameRecommendHistories); + + return new UserGamePreferenceResponseDto(saveData, gameRecommendations); + } + + public Page getGameRecommendHistories(Long userId, User loginUser) { + + if (userId != loginUser.getId()){ + throw new ApiException(ErrorCode.FORBIDDEN); + } + + Pageable pageable = PageRequest.of(0, 15); + Page histories = gameRecommendHistoryRepository.findByUserId(userId, pageable); + + // return histories.map(gameRecommendHistory -> new GameRecommendHistorysResponseDto(gameRecommendHistory)); + return histories.map(GameRecommendHistorysResponseDto::new); + + } + +} diff --git a/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java new file mode 100644 index 0000000..1eb6ada --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java @@ -0,0 +1,39 @@ +package com.example.gamemate.domain.game.service; + +import com.example.gamemate.domain.game.dto.ChatRequestDto; +import com.example.gamemate.domain.game.dto.ChatResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class GeminiService { + @Qualifier("geminiRestTemplate") + + @Autowired + private RestTemplate restTemplate; + + @Value("${gemini.api.url}") + private String apiUrl; + + @Value("${gemini.api.key}") + private String geminiApiKey; + + public String getContents(String prompt) { + + // Gemini에 요청 전송 + String requestUrl = apiUrl + "?key=" + geminiApiKey; + + ChatRequestDto request = new ChatRequestDto(prompt); + ChatResponseDto response = restTemplate.postForObject(requestUrl, request, ChatResponseDto.class); + + String message = response.getCandidates().get(0).getContent().getParts().get(0).getText().toString(); + + return message; + + } +} diff --git a/src/main/java/com/example/gamemate/global/config/aiapi/GeminiRestTemplateConfig.java b/src/main/java/com/example/gamemate/global/config/aiapi/GeminiRestTemplateConfig.java new file mode 100644 index 0000000..cb9b084 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/aiapi/GeminiRestTemplateConfig.java @@ -0,0 +1,21 @@ +package com.example.gamemate.global.config.aiapi; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + + +@Configuration +@RequiredArgsConstructor +public class GeminiRestTemplateConfig { + @Bean + @Qualifier("geminiRestTemplate") + public RestTemplate geminiRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> execution.execute(request, body)); + + return restTemplate; + } +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 10f99e1..712fcfa 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -34,6 +34,7 @@ public enum ErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), + RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND,"RECOMMENDATION_NOT_FOUND","추천 게임을 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 57743eb4f645e4ce5eada7e3916bd92cb3c7cbdb Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:18:51 +0900 Subject: [PATCH 092/215] =?UTF-8?q?feat:=20=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0' implementation 'com.fasterxml.jackson.core:jackson-core:2.18.0' 추가 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index f218557..b5becf7 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation 'org.springframework.boot:spring-boot-starter' implementation 'com.fasterxml.jackson.core:jackson-databind' + + implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0' + implementation 'com.fasterxml.jackson.core:jackson-core:2.18.0' } tasks.named('test') { From bc49562f06db77695f7a725ca20d042d55e465f1 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 15 Jan 2025 20:44:57 +0900 Subject: [PATCH 093/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=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 원하는 상대방 정보를 입력하고, 원하는 상대방의 정보와 사용자들의 정보를 매칭 로직을 통해 비교해 점수를 부여하고 가장 점수가 높은 5명의 사용자를 추천해주는 기능 구현 --- .../match/controller/MatchController.java | 41 +++- ...to.java => MatchInfoCreateRequestDto.java} | 5 +- ...onseDto.java => MatchInfoResponseDto.java} | 15 +- .../match/dto/MatchSearchConditionDto.java | 61 ++++++ .../domain/match/entity/MatchDesiredInfo.java | 71 ------- .../domain/match/entity/MatchUserInfo.java | 7 + .../gamemate/domain/match/enums/GameRank.java | 1 + .../MatchDesiredInfoRepository.java | 7 - .../repository/MatchUserInfoRepository.java | 13 ++ .../domain/match/service/MatchService.java | 180 +++++++++++++++++- 10 files changed, 302 insertions(+), 99 deletions(-) rename src/main/java/com/example/gamemate/domain/match/dto/{CreateMyInfoRequestDto.java => MatchInfoCreateRequestDto.java} (94%) rename src/main/java/com/example/gamemate/domain/match/dto/{CreateMyInfoResponseDto.java => MatchInfoResponseDto.java} (79%) create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java delete mode 100644 src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java delete mode 100644 src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 445f1da..7471f2b 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,8 +1,10 @@ package com.example.gamemate.domain.match.controller; import com.example.gamemate.domain.match.dto.*; +import com.example.gamemate.domain.match.enums.*; import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.global.config.auth.CustomUserDetails; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Set; @RestController @RequiredArgsConstructor @@ -19,6 +22,7 @@ public class MatchController { /** * 매칭 요청 생성 + * * @param dto MatchCreateRequestDto 상대 유저 id, 상대방에게 전할 메세지 * @return matchCreateResponseDto */ @@ -34,7 +38,8 @@ public ResponseEntity createMatch( /** * 매칭 수락/거절하기 - * @param id 매칭 id + * + * @param id 매칭 id * @param dto MatchUpdateRequestDto 수락/거절 * @return 204 NO CONTENT */ @@ -51,6 +56,7 @@ public ResponseEntity updateMatch( /** * 받은 매칭 전체 조회 + * * @return matchFindResponseDtoList */ @GetMapping("/received-matches") @@ -64,6 +70,7 @@ public ResponseEntity> findAllReceivedMatch( /** * 보낸 매칭 전체 조회 + * * @return matchFindResponseDtoList */ @GetMapping("/sent-matches") @@ -77,6 +84,7 @@ public ResponseEntity> findAllSentMatch( /** * 매칭 삭제(취소) + * * @param id 매칭 id * @return NO_CONTENT */ @@ -91,17 +99,32 @@ public ResponseEntity deleteMatch( } /** - * 내 매칭 정보 입력 - * @param dto CreateMyInfoRequestDto - * @return createMyInfoRequestDto + * 매칭을 위해 내 정보 입력하기 + * @param dto MatchInfoCreateRequestDto + * @return matchInfoResponseDto + */ + @PostMapping("/info") + public ResponseEntity createMyInfo( + @RequestBody MatchInfoCreateRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + MatchInfoResponseDto matchInfoResponseDto = matchService.createMyInfo(dto, customUserDetails.getUser()); + return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.CREATED); + } + + /** + * 매칭 추천 받기 + * @param dto MatchSearchConditionDto 매칭 조건 설정 + * @return recommendationList 매칭 로직을 통해 가장 점수가 높은 최대 5명 리스트 */ - @PostMapping("/my-info") - public ResponseEntity createMyInfo( - CreateMyInfoRequestDto dto, + @PostMapping("/recommendations") + public ResponseEntity> findRecommendation( + @Valid @RequestBody MatchSearchConditionDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - CreateMyInfoResponseDto createMyInfoResponseDto = matchService.createMyInfo(dto, customUserDetails.getUser()); - return new ResponseEntity<>(createMyInfoResponseDto, HttpStatus.CREATED); + List recommendationList = matchService.findRecommendation(dto, customUserDetails.getUser()); + return new ResponseEntity<>(recommendationList, HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java similarity index 94% rename from src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java rename to src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java index 8310bfd..c0b4548 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java @@ -1,6 +1,5 @@ package com.example.gamemate.domain.match.dto; -import com.example.gamemate.domain.match.entity.MatchUserInfo; import com.example.gamemate.domain.match.enums.*; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -11,7 +10,7 @@ import java.util.Set; @Getter -public class CreateMyInfoRequestDto { +public class MatchInfoCreateRequestDto { @NotNull(message = "성별은 필수 입력값입니다.") private Gender gender; @@ -41,7 +40,7 @@ public class CreateMyInfoRequestDto { @Size(max = 200, message = "메시지는 200자를 초과할 수 없습니다.") private String message; - public CreateMyInfoRequestDto( + public MatchInfoCreateRequestDto( Gender gender, Set lanes, Set purposes, diff --git a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java similarity index 79% rename from src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java rename to src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java index 3e31ccf..0e67518 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/CreateMyInfoResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java @@ -7,8 +7,9 @@ import java.util.Set; @Getter -public class CreateMyInfoResponseDto { +public class MatchInfoResponseDto { private Long id; + private String nickname; private Gender gender; private Set lanes; private Set purposes; @@ -18,8 +19,10 @@ public class CreateMyInfoResponseDto { private Boolean micUsage; private String message; - public CreateMyInfoResponseDto( - Long id, Gender gender, + public MatchInfoResponseDto( + Long id, + String nickname, + Gender gender, Set lanes, Set purposes, GameRank gameRank, @@ -29,6 +32,7 @@ public CreateMyInfoResponseDto( String message ) { this.id = id; + this.nickname = nickname; this.gender = gender; this.lanes = lanes; this.purposes = purposes; @@ -39,9 +43,10 @@ public CreateMyInfoResponseDto( this.message = message; } - public static CreateMyInfoResponseDto toDto(MatchUserInfo matchUserInfo) { - return new CreateMyInfoResponseDto( + public static MatchInfoResponseDto toDto(MatchUserInfo matchUserInfo) { + return new MatchInfoResponseDto( matchUserInfo.getId(), + matchUserInfo.getUser().getNickname(), matchUserInfo.getGender(), matchUserInfo.getLanes(), matchUserInfo.getPurposes(), diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java new file mode 100644 index 0000000..058de4d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java @@ -0,0 +1,61 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.enums.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +import java.util.Set; + +@Getter +public class MatchSearchConditionDto { + @NotNull(message = "성별은 필수 입력값입니다.") + private Gender gender; + + @NotNull(message = "라인은 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "라인은 1-2개 선택 가능합니다.") + private Set lanes; + + @NotNull(message = "목적은 필수 입력값입니다.") + @Size(min = 1, max = 3, message = "목적은 1-3개 선택 가능합니다.") + private Set purposes; + + @NotNull(message = "게임 랭크는 필수 입력값입니다.") + private GameRank gameRank; + + @NotNull(message = "플레이 시간대는 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "플레이 시간대는 1-2개 선택 가능합니다.") + private Set playTimeRanges; + + @NotNull(message = "스킬 레벨은 필수 입력값입니다.") + @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") + @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + private Integer skillLevel; + + @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") + private Boolean micUsage; + + private String priority; + + public MatchSearchConditionDto( + Gender gender, + Set lanes, + Set purposes, + GameRank gameRank, + Set playTimeRanges, + Integer skillLevel, + Boolean micUsage, + String priority + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.gameRank = gameRank; + this.playTimeRanges = playTimeRanges; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.priority = priority; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java b/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java deleted file mode 100644 index ac07d41..0000000 --- a/src/main/java/com/example/gamemate/domain/match/entity/MatchDesiredInfo.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.gamemate.domain.match.entity; - -import com.example.gamemate.domain.match.enums.*; -import com.example.gamemate.domain.user.entity.User; -import jakarta.persistence.*; -import lombok.Getter; - -import java.util.HashSet; -import java.util.Set; - -@Getter -@Entity -public class MatchDesiredInfo { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Enumerated(EnumType.STRING) - private Gender gender; - - @ElementCollection - @CollectionTable(name = "desired_lanes") - @Enumerated(EnumType.STRING) - private Set lanes = new HashSet<>(); - - @ElementCollection - @CollectionTable(name = "desired_purposes") - @Enumerated(EnumType.STRING) - private Set purposes = new HashSet<>(); - - @ElementCollection - @CollectionTable(name = "desired_play_times") - @Enumerated(EnumType.STRING) - private Set playTimeRanges = new HashSet<>(); - - @Enumerated(EnumType.STRING) - private GameRank gameRank; - - @Column - private Integer skillLevel; - - @Column - private Boolean micUsage; - - @OneToOne - @JoinColumn(name = "user_id") - private User user; - - public MatchDesiredInfo() { - } - - public MatchDesiredInfo( - Gender gender, - Set lanes, - Set purposes, - Set playTimeRanges, - GameRank gameRank, - Integer skillLevel, - Boolean micUsage, - User user - ) { - this.gender = gender; - this.lanes = lanes; - this.purposes = purposes; - this.playTimeRanges = playTimeRanges; - this.gameRank = gameRank; - this.skillLevel = skillLevel; - this.micUsage = micUsage; - this.user = user; - } -} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java index e0da7ef..ac75509 100644 --- a/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java +++ b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java @@ -49,6 +49,9 @@ public class MatchUserInfo { @JoinColumn(name = "user_id") private User user; + @Transient // DB에 저장하지 않고 런타임에만 사용 + private int matchScore; + public MatchUserInfo() { } @@ -73,4 +76,8 @@ public MatchUserInfo( this.message = message; this.user = user; } + + public void updateMatchScore(int matchScore) { + this.matchScore = matchScore; + } } diff --git a/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java b/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java index b488553..7739742 100644 --- a/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java +++ b/src/main/java/com/example/gamemate/domain/match/enums/GameRank.java @@ -4,6 +4,7 @@ @Getter public enum GameRank { + DONT_MIND("dont_mind", "상관없음"), IRON("iron", "아이언"), BRONZE("bronze", "브론즈"), SILVER("silver", "실버"), diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java deleted file mode 100644 index 71406c6..0000000 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchDesiredInfoRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.gamemate.domain.match.repository; - -import com.example.gamemate.domain.match.entity.MatchDesiredInfo; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface MatchDesiredInfoRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java index 4467798..0027cc0 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java @@ -1,7 +1,20 @@ package com.example.gamemate.domain.match.repository; import com.example.gamemate.domain.match.entity.MatchUserInfo; +import com.example.gamemate.domain.match.enums.Gender; +import com.example.gamemate.domain.match.enums.PlayTimeRange; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Set; public interface MatchUserInfoRepository extends JpaRepository { + + @Query("SELECT m FROM MatchUserInfo m WHERE m.gender = :gender AND EXISTS (SELECT pt FROM m.playTimeRanges pt WHERE pt IN :playTimeRanges) AND m.user.id <> :userId") + List findByGenderAndPlayTimeRanges(@Param("gender") Gender gender, + @Param("playTimeRanges") Set playTimeRanges, + @Param("userId") Long userId); + } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index b50b4b3..967e282 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -3,8 +3,8 @@ import com.example.gamemate.domain.match.dto.*; import com.example.gamemate.domain.match.entity.Match; import com.example.gamemate.domain.match.entity.MatchUserInfo; +import com.example.gamemate.domain.match.enums.GameRank; import com.example.gamemate.domain.match.enums.MatchStatus; -import com.example.gamemate.domain.match.repository.MatchDesiredInfoRepository; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; import com.example.gamemate.domain.user.entity.User; @@ -14,19 +14,23 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Slf4j public class MatchService { private final UserRepository userRepository; private final MatchRepository matchRepository; private final MatchUserInfoRepository matchUserInfoRepository; - private final MatchDesiredInfoRepository matchDesiredInfoRepository; // 매칭 요청 생성 @Transactional @@ -105,7 +109,7 @@ public void deleteMatch(Long id, User loginUser) { // 내 정보 입력 @Transactional - public CreateMyInfoResponseDto createMyInfo(CreateMyInfoRequestDto dto, User loginUser) { + public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User loginUser) { MatchUserInfo matchUserInfo = new MatchUserInfo( dto.getGender(), @@ -121,6 +125,174 @@ public CreateMyInfoResponseDto createMyInfo(CreateMyInfoRequestDto dto, User log matchUserInfoRepository.save(matchUserInfo); - return CreateMyInfoResponseDto.toDto(matchUserInfo); + + log.info("라인 : {}, 목적 : {}, 플레이 시간대 : {}", matchUserInfo.getLanes(), matchUserInfo.getPurposes(), matchUserInfo.getPlayTimeRanges()); + return MatchInfoResponseDto.toDto(matchUserInfo); + } + + // 매칭 로직 + public List findRecommendation(MatchSearchConditionDto dto, User loginUser) { + // 1. 성별과 플레이 시간대를 기준으로 필터링된 사용자 정보 조회 + List filteredUsers = matchUserInfoRepository.findByGenderAndPlayTimeRanges( + dto.getGender(), + dto.getPlayTimeRanges(), + loginUser.getId() + ); + + // 2. 매칭 점수 계산 및 저장 + for (MatchUserInfo matchUserInfo : filteredUsers) { + int score = calculateMatchScore(dto, matchUserInfo); + matchUserInfo.updateMatchScore(score); + } + + // 3. 매칭 점수 내림차순으로 정렬 + filteredUsers.sort((u1, u2) -> Integer.compare(u2.getMatchScore(), u1.getMatchScore())); + + + // 4. 동점자 처리 (동점자끼리 랜덤 섞기) + List resultList = handleTies(filteredUsers); + + // 5. 상위 5명 추출 및 DTO 변환 + return resultList.stream() + .limit(5) + .map(MatchInfoResponseDto::toDto) + .collect(Collectors.toList()); + } + + + // 점수 계산 로직 + private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo userInfo) { + int score = 0; + int normalScorePerMatch = 5; // 매칭되는 항목당 점수 + int priorityWeight = 2; // 우선순위 가중치 + + String priority = condition.getPriority(); + + // 우선순위 항목 점수 계산 및 가중치 적용 + if (priority != null) { + switch (priority) { + case "lanes": + int matchedLanes = (int) condition.getLanes().stream() + .filter(userInfo.getLanes()::contains) + .count(); + score += matchedLanes * normalScorePerMatch * priorityWeight; + break; + case "purposes": + int matchedPurposes = (int) condition.getPurposes().stream() + .filter(userInfo.getPurposes()::contains) + .count(); + score += matchedPurposes * normalScorePerMatch * priorityWeight; + break; + case "playTimeRanges": + int matchedPlayTimeRanges = (int) condition.getPlayTimeRanges().stream() + .filter(userInfo.getPlayTimeRanges()::contains) + .count(); + score += matchedPlayTimeRanges * normalScorePerMatch * priorityWeight; + break; + case "gameRank": + if (condition.getGameRank().equals(userInfo.getGameRank())) { + score += normalScorePerMatch * priorityWeight * 2; + } else if (isRankSimilar(condition.getGameRank(), userInfo.getGameRank())) { + score += normalScorePerMatch * priorityWeight; + } + break; + case "skillLevel": + int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); + score += (normalScorePerMatch * 2 - skillLevelDifference) * priorityWeight; + break; + case "micUsage": + if (condition.getMicUsage().equals(userInfo.getMicUsage())) { + score += normalScorePerMatch * priorityWeight * 2; + } + break; + } + } + + // 우선순위가 아닌 조건의 점수 계산 방식 + if (priority == null || !priority.equals("lanes")) { + int matchedLanes = (int) condition.getLanes().stream() + .filter(userInfo.getLanes()::contains) + .count(); + score += matchedLanes * normalScorePerMatch; + } + + if (priority == null || !priority.equals("purposes")) { + int matchedPurposes = (int) condition.getPurposes().stream() + .filter(userInfo.getPurposes()::contains) + .count(); + score += matchedPurposes * normalScorePerMatch; + } + + if (priority == null || !priority.equals("playTimeRanges")) { + int matchedPlayTimeRanges = (int) condition.getPlayTimeRanges().stream() + .filter(userInfo.getPlayTimeRanges()::contains) + .count(); + score += matchedPlayTimeRanges * normalScorePerMatch; + } + + if (priority == null || !priority.equals("gameRank")) { + if (condition.getGameRank().equals(userInfo.getGameRank())) { + score += normalScorePerMatch * 2; + } else if (isRankSimilar(condition.getGameRank(), userInfo.getGameRank())) { + score += normalScorePerMatch; + } + } + + if (priority == null || !priority.equals("skillLevel")){ + int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); + score += (normalScorePerMatch * 2 - skillLevelDifference); + } + + if(priority == null || !priority.equals("micUsage")){ + if (condition.getMicUsage().equals(userInfo.getMicUsage())) { + score += normalScorePerMatch * 2; + } + } + + return score; + } + + + // 랭크가 비슷한지 판단하는 로직 추가 + private boolean isRankSimilar(GameRank conditionRank, GameRank userRank) { + if (conditionRank == GameRank.DONT_MIND) { + return true; // "상관없음"은 모든 랭크와 유사하다고 판단 + } + + int conditionRankIndex = conditionRank.ordinal(); + int userRankIndex = userRank.ordinal(); + return Math.abs(conditionRankIndex - userRankIndex) <= 1; // 랭크 차이가 1 이하면 유사하다고 판단 + } + + + // 동점자는 랜덤으로 섞어서 출력 + private List handleTies(List sortedUsers) { + if (sortedUsers.isEmpty()) { + return sortedUsers; + } + List resultList = new ArrayList<>(); + List tieGroup = new ArrayList<>(); // 동점자 그룹 임시 저장 + + tieGroup.add(sortedUsers.get(0)); + + for (int i = 1; i < sortedUsers.size(); i++) { + MatchUserInfo currentUser = sortedUsers.get(i); + MatchUserInfo previousUser = sortedUsers.get(i - 1); + + if (currentUser.getMatchScore() == previousUser.getMatchScore()) { + tieGroup.add(currentUser); + } else { + Collections.shuffle(tieGroup); // 동점자 그룹 섞기 + resultList.addAll(tieGroup); + tieGroup.clear(); // 다음 그룹을 위해 비우기 + tieGroup.add(currentUser); //새로운 그룹 시작 + } + } + + Collections.shuffle(tieGroup); + resultList.addAll(tieGroup); //마지막 그룹 추가 + + return resultList; } } + From d248e7674852ba37be159a664a3865f89dff9444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 15 Jan 2025 21:42:43 +0900 Subject: [PATCH 094/215] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/auth/controller/AuthController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index e0c4b7b..4a5afb3 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -6,7 +6,6 @@ import com.example.gamemate.domain.auth.service.OAuth2Service; import com.example.gamemate.domain.auth.service.TokenService; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -33,7 +32,7 @@ public ResponseEntity signup( return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } - @PostMapping("/email/verification-request") + @PostMapping("/email") public ResponseEntity sendVerificationEmail( @Valid @RequestBody EmailVerificationCodeRequestDto requestDto ) { From 7c5a5c8e4d8e7df04cb2ceff7a93a97282d25f93 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Thu, 16 Jan 2025 10:38:05 +0900 Subject: [PATCH 095/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=EA=B3=BC=20=EB=8C=93=EA=B8=80/=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20API=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게시글 조회와 댓글/대댓글 조회 분리 --- .../board/controller/BoardController.java | 6 +- .../board/dto/BoardFindOneResponseDto.java | 4 +- .../gamemate/domain/board/enums/ListSize.java | 4 +- .../domain/board/service/BoardService.java | 44 +------------ .../comment/controller/CommentController.java | 18 ++++++ .../comment/service/CommentService.java | 64 +++++++++++++++++++ 6 files changed, 90 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index ca0df50..65042a7 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -12,9 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -67,17 +65,15 @@ public ResponseEntity> findAllBoards( /** * 게시글 단건 조회 API - * @param page * @param id * @return */ @GetMapping("/{id}") public ResponseEntity findBoardById( - @RequestParam(required = false, defaultValue = "0") int page, @PathVariable Long id ){ - BoardFindOneResponseDto dto = boardService.findBoardById(page,id); + BoardFindOneResponseDto dto = boardService.findBoardById(id); return new ResponseEntity<>(dto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java index 02afae6..66fcefb 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java @@ -17,9 +17,8 @@ public class BoardFindOneResponseDto { private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; - private final List comments; - public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime modifiedAt, List comments) { + public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime modifiedAt) { this.id = id; this.category = category; this.title = title; @@ -27,6 +26,5 @@ public BoardFindOneResponseDto(Long id, BoardCategory category, String title, St this.nickname = nickname; this.createdAt = createdAt; this.modifiedAt = modifiedAt; - this.comments = comments; } } diff --git a/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java b/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java index 791fb63..000f1e2 100644 --- a/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java +++ b/src/main/java/com/example/gamemate/domain/board/enums/ListSize.java @@ -4,7 +4,9 @@ @Getter public enum ListSize { - LIST_SIZE(15); + BOARD_LIST_SIZE(15), + COMMENT_LIST_SIZE(25),; + private final int size; ListSize(int size) { diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 560b28c..ccdfc7b 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -68,7 +68,7 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { */ public List findAllBoards(int page, BoardCategory category, String title, String content) { - Pageable pageable = PageRequest.of(page, ListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.desc("createdAt"))); + Pageable pageable = PageRequest.of(page, ListSize.BOARD_LIST_SIZE.getSize(), Sort.by(Sort.Order.desc("createdAt"))); Page boardPage = boardRepository.searchBoardQuerydsl(category, title, content, pageable); @@ -86,24 +86,14 @@ public List findAllBoards(int page, BoardCategory categ /** * 게시글 단건 조회 메서드 - * @param page * @param id * @return */ - public BoardFindOneResponseDto findBoardById(int page, Long id) { - // page는 댓글 페이지네이션을 위해 필요 - Pageable pageable = PageRequest.of(page, ListSize.LIST_SIZE.getSize(), Sort.by(Sort.Order.asc("createdAt"))); + public BoardFindOneResponseDto findBoardById(Long id) { // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); - // 댓글 조회 - Page comments = commentRepository.findByBoard(findBoard,pageable); - - List commentDtos = comments.stream() - .map(this::convertCommentDto) - .collect(Collectors.toList()); - return new BoardFindOneResponseDto( findBoard.getBoardId(), findBoard.getCategory(), @@ -111,8 +101,7 @@ public BoardFindOneResponseDto findBoardById(int page, Long id) { findBoard.getContent(), findBoard.getUser().getNickname(), findBoard.getCreatedAt(), - findBoard.getModifiedAt(), - commentDtos + findBoard.getModifiedAt() ); } @@ -154,31 +143,4 @@ public void deleteBoard(User loginUser, Long id) { boardRepository.delete(findBoard); } - - private CommentFindResponseDto convertCommentDto(Comment comment) { - List replyDtos = Optional.ofNullable(replyRepository.findByComment(comment)) - .orElse(Collections.emptyList()) - .stream() - .map(this::convertReplyDto) - .collect(Collectors.toList()); - return new CommentFindResponseDto( - comment.getCommentId(), - comment.getContent(), - comment.getUser().getNickname(), - comment.getCreatedAt(), - comment.getModifiedAt(), - replyDtos - ); - } - - private ReplyFindResponseDto convertReplyDto(Reply reply) { - String findUserName = reply.getParentReply() == null ? null : reply.getParentReply().getUser().getNickname(); - return new ReplyFindResponseDto( - reply.getReplyId(), - findUserName, - reply.getContent(), - reply.getCreatedAt(), - reply.getModifiedAt() - ); - } } diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index 027074f..e461219 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.comment.controller; +import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; import com.example.gamemate.domain.comment.dto.CommentRequestDto; import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.service.CommentService; @@ -10,6 +11,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RequiredArgsConstructor @RestController @RequestMapping("/boards/{boardId}/comments") @@ -33,6 +36,21 @@ public ResponseEntity createComment( return new ResponseEntity<>(dto, HttpStatus.CREATED); } + /** + * 댓글/대댓글 조회 + * @param boardId + * @param page + * @return + */ + @GetMapping + public ResponseEntity> getComments( + @PathVariable Long boardId, + @RequestParam(defaultValue = "0") int page + ){ + List dtos = commentService.getComments(boardId, page); + return new ResponseEntity<>(dtos, HttpStatus.OK); + } + /** * 댓글 수정 API * @param id diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index dc2b125..b75146c 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -1,23 +1,38 @@ package com.example.gamemate.domain.comment.service; import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.enums.ListSize; import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; import com.example.gamemate.domain.comment.dto.CommentRequestDto; import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; +import com.example.gamemate.domain.reply.entity.Reply; +import com.example.gamemate.domain.reply.repository.ReplyRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class CommentService { private final CommentRepository commentRepository; + private final ReplyRepository replyRepository; private final BoardRepository boardRepository; /** @@ -81,4 +96,53 @@ public void deleteComment(User loginUser, Long id) { commentRepository.delete(findComment); } + + /** + * 댓글 조회 메서드 + * @param boardId + * @param page + * @return + */ + public List getComments(Long boardId, int page) { + // page는 댓글 페이지네이션을 위해 필요 + Pageable pageable = PageRequest.of(page, ListSize.COMMENT_LIST_SIZE.getSize(), Sort.by(Sort.Order.asc("createdAt"))); + + //게시글 조회 + Board findBoard = boardRepository.findById(boardId) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); + + // 댓글 조회 + Page comments = commentRepository.findByBoard(findBoard,pageable); + + return comments.stream() + .map(this::convertCommentDto) + .collect(Collectors.toList()); + } + + private CommentFindResponseDto convertCommentDto(Comment comment) { + List replyDtos = Optional.ofNullable(replyRepository.findByComment(comment)) + .orElse(Collections.emptyList()) + .stream() + .map(this::convertReplyDto) + .collect(Collectors.toList()); + return new CommentFindResponseDto( + comment.getCommentId(), + comment.getContent(), + comment.getUser().getNickname(), + comment.getCreatedAt(), + comment.getModifiedAt(), + replyDtos + ); + } + + private ReplyFindResponseDto convertReplyDto(Reply reply) { + String findUserName = reply.getParentReply() == null ? null : reply.getParentReply().getUser().getNickname(); + return new ReplyFindResponseDto( + reply.getReplyId(), + findUserName, + reply.getContent(), + reply.getCreatedAt(), + reply.getModifiedAt() + ); + } } From 3a5eeee6a78e2b1b5cdd89dff30602c2f433c35c Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 10:54:54 +0900 Subject: [PATCH 096/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 내 정보 삭제, 내 정보 삭제시 더이상 다른 사람의 매칭 추천에서 검색되지 않음 --- .../match/controller/MatchController.java | 17 ++++++++++++++--- .../repository/MatchUserInfoRepository.java | 3 +++ .../domain/match/service/MatchService.java | 14 ++++++++++---- .../gamemate/global/constant/ErrorCode.java | 1 + 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 7471f2b..3de5411 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -1,7 +1,6 @@ package com.example.gamemate.domain.match.controller; import com.example.gamemate.domain.match.dto.*; -import com.example.gamemate.domain.match.enums.*; import com.example.gamemate.domain.match.service.MatchService; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; @@ -12,7 +11,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Set; @RestController @RequiredArgsConstructor @@ -99,7 +97,7 @@ public ResponseEntity deleteMatch( } /** - * 매칭을 위해 내 정보 입력하기 + * 매칭을 위해 내 정보 입력하기, 매칭 정보 입력시 매칭추천에서 검색됨 * @param dto MatchInfoCreateRequestDto * @return matchInfoResponseDto */ @@ -113,6 +111,19 @@ public ResponseEntity createMyInfo( return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.CREATED); } + /** + * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않음 + * @return 204 NO_CONTENT + */ + @DeleteMapping("/info") + public ResponseEntity deleteMyInfo( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + matchService.deleteMyInfo(customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + /** * 매칭 추천 받기 * @param dto MatchSearchConditionDto 매칭 조건 설정 diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java index 0027cc0..6099ddf 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java @@ -3,14 +3,17 @@ import com.example.gamemate.domain.match.entity.MatchUserInfo; import com.example.gamemate.domain.match.enums.Gender; import com.example.gamemate.domain.match.enums.PlayTimeRange; +import com.example.gamemate.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.List; +import java.util.Optional; import java.util.Set; public interface MatchUserInfoRepository extends JpaRepository { + Optional findByUser(User user); @Query("SELECT m FROM MatchUserInfo m WHERE m.gender = :gender AND EXISTS (SELECT pt FROM m.playTimeRanges pt WHERE pt IN :playTimeRanges) AND m.user.id <> :userId") List findByGenderAndPlayTimeRanges(@Param("gender") Gender gender, diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 967e282..5cd4900 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -25,7 +25,6 @@ @Service @RequiredArgsConstructor -@Slf4j public class MatchService { private final UserRepository userRepository; @@ -107,7 +106,7 @@ public void deleteMatch(Long id, User loginUser) { matchRepository.delete(findMatch); } - // 내 정보 입력 + // 내 정보 입력, 정보 입력시 매칭 추천에서 검색됨 @Transactional public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User loginUser) { @@ -125,11 +124,18 @@ public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User log matchUserInfoRepository.save(matchUserInfo); - - log.info("라인 : {}, 목적 : {}, 플레이 시간대 : {}", matchUserInfo.getLanes(), matchUserInfo.getPurposes(), matchUserInfo.getPlayTimeRanges()); return MatchInfoResponseDto.toDto(matchUserInfo); } + // 내 정보 삭제, 내정보 삭제시 매칭 추천에서 더이상 검색되지 않음 + @Transactional + public void deleteMyInfo(User loginUser) { + MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + + matchUserInfoRepository.delete(matchUserInfo); + } + // 매칭 로직 public List findRecommendation(MatchSearchConditionDto dto, User loginUser) { // 1. 성별과 플레이 시간대를 기준으로 필터링된 사용자 정보 조회 diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 939dc43..82b4ebe 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -33,6 +33,7 @@ public enum ErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), + MATCH_USER_INFO_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭 정보를 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 0b24fd9dbe180d444d585b460bd3cb99dbc2b152 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 11:02:13 +0900 Subject: [PATCH 097/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/match/controller/MatchController.java | 9 +++++++++ .../gamemate/domain/match/service/MatchService.java | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 3de5411..2154eba 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -111,6 +111,15 @@ public ResponseEntity createMyInfo( return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.CREATED); } + @GetMapping("/info") + public ResponseEntity findMyInfo( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + MatchInfoResponseDto matchInfoResponseDto = matchService.findMyInfo(customUserDetails.getUser()); + return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.OK); + } + /** * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않음 * @return 204 NO_CONTENT diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 5cd4900..0343d0a 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -127,6 +127,14 @@ public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User log return MatchInfoResponseDto.toDto(matchUserInfo); } + // 내 정보 조회 + public MatchInfoResponseDto findMyInfo(User loginUser) { + MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + + return MatchInfoResponseDto.toDto(matchUserInfo); + } + // 내 정보 삭제, 내정보 삭제시 매칭 추천에서 더이상 검색되지 않음 @Transactional public void deleteMyInfo(User loginUser) { From c6ce467bd1a948aa1a91c1b46b48a884964eee12 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:17:08 +0900 Subject: [PATCH 098/215] =?UTF-8?q?fix:=20=20S3=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 리턴값 변경 --- src/main/java/com/example/gamemate/global/s3/S3Service.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/global/s3/S3Service.java b/src/main/java/com/example/gamemate/global/s3/S3Service.java index 4f72e9e..f00a4f0 100644 --- a/src/main/java/com/example/gamemate/global/s3/S3Service.java +++ b/src/main/java/com/example/gamemate/global/s3/S3Service.java @@ -31,7 +31,7 @@ public String uploadFile(MultipartFile file) throws IOException { amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata)); - return amazonS3Client.getUrl(bucket, fileName).toString(); + return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, amazonS3Client.getRegionName() ,fileName); } private String createFileName(String originalFileName) { From dfe10035eda1b55d0eb28513e3e213c002db0047 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 12:53:58 +0900 Subject: [PATCH 099/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 41 ++++++++---- .../match/dto/MatchInfoUpdateRequestDto.java | 62 +++++++++++++++++++ .../domain/match/entity/MatchUserInfo.java | 23 ++++++- .../domain/match/service/MatchService.java | 31 ++++++++-- 4 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 2154eba..5dc470f 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -20,7 +20,6 @@ public class MatchController { /** * 매칭 요청 생성 - * * @param dto MatchCreateRequestDto 상대 유저 id, 상대방에게 전할 메세지 * @return matchCreateResponseDto */ @@ -30,13 +29,12 @@ public ResponseEntity createMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - MatchResponseDto matchCreateResponseDto = matchService.createMatch(dto, customUserDetails.getUser()); - return new ResponseEntity<>(matchCreateResponseDto, HttpStatus.CREATED); + MatchResponseDto matchResponseDto = matchService.createMatch(dto, customUserDetails.getUser()); + return new ResponseEntity<>(matchResponseDto, HttpStatus.CREATED); } /** * 매칭 수락/거절하기 - * * @param id 매칭 id * @param dto MatchUpdateRequestDto 수락/거절 * @return 204 NO CONTENT @@ -54,35 +52,32 @@ public ResponseEntity updateMatch( /** * 받은 매칭 전체 조회 - * - * @return matchFindResponseDtoList + * @return MatchResponseDtoList */ @GetMapping("/received-matches") public ResponseEntity> findAllReceivedMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List matchResponseDtoList = matchService.findAllReceivedMatch(customUserDetails.getUser()); - return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); + List MatchResponseDtoList = matchService.findAllReceivedMatch(customUserDetails.getUser()); + return new ResponseEntity<>(MatchResponseDtoList, HttpStatus.OK); } /** * 보낸 매칭 전체 조회 - * - * @return matchFindResponseDtoList + * @return MatchResponseDtoList */ @GetMapping("/sent-matches") public ResponseEntity> findAllSentMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List matchResponseDtoList = matchService.findAllSentMatch(customUserDetails.getUser()); - return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); + List MatchResponseDtoList = matchService.findAllSentMatch(customUserDetails.getUser()); + return new ResponseEntity<>(MatchResponseDtoList, HttpStatus.OK); } /** * 매칭 삭제(취소) - * * @param id 매칭 id * @return NO_CONTENT */ @@ -111,6 +106,10 @@ public ResponseEntity createMyInfo( return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.CREATED); } + /** + * 매칭 내정보 조회하기 + * @return MatchInfoResponseDto + */ @GetMapping("/info") public ResponseEntity findMyInfo( @AuthenticationPrincipal CustomUserDetails customUserDetails @@ -120,6 +119,22 @@ public ResponseEntity findMyInfo( return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.OK); } + /** + * 매칭 내정보 수정하기 + * @param dto MatchInfoUpdateRequestDto + * @return 204 NO_CONTENT + */ + @PutMapping("/info") + public ResponseEntity updateMyInfo( + @Valid @RequestBody MatchInfoUpdateRequestDto dto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + matchService.updateMyInfo(dto, customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + /** * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않음 * @return 204 NO_CONTENT diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java new file mode 100644 index 0000000..d5db68a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java @@ -0,0 +1,62 @@ +package com.example.gamemate.domain.match.dto; + +import com.example.gamemate.domain.match.enums.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +import java.util.Set; + +@Getter +public class MatchInfoUpdateRequestDto { + @NotNull(message = "성별은 필수 입력값입니다.") + private Gender gender; + + @NotNull(message = "라인은 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "라인은 1-2개 선택 가능합니다.") + private Set lanes; + + @NotNull(message = "목적은 필수 입력값입니다.") + @Size(min = 1, max = 3, message = "목적은 1-3개 선택 가능합니다.") + private Set purposes; + + @NotNull(message = "게임 랭크는 필수 입력값입니다.") + private GameRank gameRank; + + @NotNull(message = "플레이 시간대는 필수 입력값입니다.") + @Size(min = 1, max = 2, message = "플레이 시간대는 1-2개 선택 가능합니다.") + private Set playTimeRanges; + + @NotNull(message = "스킬 레벨은 필수 입력값입니다.") + @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") + @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + private Integer skillLevel; + + @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") + private Boolean micUsage; + + @Size(max = 200, message = "메시지는 200자를 초과할 수 없습니다.") + private String message; + + public MatchInfoUpdateRequestDto( + Gender gender, + Set lanes, + Set purposes, + GameRank gameRank, + Set playTimeRanges, + Integer skillLevel, + Boolean micUsage, + String message + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.gameRank = gameRank; + this.playTimeRanges = playTimeRanges; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.message = message; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java index ac75509..7ab5ce1 100644 --- a/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java +++ b/src/main/java/com/example/gamemate/domain/match/entity/MatchUserInfo.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.match.enums.*; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; @@ -10,7 +11,7 @@ @Getter @Entity -public class MatchUserInfo { +public class MatchUserInfo extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -77,6 +78,26 @@ public MatchUserInfo( this.user = user; } + public void updateMatchUserInfo( + Gender gender, + Set lanes, + Set purposes, + Set playTimeRanges, + GameRank gameRank, + Integer skillLevel, + Boolean micUsage, + String message + ) { + this.gender = gender; + this.lanes = lanes; + this.purposes = purposes; + this.playTimeRanges = playTimeRanges; + this.gameRank = gameRank; + this.skillLevel = skillLevel; + this.micUsage = micUsage; + this.message = message; + } + public void updateMatchScore(int matchScore) { this.matchScore = matchScore; } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 0343d0a..72a879b 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -14,7 +14,6 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -38,6 +37,12 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { User receiver = userRepository.findById(dto.getUserId()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + MatchUserInfo loginUserInfo = matchUserInfoRepository.findByUser(loginUser) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + + MatchUserInfo receiverInfo = matchUserInfoRepository.findByUser(receiver) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + if (receiver.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } @@ -75,8 +80,7 @@ public List findAllReceivedMatch(User loginUser) { List matchList = matchRepository.findAllByReceiverId(loginUser.getId()); - return matchList - .stream() + return matchList.stream() .map(MatchResponseDto::toDto) .toList(); } @@ -86,8 +90,7 @@ public List findAllSentMatch(User loginUser) { List matchList = matchRepository.findAllBySenderId(loginUser.getId()); - return matchList - .stream() + return matchList.stream() .map(MatchResponseDto::toDto) .toList(); } @@ -135,6 +138,24 @@ public MatchInfoResponseDto findMyInfo(User loginUser) { return MatchInfoResponseDto.toDto(matchUserInfo); } + // 내 정보 수정 + @Transactional + public void updateMyInfo(MatchInfoUpdateRequestDto dto, User loginUser) { + MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + + matchUserInfo.updateMatchUserInfo( + dto.getGender(), + dto.getLanes(), + dto.getPurposes(), + dto.getPlayTimeRanges(), + dto.getGameRank(), + dto.getSkillLevel(), + dto.getMicUsage(), + dto.getMessage() + ); + } + // 내 정보 삭제, 내정보 삭제시 매칭 추천에서 더이상 검색되지 않음 @Transactional public void deleteMyInfo(User loginUser) { From 731d87fcb97def82e5f4f62c8c008ee16e274949 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 13:31:28 +0900 Subject: [PATCH 100/215] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=EB=B0=A9=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 24 +++++++++++++---- .../domain/match/service/MatchService.java | 27 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 5dc470f..d52987d 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -96,7 +96,7 @@ public ResponseEntity deleteMatch( * @param dto MatchInfoCreateRequestDto * @return matchInfoResponseDto */ - @PostMapping("/info") + @PostMapping("/my-info") public ResponseEntity createMyInfo( @RequestBody MatchInfoCreateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails @@ -110,7 +110,7 @@ public ResponseEntity createMyInfo( * 매칭 내정보 조회하기 * @return MatchInfoResponseDto */ - @GetMapping("/info") + @GetMapping("/my-info") public ResponseEntity findMyInfo( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { @@ -119,12 +119,27 @@ public ResponseEntity findMyInfo( return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.OK); } + /** + * 매칭 상대방 정보 조회하기 + * @param id 매치 id + * @return MatchInfoResponseDto 상대방 정보 + */ + @GetMapping("/{id}/opponent-info") + public ResponseEntity findOpponentInfo( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + MatchInfoResponseDto matchInfoResponseDto = matchService.findOpponentInfo(id, customUserDetails.getUser()); + return new ResponseEntity<>(matchInfoResponseDto, HttpStatus.OK); + } + /** * 매칭 내정보 수정하기 * @param dto MatchInfoUpdateRequestDto * @return 204 NO_CONTENT */ - @PutMapping("/info") + @PutMapping("/my-info") public ResponseEntity updateMyInfo( @Valid @RequestBody MatchInfoUpdateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails @@ -134,12 +149,11 @@ public ResponseEntity updateMyInfo( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } - /** * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않음 * @return 204 NO_CONTENT */ - @DeleteMapping("/info") + @DeleteMapping("/my-info") public ResponseEntity deleteMyInfo( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 72a879b..91fef42 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -10,6 +10,7 @@ import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; +import com.example.gamemate.global.config.auth.CustomUserDetails; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; @@ -138,6 +139,21 @@ public MatchInfoResponseDto findMyInfo(User loginUser) { return MatchInfoResponseDto.toDto(matchUserInfo); } + // 상대방 정보 조회 + public MatchInfoResponseDto findOpponentInfo(Long id, User loginUser) { + Match findMatch = matchRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); + + if (!Objects.equals(findMatch.getReceiver().getId(), loginUser.getId()) + && !Objects.equals(findMatch.getSender().getId(), loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + return (Objects.equals(findMatch.getReceiver().getId(), loginUser.getId())) + ? getMatchInfoResponseDto(findMatch.getSender()) + : getMatchInfoResponseDto(findMatch.getReceiver()); + } + // 내 정보 수정 @Transactional public void updateMyInfo(MatchInfoUpdateRequestDto dto, User loginUser) { @@ -273,12 +289,12 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo } } - if (priority == null || !priority.equals("skillLevel")){ + if (priority == null || !priority.equals("skillLevel")) { int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); score += (normalScorePerMatch * 2 - skillLevelDifference); } - if(priority == null || !priority.equals("micUsage")){ + if (priority == null || !priority.equals("micUsage")) { if (condition.getMicUsage().equals(userInfo.getMicUsage())) { score += normalScorePerMatch * 2; } @@ -329,5 +345,12 @@ private List handleTies(List sortedUsers) { return resultList; } + + // 매칭 상대방 정보 찾아서 dto 로 변환 + private MatchInfoResponseDto getMatchInfoResponseDto(User user) { + MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(user) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + return MatchInfoResponseDto.toDto(matchUserInfo); + } } From 612f63f3af51e019d23d54926bf4159e9cd9d976 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 13:55:34 +0900 Subject: [PATCH 101/215] =?UTF-8?q?fix=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=83=9D=EC=84=B1=EC=8B=9C=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매칭 요청 생성시 로그인한 유저나 상대방 유저가 입력한 본인의 정보가 없을때 예외처리를 하도록 수정 --- .../repository/MatchUserInfoRepository.java | 3 +++ .../domain/match/service/MatchService.java | 22 ++++++++++--------- .../gamemate/global/constant/ErrorCode.java | 3 ++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java index 6099ddf..9953d42 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java @@ -7,13 +7,16 @@ 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; import java.util.Set; +@Repository public interface MatchUserInfoRepository extends JpaRepository { Optional findByUser(User user); + Boolean existsByUser(User user); @Query("SELECT m FROM MatchUserInfo m WHERE m.gender = :gender AND EXISTS (SELECT pt FROM m.playTimeRanges pt WHERE pt IN :playTimeRanges) AND m.user.id <> :userId") List findByGenderAndPlayTimeRanges(@Param("gender") Gender gender, diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 91fef42..db568a9 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -38,19 +38,21 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { User receiver = userRepository.findById(dto.getUserId()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - MatchUserInfo loginUserInfo = matchUserInfoRepository.findByUser(loginUser) - .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + if (!matchUserInfoRepository.existsByUser(receiver)) { + throw new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND); + } // 받는 사람의 매칭 유저 정보가 없을때 예외처리 - MatchUserInfo receiverInfo = matchUserInfoRepository.findByUser(receiver) - .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); + if (!matchUserInfoRepository.existsByUser(loginUser)) { + throw new ApiException(ErrorCode.MATCH_USER_INFO_NOT_WRITTEN); + } // 로그인 한 유저의 매칭 유저 정보가 없을때 예외처리 if (receiver.getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } + } // 받는 사람의 유저 상태가 탈퇴 상태일때 예외처리 if (matchRepository.existsBySenderAndReceiverAndStatus(loginUser, receiver, MatchStatus.PENDING)) { throw new ApiException(ErrorCode.IS_ALREADY_PENDING); - } + } // 이미 보낸 요청이 있을때 예외처리 Match match = new Match(dto.getMessage(), loginUser, receiver); matchRepository.save(match); @@ -67,11 +69,11 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { if (findMatch.getStatus() != MatchStatus.PENDING) { throw new ApiException(ErrorCode.IS_ALREADY_PROCESSED); - } + } // 매칭의 상태가 보류중이 아닐때 예외처리 if (!Objects.equals(loginUser.getId(), findMatch.getReceiver().getId())) { throw new ApiException(ErrorCode.FORBIDDEN); - } + } // 로그인한 유저가 매칭의 받는 사람이 아닐때 예외처리 findMatch.updateStatus(dto.getStatus()); } @@ -105,7 +107,7 @@ public void deleteMatch(Long id, User loginUser) { if (!Objects.equals(findMatch.getSender().getId(), loginUser.getId())) { throw new ApiException(ErrorCode.FORBIDDEN); - } + } // 로그인한 유저가 매칭의 보낸사람이 아닐때 예외처리 matchRepository.delete(findMatch); } @@ -147,7 +149,7 @@ public MatchInfoResponseDto findOpponentInfo(Long id, User loginUser) { if (!Objects.equals(findMatch.getReceiver().getId(), loginUser.getId()) && !Objects.equals(findMatch.getSender().getId(), loginUser.getId())) { throw new ApiException(ErrorCode.FORBIDDEN); - } + } // 매칭의 상대방을 검색할때, 로그인한 유저가 검색할 매칭과 연관이 없을때 예외처리 return (Objects.equals(findMatch.getReceiver().getId(), loginUser.getId())) ? getMatchInfoResponseDto(findMatch.getSender()) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 82b4ebe..f11cdb6 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -33,7 +33,8 @@ public enum ErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), - MATCH_USER_INFO_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭 정보를 찾을 수 없습니다."), + MATCH_USER_INFO_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_FOUND", "매칭을 위해 입력된 회원 정보를 찾을 수 없습니다."), + MATCH_USER_INFO_NOT_WRITTEN(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_WRITTEN", "매칭을 위해 회원 정보 입력은 필수입니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 790791c092a962f96e11e14303ad7b000acac693 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 13:59:28 +0900 Subject: [PATCH 102/215] =?UTF-8?q?feat=20:=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=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 --- .../gamemate/domain/match/controller/MatchController.java | 6 +++--- .../gamemate/domain/match/dto/MatchCreateRequestDto.java | 6 ++++++ .../gamemate/domain/match/dto/MatchUpdateRequestDto.java | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index d52987d..887cd41 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -25,7 +25,7 @@ public class MatchController { */ @PostMapping public ResponseEntity createMatch( - @RequestBody MatchCreateRequestDto dto, + @Valid @RequestBody MatchCreateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { @@ -42,7 +42,7 @@ public ResponseEntity createMatch( @PatchMapping("/{id}") public ResponseEntity updateMatch( @PathVariable Long id, - @RequestBody MatchUpdateRequestDto dto, + @Valid @RequestBody MatchUpdateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { @@ -98,7 +98,7 @@ public ResponseEntity deleteMatch( */ @PostMapping("/my-info") public ResponseEntity createMyInfo( - @RequestBody MatchInfoCreateRequestDto dto, + @Valid @RequestBody MatchInfoCreateRequestDto dto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java index 22bd2aa..d2ddfa3 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchCreateRequestDto.java @@ -1,10 +1,16 @@ package com.example.gamemate.domain.match.dto; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Getter; @Getter public class MatchCreateRequestDto { + @NotNull private Long userId; + + @NotNull + @Size(max = 100, message = "메시지는 100자를 초과할 수 없습니다.") private String message; public MatchCreateRequestDto(Long userId, String message) { diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java index 0ad753d..b03bd19 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchUpdateRequestDto.java @@ -1,10 +1,12 @@ package com.example.gamemate.domain.match.dto; import com.example.gamemate.domain.match.enums.MatchStatus; +import jakarta.validation.constraints.NotNull; import lombok.Getter; @Getter public class MatchUpdateRequestDto { + @NotNull private MatchStatus status; public MatchUpdateRequestDto(MatchStatus status) { From 6decc764e06cabc8ed974a6d15879f2fcdc5667e Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 14:35:27 +0900 Subject: [PATCH 103/215] =?UTF-8?q?refactor=20:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=A0=90=EC=88=98=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skillLevel 의 점수 로직 변경 (기존 최대 10점 -> 최대 5점) --- .../domain/match/dto/MatchInfoCreateRequestDto.java | 2 +- .../gamemate/domain/match/dto/MatchInfoResponseDto.java | 6 +++--- .../domain/match/dto/MatchInfoUpdateRequestDto.java | 2 +- .../gamemate/domain/match/dto/MatchSearchConditionDto.java | 2 +- .../example/gamemate/domain/match/service/MatchService.java | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java index c0b4548..ae5a816 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoCreateRequestDto.java @@ -31,7 +31,7 @@ public class MatchInfoCreateRequestDto { @NotNull(message = "스킬 레벨은 필수 입력값입니다.") @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") - @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + @Max(value = 5, message = "스킬 레벨은 5 이하여야 합니다.") private Integer skillLevel; @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java index 0e67518..0cafd6b 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoResponseDto.java @@ -8,7 +8,7 @@ @Getter public class MatchInfoResponseDto { - private Long id; + private Long matchUserInfoId; private String nickname; private Gender gender; private Set lanes; @@ -20,7 +20,7 @@ public class MatchInfoResponseDto { private String message; public MatchInfoResponseDto( - Long id, + Long matchUserInfoId, String nickname, Gender gender, Set lanes, @@ -31,7 +31,7 @@ public MatchInfoResponseDto( Boolean micUsage, String message ) { - this.id = id; + this.matchUserInfoId = matchUserInfoId; this.nickname = nickname; this.gender = gender; this.lanes = lanes; diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java index d5db68a..51ad254 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchInfoUpdateRequestDto.java @@ -31,7 +31,7 @@ public class MatchInfoUpdateRequestDto { @NotNull(message = "스킬 레벨은 필수 입력값입니다.") @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") - @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + @Max(value = 5, message = "스킬 레벨은 5 이하여야 합니다.") private Integer skillLevel; @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java index 058de4d..fd0c192 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java @@ -31,7 +31,7 @@ public class MatchSearchConditionDto { @NotNull(message = "스킬 레벨은 필수 입력값입니다.") @Min(value = 1, message = "스킬 레벨은 1 이상이어야 합니다.") - @Max(value = 10, message = "스킬 레벨은 10 이하여야 합니다.") + @Max(value = 5, message = "스킬 레벨은 5 이하여야 합니다.") private Integer skillLevel; @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index db568a9..547401b 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -251,7 +251,7 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo break; case "skillLevel": int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); - score += (normalScorePerMatch * 2 - skillLevelDifference) * priorityWeight; + score += (normalScorePerMatch - skillLevelDifference) * priorityWeight; break; case "micUsage": if (condition.getMicUsage().equals(userInfo.getMicUsage())) { @@ -293,7 +293,7 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo if (priority == null || !priority.equals("skillLevel")) { int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); - score += (normalScorePerMatch * 2 - skillLevelDifference); + score += (normalScorePerMatch - skillLevelDifference); } if (priority == null || !priority.equals("micUsage")) { From c2abc43c54572e35e100ead2a7c4ef9da1cd11ce Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 14:48:57 +0900 Subject: [PATCH 104/215] =?UTF-8?q?refactor=20:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=20=EB=B0=8F=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20priority=20=EB=A5=BC=20Enum=20=EC=9D=84=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EC=96=B4=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MatchSearchConditionDto 에서 priority 를 String 으로 받던 것에서 Enum 으로 만든 뒤 받도록 변경 --- .../match/dto/MatchSearchConditionDto.java | 4 +-- .../gamemate/domain/match/enums/Priority.java | 20 +++++++++++++ .../domain/match/service/MatchService.java | 30 ++++++++++--------- 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/match/enums/Priority.java diff --git a/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java index fd0c192..a82cf3b 100644 --- a/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java +++ b/src/main/java/com/example/gamemate/domain/match/dto/MatchSearchConditionDto.java @@ -37,7 +37,7 @@ public class MatchSearchConditionDto { @NotNull(message = "마이크 사용 여부는 필수 입력값입니다.") private Boolean micUsage; - private String priority; + private Priority priority; public MatchSearchConditionDto( Gender gender, @@ -47,7 +47,7 @@ public MatchSearchConditionDto( Set playTimeRanges, Integer skillLevel, Boolean micUsage, - String priority + Priority priority ) { this.gender = gender; this.lanes = lanes; diff --git a/src/main/java/com/example/gamemate/domain/match/enums/Priority.java b/src/main/java/com/example/gamemate/domain/match/enums/Priority.java new file mode 100644 index 0000000..35461f1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/match/enums/Priority.java @@ -0,0 +1,20 @@ +package com.example.gamemate.domain.match.enums; + +import lombok.Getter; + +@Getter +public enum Priority { + GAME_RANK("gameRank"), + GENDER("gender"), + LANES("lanes"), + PLAY_TIME_RANGES("playTimeRanges"), + PURPOSES("purposes"), + MIC_USAGE("micUsage"), + SKILL_LEVEL("skillLevel"); + + private final String name; + + Priority(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 547401b..1e64d8c 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -5,12 +5,12 @@ import com.example.gamemate.domain.match.entity.MatchUserInfo; import com.example.gamemate.domain.match.enums.GameRank; import com.example.gamemate.domain.match.enums.MatchStatus; +import com.example.gamemate.domain.match.enums.Priority; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; -import com.example.gamemate.global.config.auth.CustomUserDetails; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; @@ -23,6 +23,8 @@ import java.util.Objects; import java.util.stream.Collectors; +import static com.example.gamemate.domain.match.enums.Priority.*; + @Service @RequiredArgsConstructor public class MatchService { @@ -219,41 +221,41 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo int normalScorePerMatch = 5; // 매칭되는 항목당 점수 int priorityWeight = 2; // 우선순위 가중치 - String priority = condition.getPriority(); + Priority priority = condition.getPriority(); // 우선순위 항목 점수 계산 및 가중치 적용 if (priority != null) { switch (priority) { - case "lanes": + case LANES: int matchedLanes = (int) condition.getLanes().stream() .filter(userInfo.getLanes()::contains) .count(); score += matchedLanes * normalScorePerMatch * priorityWeight; break; - case "purposes": + case PURPOSES: int matchedPurposes = (int) condition.getPurposes().stream() .filter(userInfo.getPurposes()::contains) .count(); score += matchedPurposes * normalScorePerMatch * priorityWeight; break; - case "playTimeRanges": + case PLAY_TIME_RANGES: int matchedPlayTimeRanges = (int) condition.getPlayTimeRanges().stream() .filter(userInfo.getPlayTimeRanges()::contains) .count(); score += matchedPlayTimeRanges * normalScorePerMatch * priorityWeight; break; - case "gameRank": + case GAME_RANK: if (condition.getGameRank().equals(userInfo.getGameRank())) { score += normalScorePerMatch * priorityWeight * 2; } else if (isRankSimilar(condition.getGameRank(), userInfo.getGameRank())) { score += normalScorePerMatch * priorityWeight; } break; - case "skillLevel": + case SKILL_LEVEL: int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); score += (normalScorePerMatch - skillLevelDifference) * priorityWeight; break; - case "micUsage": + case MIC_USAGE: if (condition.getMicUsage().equals(userInfo.getMicUsage())) { score += normalScorePerMatch * priorityWeight * 2; } @@ -262,28 +264,28 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo } // 우선순위가 아닌 조건의 점수 계산 방식 - if (priority == null || !priority.equals("lanes")) { + if (priority == null || !priority.equals(LANES)) { int matchedLanes = (int) condition.getLanes().stream() .filter(userInfo.getLanes()::contains) .count(); score += matchedLanes * normalScorePerMatch; } - if (priority == null || !priority.equals("purposes")) { + if (priority == null || !priority.equals(PURPOSES)) { int matchedPurposes = (int) condition.getPurposes().stream() .filter(userInfo.getPurposes()::contains) .count(); score += matchedPurposes * normalScorePerMatch; } - if (priority == null || !priority.equals("playTimeRanges")) { + if (priority == null || !priority.equals(PLAY_TIME_RANGES)) { int matchedPlayTimeRanges = (int) condition.getPlayTimeRanges().stream() .filter(userInfo.getPlayTimeRanges()::contains) .count(); score += matchedPlayTimeRanges * normalScorePerMatch; } - if (priority == null || !priority.equals("gameRank")) { + if (priority == null || !priority.equals(GAME_RANK)) { if (condition.getGameRank().equals(userInfo.getGameRank())) { score += normalScorePerMatch * 2; } else if (isRankSimilar(condition.getGameRank(), userInfo.getGameRank())) { @@ -291,12 +293,12 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo } } - if (priority == null || !priority.equals("skillLevel")) { + if (priority == null || !priority.equals(SKILL_LEVEL)) { int skillLevelDifference = Math.abs(condition.getSkillLevel() - userInfo.getSkillLevel()); score += (normalScorePerMatch - skillLevelDifference); } - if (priority == null || !priority.equals("micUsage")) { + if (priority == null || !priority.equals(MIC_USAGE)) { if (condition.getMicUsage().equals(userInfo.getMicUsage())) { score += normalScorePerMatch * 2; } From 26524830351e64b109b7efc92853eb1b0234608a Mon Sep 17 00:00:00 2001 From: sumyeom Date: Thu, 16 Jan 2025 14:53:24 +0900 Subject: [PATCH 105/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=EC=97=90=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=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 1. 게시글에 파일 업로드 기능 추가 --- .../board/controller/BoardController.java | 3 - .../gamemate/domain/board/entity/Board.java | 7 +- .../controller/BoardImageController.java | 72 +++++++++++ .../domain/boardImage/entity/BoardImage.java | 48 ++++++++ .../repository/BoardImageRepository.java | 7 ++ .../boardImage/service/BoardImageService.java | 115 ++++++++++++++++++ .../gamemate/global/constant/ErrorCode.java | 1 + 7 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java create mode 100644 src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java create mode 100644 src/main/java/com/example/gamemate/domain/boardImage/repository/BoardImageRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index ca0df50..0447547 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -6,15 +6,12 @@ import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.service.BoardService; -import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import java.util.List; diff --git a/src/main/java/com/example/gamemate/domain/board/entity/Board.java b/src/main/java/com/example/gamemate/domain/board/entity/Board.java index 02d331e..09a50c3 100644 --- a/src/main/java/com/example/gamemate/domain/board/entity/Board.java +++ b/src/main/java/com/example/gamemate/domain/board/entity/Board.java @@ -1,12 +1,14 @@ package com.example.gamemate.domain.board.entity; import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.boardImage.entity.BoardImage; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.common.BaseEntity; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.context.annotation.EnableMBeanExport; + +import java.util.List; @Entity @Getter @@ -33,6 +35,9 @@ public class Board extends BaseEntity { @JoinColumn(name = "user_id") private User user; + @OneToMany(mappedBy = "board") + private List boardImages; + public Board(BoardCategory category, String title, String content, User user) { this.category = category; this.title = title; diff --git a/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java b/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java new file mode 100644 index 0000000..0524525 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java @@ -0,0 +1,72 @@ +package com.example.gamemate.domain.boardImage.controller; + +import com.example.gamemate.domain.boardImage.service.BoardImageService; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/boards/{boardId}/files") +public class BoardImageController { + + private final BoardImageService boardImageService; + + /** + * 게시글 첨부파일 추가 API + * @param boardId + * @param image + * @return + * @throws IOException + */ + @PostMapping + public ResponseEntity createBoardImage( + @PathVariable Long boardId, + @RequestParam("image") MultipartFile image, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) throws IOException { + boardImageService.createBoardImage(customUserDetails.getUser(), boardId, image); + return new ResponseEntity<>("업로드", HttpStatus.CREATED); + } + + /** + * 게시글 첨부파일 수정 API + * @param id + * @param image + * @param customUserDetails + * @return + */ + @PutMapping("/{id}") + public ResponseEntity updateBoardImage( + @PathVariable Long id, + @RequestParam("image") MultipartFile image, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) throws IOException { + boardImageService.updateBoardImage(customUserDetails.getUser(), id, image); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + + /** + * 이미지 삭제 + * @param id + * @param customUserDetails + * @return + * @throws IOException + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteBoardImage( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) throws IOException { + boardImageService.deleteImage(customUserDetails.getUser(), id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + +} diff --git a/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java b/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java new file mode 100644 index 0000000..aeb855d --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java @@ -0,0 +1,48 @@ +package com.example.gamemate.domain.boardImage.entity; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.global.common.BaseCreatedEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "board_image") +public class BoardImage extends BaseCreatedEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long BoardImageId; + + @ManyToOne + @JoinColumn(name = "board_id") + private Board board; + + @Column(nullable = false) + private String fileName; + + @Column(nullable = false) + private String fileType; + + @Column(nullable = false) + private String filePath; + + @Column(nullable = false) + private Long fileSize; + + public BoardImage(String fileName, String fileType, String filePath, Long fileSize, Board board) { + this.fileName = fileName; + this.fileType = fileType; + this.filePath = filePath; + this.fileSize = fileSize; + this.board = board; + } + + public void updateBoardImage(String fileName, String fileType, String filePath, Long fileSize) { + this.fileName = fileName; + this.fileType = fileType; + this.filePath = filePath; + this.fileSize = fileSize; + } +} diff --git a/src/main/java/com/example/gamemate/domain/boardImage/repository/BoardImageRepository.java b/src/main/java/com/example/gamemate/domain/boardImage/repository/BoardImageRepository.java new file mode 100644 index 0000000..5b7db3a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/boardImage/repository/BoardImageRepository.java @@ -0,0 +1,7 @@ +package com.example.gamemate.domain.boardImage.repository; + +import com.example.gamemate.domain.boardImage.entity.BoardImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BoardImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java b/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java new file mode 100644 index 0000000..6bb927b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java @@ -0,0 +1,115 @@ +package com.example.gamemate.domain.boardImage.service; + +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.boardImage.entity.BoardImage; +import com.example.gamemate.domain.boardImage.repository.BoardImageRepository; +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.s3.S3Service; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BoardImageService { + + private final S3Service s3Service; + private final BoardImageRepository boardImageRepository; + private final BoardRepository boardRepository; + + /** + * 이미지 업로드 메서드 + * @param loginUser + * @param boardId + * @param image + * @throws IOException + */ + @Transactional + public void createBoardImage(User loginUser, Long boardId, MultipartFile image) throws IOException { + // 게시글 조회 + Board findBoard = boardRepository.findById(boardId) + .orElseThrow(()-> new ApiException(ErrorCode.BOARD_NOT_FOUND)); + + // 게시글 작성자와 로그인한 유저 확인 + if(!findBoard.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + //업로드 된 이미지 파일 주소 + String publicUrl = s3Service.uploadFile(image); + //BoardImage 테이블에 담을 변수 생성 + BoardImage boardImage = new BoardImage(image.getOriginalFilename(), image.getContentType(), publicUrl, image.getSize(),findBoard); + + //BoardImage 테이블에 저장 + boardImageRepository.save(boardImage); + } + + /** + * 이미지 업데이트 메서드 + * @param loginUser + * @param id + * @param image + * @throws IOException + */ + @Transactional + public void updateBoardImage(User loginUser, Long id, MultipartFile image) throws IOException { + // 이미지 조회 + BoardImage findBoardImage = boardImageRepository.findById(id) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_IMAGE_NOT_FOUND)); + + // 이미지 업로드 유저와 로그인 유저 확인 + if(!findBoardImage.getBoard().getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + //업로드 된 이미지 파일 주소 + String publicUrl = s3Service.uploadFile(image); + //BoardImage 테이블에 담을 변수 생성 + findBoardImage.updateBoardImage(image.getOriginalFilename(), image.getContentType(), publicUrl, image.getSize()); + + //BoardImage 테이블에 저장 + boardImageRepository.save(findBoardImage); + + try{ + // S3 에서 이미지 삭제 + s3Service.deleteFile(findBoardImage.getFilePath()); + } catch(Exception e){ + log.error("파일 업로드 에러 발생 : {}",e.getMessage()); + } + + } + + + /** + * 이미지 삭제 메서드 + * @param loginUser + * @param id + */ + @Transactional + public void deleteImage(User loginUser, Long id) { + // 이미지 조회 + BoardImage findBoardImage = boardImageRepository.findById(id) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_IMAGE_NOT_FOUND)); + + // 이미지 업로드 유저와 로그인 유저 확인 + if(!findBoardImage.getBoard().getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + // S3 에서 이미지 삭제 + s3Service.deleteFile(findBoardImage.getFilePath()); + + // 이미지 삭제 + boardImageRepository.delete(findBoardImage); + } + + +} diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 10f99e1..752ab9e 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -34,6 +34,7 @@ public enum ErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_NOT_FOUND","리뷰를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_NOT_FOUND", "댓글을 찾을 수 없습니다."), MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_NOT_FOUND", "매칭을 찾을 수 없습니다."), + BOARD_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD_IMAGE_NOT_FOUND", "이미지를 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."),; From 7df94c0bb2168f1a8b1869168b9bf663e4c1cf06 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 14:54:04 +0900 Subject: [PATCH 106/215] =?UTF-8?q?feat=20:=20DataIntegrityViolationExcept?= =?UTF-8?q?ion=20=EC=A0=84=EC=97=AD=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/global/constant/ErrorCode.java | 1 + .../global/exception/GlobalExceptionHandler.java | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index f11cdb6..dc88062 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -16,6 +16,7 @@ public enum ErrorCode { INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "비밀번호가 일치하지 않습니다."), IS_ALREADY_PENDING(HttpStatus.BAD_REQUEST, "IS_ALREADY_PENDING", "이미 대기중인 요청이 있습니다."), IS_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "IS_ALREADY_PROCESSED", "이미 처리된 요청입니다."), + IS_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "IS_ALREADY_EXIST", "존재하는 정보가 있습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 8b528e7..ca1f663 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.MalformedJwtException; import lombok.extern.slf4j.Slf4j; import org.apache.coyote.Response; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -33,6 +34,7 @@ public ResponseEntity handleCustomException(ApiException e) { ErrorCode errorCode = e.getErrorCode(); return handleExceptionInternal(errorCode,errorCode.getMessage()); } + @ExceptionHandler(ResponseStatusException.class) public ResponseEntity handleResponseStatusException(ResponseStatusException ex) { @@ -100,6 +102,13 @@ public ResponseEntity handleHttpMessageNotReadableException(HttpMessageN return handleExceptionInternal(errorCode); } + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e) { + log.warn("handleDataIntegrityViolationException", e); + ErrorCode errorCode = ErrorCode.IS_ALREADY_EXIST; + return handleExceptionInternal(errorCode); + } + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getStatus()) .body(makeErrorResponse(errorCode)); From 104a24df16f8978112d499a3622f5a0b49467086 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 15:57:31 +0900 Subject: [PATCH 107/215] =?UTF-8?q?refactor=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=EC=9D=84=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=98=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 --- .../service/AsyncNotificationService.java | 49 +++++++++++++++++++ .../service/NotificationService.java | 32 ++---------- 2 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java diff --git a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java new file mode 100644 index 0000000..6342f6c --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java @@ -0,0 +1,49 @@ +package com.example.gamemate.domain.notification.service; + +import com.example.gamemate.domain.notification.entity.Notification; +import com.example.gamemate.domain.notification.repository.NotificationRepository; +import com.example.gamemate.domain.user.entity.User; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@EnableAsync +public class AsyncNotificationService { + private final JavaMailSender javaMailSender; + private final NotificationRepository notificationRepository; + + @Async + public void sendNotificationMail(User user, List notifications) { + try { + SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); + simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 + simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 + simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 + simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 + + javaMailSender.send(simpleMailMessage); + log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); + + updateNotificationStatus(notifications); + } catch (Exception e) { + log.error("알림 메일 전송 실패: {}", user.getEmail(), e); + } + } + + // 알림 전송 후 notified(false -> true) 상태 변경 + @Transactional + public void updateNotificationStatus(List notifications) { + notifications.forEach(notification -> notification.updateSentStatus(true)); + notificationRepository.saveAll(notifications); + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 3db057b..2faca2d 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -5,13 +5,10 @@ import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.repository.NotificationRepository; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -25,7 +22,7 @@ public class NotificationService { private final NotificationRepository notificationRepository; - private final JavaMailSender javaMailSender; + private final AsyncNotificationService asyncNotificationService; // 알림 생성 @Transactional @@ -47,8 +44,9 @@ public List findAllNotification() { } // 알림 발송 (이메일) - @Scheduled(cron = "0 0/10 * * * *") - public void sendNotificationMail() { + @Scheduled(cron = "0 0/3 * * * *") + public void scheduleNotificationEmail() { + log.info("스케쥴링 활성화"); List unnotifiedNotificationList = notificationRepository.findAllBySentStatus(false); @@ -66,27 +64,7 @@ public void sendNotificationMail() { User user = entry.getKey(); List notifications = entry.getValue(); - try { - SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); - simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 - simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 - simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 - simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 - - javaMailSender.send(simpleMailMessage); - log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); - - updateNotificationStatus(notifications); - } catch (Exception e) { - log.error("알림 메일 전송 실패: {}", user.getEmail(), e); - } + asyncNotificationService.sendNotificationMail(user, notifications); } } - - // 알림 전송 후 notified(false -> true) 상태 변경 - @Transactional - public void updateNotificationStatus(List notifications) { - notifications.forEach(notification -> notification.updateSentStatus(true)); - notificationRepository.saveAll(notifications); - } } From 1a90825658c46116dce360146d83d09d44f6c187 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:12:47 +0900 Subject: [PATCH 108/215] =?UTF-8?q?fix:=20=20#48=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. ) { 앞에서 엔터 --- .../domain/game/controller/GameRecommendContorller.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java index 4a47ed5..bf37544 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java @@ -25,7 +25,8 @@ public class GameRecommendContorller { @PostMapping public ResponseEntity createUserGamePreference( @RequestBody UserGamePreferenceRequestDto requestDto, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { UserGamePreferenceResponseDto responseDto = gameRecommendService.createUserGamePreference(requestDto, customUserDetails.getUser()); From 9190288998d9a985357b5d5c4ee3f1ed066c003e Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:01:07 +0900 Subject: [PATCH 109/215] =?UTF-8?q?fix:=20=EA=B2=8C=EC=9E=84=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C/=EC=83=88=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게임 첨부파일 수정시 기존 이미지 삭제/새파일 업로드 로직 분할 --- .../domain/game/service/GameService.java | 47 ++++++++++--------- .../gamemate/global/constant/ErrorCode.java | 1 + 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index f277b27..808e63d 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -89,29 +89,39 @@ public GameFindByIdResponseDto findGameById(Long id) { @Transactional public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile, User loginUser) { - - //관리자만 가능함(수정) - if (!loginUser.getRole().equals(Role.ADMIN)) { + if (loginUser == null || !loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } Game game = gameRepository.findGameById(id) .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); - // 기존 파일이 있고 새 파일이 업로드된 경우 - // 1. 기존 S3 파일 삭제 - if (!game.getImages().isEmpty()) { - for (GameImage image : game.getImages()) { + deleteExistingImages(game); + uploadNewImage(game, newFile); + + game.updateGame( + requestDto.getTitle(), + requestDto.getGenre(), + requestDto.getPlatform(), + requestDto.getDescription() + ); + + gameRepository.save(game); + } + + private void deleteExistingImages(Game game) { + for (GameImage image : game.getImages()) { + try { s3Service.deleteFile(image.getFilePath()); + } catch (Exception e) { + // 로그 기록 후 계속 진행 + log.error("Failed to delete file: {}", image.getFilePath(), e); } } + game.getImages().clear(); + } - List gameImages = gameImageRepository.findGameImagesByGameId(id); - if (!gameImages.isEmpty()) { - gameImageRepository.deleteAll(gameImages); - } - - // 2. 새 파일 업로드 + private void uploadNewImage(Game game, MultipartFile newFile) { if (newFile != null && !newFile.isEmpty()) { try { String fileUrl = s3Service.uploadFile(newFile); @@ -123,19 +133,12 @@ public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile n ); game.addImage(gameImage); } catch (IOException e) { - throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e); + throw new ApiException(ErrorCode.FILE_UPLOAD_ERROR); } } - - game.updateGame( - requestDto.getTitle(), - requestDto.getGenre(), - requestDto.getPlatform(), - requestDto.getDescription() - ); - gameRepository.save(game); } + @Transactional public void deleteGame(Long id, User loginUser) { diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 712fcfa..e11e708 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -17,6 +17,7 @@ public enum ErrorCode { REVIEW_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"REVIEW_ALREADY_EXISTS","이미 리뷰를 작성한 회원입니다."), IS_ALREADY_PENDING(HttpStatus.BAD_REQUEST, "IS_ALREADY_PENDING", "이미 대기중인 요청이 있습니다."), IS_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "IS_ALREADY_PROCESSED", "이미 처리된 요청입니다."), + FILE_UPLOAD_ERROR(HttpStatus.BAD_REQUEST,"FILE_UPLOAD_ERROR","파일 업로드 중 오류가 발생했습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), From 1249989c5b8394fb9c859ef5de23f26406fe55da Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:02:53 +0900 Subject: [PATCH 110/215] =?UTF-8?q?refact:=20=EC=9E=90=EB=B0=94=EB=8F=85?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게임 컨트롤러 자바독 작성 2. 게임요청 컨트롤러 자바독 작성 3. 게임추천 컨트롤러 자바독 작성 4. 게임리뷰 컨트롤러 자바독 작성 5. 좋아요 컨트롤러 자바독 작성 --- .../game/controller/GameController.java | 45 ++++++++++++------- .../GameEnrollRequestController.java | 38 +++++++++++----- .../controller/GameRecommendContorller.java | 22 +++++++-- .../like/controller/LikeController.java | 32 +++++++++++++ .../review/controller/ReviewController.java | 44 +++++++++++------- 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 5d1460f..6425a6c 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -17,6 +17,10 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +/** + * 게임 관련 API를 처리하는 컨트롤러 클래스입니다. + * 게임의 생성, 조회, 수정, 삭제 기능을 제공합니다. + */ @RestController @RequestMapping("/games") @Slf4j @@ -25,10 +29,13 @@ public class GameController { private final GameService gameService; /** - * @param gameDataString - * @param file - * @param customUserDetails - * @return + * 새로운 게임을 생성합니다. + * + * @param gameDataString 게임 데이터를 포함한 JSON 문자열 + * @param file 게임 관련 이미지 파일 (선택적) + * @param customUserDetails 인증된 사용자 정보 + * @return 생성된 게임 정보를 포함한 ResponseEntity + * @throws RuntimeException JSON 파싱 오류 시 발생 */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createGame( @@ -49,11 +56,14 @@ public ResponseEntity createGame( } /** - * 게임 전체 조회 + * 모든 게임을 페이지네이션하여 조회하거나 검색합니다. * - * @param page - * @param size - * @return + * @param keyword 검색 키워드 (선택적) + * @param genre 게임 장르 (선택적) + * @param platform 게임 플랫폼 (선택적) + * @param page 페이지 번호 (기본값: 0) + * @param size 페이지 크기 (기본값: 10) + * @return 게임 목록을 포함한 ResponseEntity */ @GetMapping public ResponseEntity> findAllGame( @@ -77,10 +87,14 @@ public ResponseEntity> findAllGame( } /** - * 게임 단건 조회 + * 특정 ID의 게임 정보를 수정합니다. * - * @param id - * @return + * @param id 수정할 게임의 ID + * @param gameDataString 수정할 게임 데이터를 포함한 JSON 문자열 + * @param newFile 새로운 게임 이미지 파일 (선택적) + * @param customUserDetails 인증된 사용자 정보 + * @return 수정 결과를 나타내는 ResponseEntity + * @throws RuntimeException JSON 파싱 오류 시 발생 */ @GetMapping("/{id}") public ResponseEntity findGameById( @@ -92,10 +106,11 @@ public ResponseEntity findGameById( } /** - * @param id - * @param gameDataString - * @param newFile - * @return + * 특정 ID의 게임을 삭제합니다. + * + * @param id 삭제할 게임의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 삭제 결과를 나타내는 ResponseEntity */ @PatchMapping("/{id}") public ResponseEntity updateGame( diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 1e77186..8b0b1bc 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -14,6 +14,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +/** + * 게임 등록 요청 관련 API를 처리하는 컨트롤러 클래스입니다. + * 게임 등록 요청의 생성, 조회, 수정, 삭제 기능을 제공합니다. + */ @RestController @RequestMapping("/games/requests") @RequiredArgsConstructor @@ -21,10 +25,11 @@ public class GameEnrollRequestController { private final GameEnrollRequestService gameEnrollRequestService; /** - * 게임등록 요청 + * 새로운 게임 등록 요청을 생성합니다. * - * @param requestDto - * @return + * @param requestDto 게임 등록 요청 데이터 + * @param customUserDetails 인증된 사용자 정보 + * @return 생성된 게임 등록 요청 정보를 포함한 ResponseEntity */ @PostMapping public ResponseEntity CreateGameEnrollRequest( @@ -36,9 +41,10 @@ public ResponseEntity CreateGameEnrollRequest( } /** - * 게임등록 요청 전체 조회 + * 모든 게임 등록 요청을 조회합니다. * - * @return + * @param customUserDetails 인증된 사용자 정보 + * @return 게임 등록 요청 목록을 포함한 ResponseEntity */ @GetMapping public ResponseEntity> findAllGameEnrollRequest( @@ -50,10 +56,10 @@ public ResponseEntity> findAllGameEnrollReque } /** - * 게임등록 요청 단건 조회 + * 모든 게임 등록 요청을 조회합니다. * - * @param id - * @return + * @param customUserDetails 인증된 사용자 정보 + * @return 게임 등록 요청 목록을 포함한 ResponseEntity */ @GetMapping("/{id}") public ResponseEntity findGameEnrollRequestById( @@ -65,11 +71,12 @@ public ResponseEntity findGameEnrollRequestById( } /** - * 게임등록 요청 수정 & 게임등록 기능 연계 + * 특정 ID의 게임 등록 요청을 수정하고, 필요시 게임 등록 기능과 연계합니다. * - * @param id - * @param requestDto - * @return + * @param id 수정할 게임 등록 요청의 ID + * @param requestDto 수정할 게임 등록 요청 데이터 + * @param customUserDetails 인증된 사용자 정보 + * @return 수정 결과를 나타내는 ResponseEntity */ @PatchMapping("/{id}") public ResponseEntity updateGameEnroll( @@ -81,6 +88,13 @@ public ResponseEntity updateGameEnroll( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 특정 ID의 게임 등록 요청을 삭제합니다. + * + * @param id 삭제할 게임 등록 요청의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 삭제 결과를 나타내는 ResponseEntity + */ @DeleteMapping("/{id}") public ResponseEntity deleteGameEnroll( @PathVariable Long id, diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java index bf37544..564f88c 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java @@ -1,7 +1,6 @@ package com.example.gamemate.domain.game.controller; import com.example.gamemate.domain.game.dto.GameRecommendHistorysResponseDto; -import com.example.gamemate.domain.game.dto.GameRecommendationResponseDto; import com.example.gamemate.domain.game.dto.UserGamePreferenceRequestDto; import com.example.gamemate.domain.game.dto.UserGamePreferenceResponseDto; import com.example.gamemate.domain.game.service.GameRecommendService; @@ -13,8 +12,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; - +/** + * 게임 추천 관련 API를 처리하는 컨트롤러 클래스입니다. + * 사용자 게임 선호도 생성 및 게임 추천 이력 조회 기능을 제공합니다. + */ @RestController @RequestMapping("/games/recommendations") @RequiredArgsConstructor @@ -22,6 +23,13 @@ public class GameRecommendContorller { private final GameRecommendService gameRecommendService; + /** + * 사용자의 게임 선호도를 생성합니다. + * + * @param requestDto 사용자 게임 선호도 정보 + * @param customUserDetails 인증된 사용자 정보 + * @return 생성된 사용자 게임 선호도 정보를 포함한 ResponseEntity + */ @PostMapping public ResponseEntity createUserGamePreference( @RequestBody UserGamePreferenceRequestDto requestDto, @@ -33,11 +41,19 @@ public ResponseEntity createUserGamePreference( return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } + /** + * 특정 사용자의 게임 추천 이력을 조회합니다. + * + * @param userId 조회할 사용자의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 게임 추천 이력 목록을 포함한 ResponseEntity + */ @GetMapping("/{userId}") public ResponseEntity> getGameRecommendHistories( @PathVariable Long userId, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { + Page responseDto = gameRecommendService.getGameRecommendHistories(userId, customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java index 1b3f24b..d8f3052 100644 --- a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -13,6 +13,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +/** + * 좋아요 기능을 처리하는 컨트롤러 클래스입니다. + * 리뷰와 게시판에 대한 좋아요 기능을 제공합니다. + */ @RestController @RequestMapping("/likes") @RequiredArgsConstructor @@ -20,6 +24,14 @@ public class LikeController { private final LikeService likeService; + /** + * 리뷰에 대한 좋아요를 처리합니다. + * + * @param reviewId 좋아요를 누를 리뷰의 ID + * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소) + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 좋아요 처리 결과를 담은 ResponseEntity + */ @PostMapping("/reviews/{reviewId}") public ResponseEntity reviewLikeUp( @PathVariable Long reviewId, @@ -30,6 +42,14 @@ public ResponseEntity reviewLikeUp( return new ResponseEntity<>(responseDto, HttpStatus.OK); } + /** + * 게시글에 대한 좋아요를 처리합니다. + * + * @param boardId 좋아요를 누를 게시글의 ID + * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소) + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 좋아요 처리 결과를 담은 ResponseEntity + */ @PostMapping("/boards/{boardId}") public ResponseEntity boardLikeUp( @PathVariable Long boardId, @@ -40,6 +60,12 @@ public ResponseEntity boardLikeUp( return new ResponseEntity<>(responseDto, HttpStatus.OK); } + /** + * 특정 리뷰의 좋아요 수를 조회합니다. + * + * @param reviewId 조회할 리뷰의 ID + * @return 리뷰의 좋아요 수를 담은 ResponseEntity + */ @GetMapping("/reviews/{reviewId}") public ResponseEntity reviewLikeCount( @PathVariable Long reviewId) { @@ -49,6 +75,12 @@ public ResponseEntity reviewLikeCount( return new ResponseEntity<>(responseDto, HttpStatus.OK); } + /** + * 특정 게시글의 좋아요 수를 조회합니다. + * + * @param boardId 조회할 게시글의 ID + * @return 게시글의 좋아요 수를 담은 ResponseEntity + */ @GetMapping("/boards/{boardId}") public ResponseEntity boardLikeCount( @PathVariable Long boardId) { diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 3cd4907..9cfec15 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -25,6 +25,10 @@ import static com.example.gamemate.global.constant.ErrorCode.USER_NOT_FOUND; +/** + * 게임 리뷰 관련 API를 처리하는 컨트롤러 클래스입니다. + * 이 컨트롤러는 리뷰의 생성, 수정, 삭제 및 조회 기능을 제공합니다. + */ @RestController @RequestMapping("/games/{gameId}/reviews") @Slf4j @@ -33,14 +37,13 @@ public class ReviewController { private final ReviewService reviewService; - /** - * 리뷰등록 + * 새로운 게임 리뷰를 생성합니다. * - * @param gameId - * @param requestDto - * @param customUserDetails - * @return + * @param gameId 리뷰를 작성할 게임의 ID + * @param requestDto 리뷰 생성 요청 데이터 + * @param customUserDetails 인증된 사용자 정보 + * @return 생성된 리뷰 정보를 포함한 ResponseEntity */ @PostMapping public ResponseEntity createReview( @@ -53,13 +56,13 @@ public ResponseEntity createReview( } /** - * 리뷰수정 + * 기존 게임 리뷰를 수정합니다. * - * @param gameId - * @param id - * @param requestDto - * @param customUserDetails - * @return + * @param gameId 리뷰가 속한 게임의 ID + * @param id 수정할 리뷰의 ID + * @param requestDto 리뷰 수정 요청 데이터 + * @param customUserDetails 인증된 사용자 정보 + * @return 수정 결과를 나타내는 ResponseEntity */ @PatchMapping("/{id}") public ResponseEntity updateReview( @@ -73,12 +76,12 @@ public ResponseEntity updateReview( } /** - * 리뷰삭제 + * 게임 리뷰를 삭제합니다. * - * @param gameId - * @param id - * @param customUserDetails - * @return + * @param gameId 리뷰가 속한 게임의 ID + * @param id 삭제할 리뷰의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 삭제 결과를 나타내는 ResponseEntity */ @DeleteMapping("/{id}") public ResponseEntity deleteReview( @@ -90,6 +93,13 @@ public ResponseEntity deleteReview( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 특정 게임의 모든 리뷰를 조회합니다. + * + * @param gameId 리뷰를 조회할 게임의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 게임의 모든 리뷰 목록을 포함한 ResponseEntity + */ @GetMapping public ResponseEntity> ReviewFindAllByGameId( @PathVariable Long gameId, From 5b4790d3f7f2e627ab9c62ae6eeee15f691267ac Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 20:34:03 +0900 Subject: [PATCH 111/215] =?UTF-8?q?feat=20:=20NotificationType=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/enums/NotificationType.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java b/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java index f6ffe03..00d64ff 100644 --- a/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java +++ b/src/main/java/com/example/gamemate/domain/notification/enums/NotificationType.java @@ -4,10 +4,12 @@ @Getter public enum NotificationType { - FOLLOW("follow", "새 팔로워가 생겼습니다."), - COMMENT("comment", "새로운 댓글이 달렸습니다."), - MATCHING("matching", "매칭에 성공했습니다."), - LIKE("like", "좋아요가 달렸습니다."); + NEW_FOLLOWER("follow", "새 팔로워가 생겼습니다."), + NEW_COMMENT("comment", "새로운 댓글이 달렸습니다."), + NEW_MATCH("new_match", "새로운 매칭 요청이 왔습니다."), + MATCH_REJECTED("match_rejected", "보낸 매칭이 거절되었습니다."), + MATCH_ACCEPTED("match_accepted", "보낸 매칭이 수락되었습니다."), + NEW_LIKE("like", "새 좋아요가 달렸습니다."); private final String name; private final String content; From a92c857cb8a5f1f6447d53b966fcdaacd4eadb38 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 20:35:17 +0900 Subject: [PATCH 112/215] =?UTF-8?q?refactor=20:=20MatchController=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=BD=94=EB=93=9C=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EC=95=88=EB=A7=9E=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/match/controller/MatchController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 887cd41..6c007c0 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -59,8 +59,8 @@ public ResponseEntity> findAllReceivedMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List MatchResponseDtoList = matchService.findAllReceivedMatch(customUserDetails.getUser()); - return new ResponseEntity<>(MatchResponseDtoList, HttpStatus.OK); + List matchResponseDtoList = matchService.findAllReceivedMatch(customUserDetails.getUser()); + return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } /** @@ -72,8 +72,8 @@ public ResponseEntity> findAllSentMatch( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List MatchResponseDtoList = matchService.findAllSentMatch(customUserDetails.getUser()); - return new ResponseEntity<>(MatchResponseDtoList, HttpStatus.OK); + List matchResponseDtoList = matchService.findAllSentMatch(customUserDetails.getUser()); + return new ResponseEntity<>(matchResponseDtoList, HttpStatus.OK); } /** From 971d109f1cb4b39418ae146915eeed43cf58f09f Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 16 Jan 2025 22:24:23 +0900 Subject: [PATCH 113/215] =?UTF-8?q?fix=20:=20Lane=20Enu=20=EC=9D=98=20kore?= =?UTF-8?q?anName=20=EC=9D=B4=20=EC=9E=98=EB=AA=BB=20=EA=B8=B0=EC=9E=85?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8D=98=20=EB=B6=80=EB=B6=84=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 MID("정글") -> MID("미드") 로 수정 --- src/main/java/com/example/gamemate/domain/match/enums/Lane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/match/enums/Lane.java b/src/main/java/com/example/gamemate/domain/match/enums/Lane.java index 856276a..9151294 100644 --- a/src/main/java/com/example/gamemate/domain/match/enums/Lane.java +++ b/src/main/java/com/example/gamemate/domain/match/enums/Lane.java @@ -6,7 +6,7 @@ public enum Lane { TOP("top", "탑"), JUNGLE("jungle", "정글"), - MID("mid","정글"), + MID("mid","미드"), BOTTOM_AD("bottom_ad", "원딜"), BOTTOM_SUPPORTER("bottom_supporter", "서포터"); From e293b38ca109a03f562a6d1574666d6ac762efdd Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 17 Jan 2025 14:43:07 +0900 Subject: [PATCH 114/215] =?UTF-8?q?feat=20:=20=EA=B0=81=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=97=90=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=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 각 기능의 이벤트 발생시 알림이 전송되도록 구현 --- .../comment/service/CommentService.java | 4 ++++ .../domain/follow/service/FollowService.java | 4 ++++ .../domain/like/service/LikeService.java | 5 +++++ .../domain/match/service/MatchService.java | 12 +++++++++++ .../domain/reply/service/ReplyService.java | 20 ++++++++++++++++++- 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index b75146c..0454aaa 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -8,6 +8,8 @@ import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; import com.example.gamemate.domain.reply.entity.Reply; import com.example.gamemate.domain.reply.repository.ReplyRepository; @@ -34,6 +36,7 @@ public class CommentService { private final CommentRepository commentRepository; private final ReplyRepository replyRepository; private final BoardRepository boardRepository; + private final NotificationService notificationService; /** * 댓글 생성 메서드 @@ -49,6 +52,7 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq Comment comment = new Comment(requestDto.getContent(), findBoard, loginUser); Comment createComment = commentRepository.save(comment); + notificationService.createNotification(findBoard.getUser(), NotificationType.NEW_COMMENT); return new CommentResponseDto( createComment.getCommentId(), diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index ad85def..26f3913 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -3,6 +3,8 @@ import com.example.gamemate.domain.follow.dto.*; import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.domain.follow.repository.FollowRepository; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; @@ -22,6 +24,7 @@ public class FollowService { private final UserRepository userRepository; private final FollowRepository followRepository; + private final NotificationService notificationService; // 팔로우하기 @Transactional @@ -44,6 +47,7 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser Follow follow = new Follow(loginUser, followee); followRepository.save(follow); + notificationService.createNotification(followee, NotificationType.NEW_FOLLOWER); return new FollowResponseDto( follow.getId(), diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 05effbd..505ce2b 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -8,6 +8,8 @@ import com.example.gamemate.domain.like.entity.ReviewLike; import com.example.gamemate.domain.like.repository.BoardLikeRepository; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; @@ -25,6 +27,7 @@ public class LikeService { private final ReviewRepository reviewRepository; private final BoardLikeRepository boardLikeRepository; private final BoardRepository boardRepository; + private final NotificationService notificationService; @Transactional public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User loginUser) { @@ -40,6 +43,7 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo if (reviewLike.getId() == null) { reviewLikeRepository.save(reviewLike); + notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE); } else { reviewLike.changeStatus(status); } @@ -61,6 +65,7 @@ public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User login if (boardLike.getId() == null) { boardLikeRepository.save(boardLike); + notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE); } else { boardLike.changeStatus(status); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 1e64d8c..9fcc04e 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -8,6 +8,8 @@ import com.example.gamemate.domain.match.enums.Priority; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; @@ -32,6 +34,7 @@ public class MatchService { private final UserRepository userRepository; private final MatchRepository matchRepository; private final MatchUserInfoRepository matchUserInfoRepository; + private final NotificationService notificationService; // 매칭 요청 생성 @Transactional @@ -58,6 +61,7 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { Match match = new Match(dto.getMessage(), loginUser, receiver); matchRepository.save(match); + notificationService.createNotification(receiver, NotificationType.NEW_MATCH); return MatchResponseDto.toDto(match); } @@ -77,6 +81,14 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { throw new ApiException(ErrorCode.FORBIDDEN); } // 로그인한 유저가 매칭의 받는 사람이 아닐때 예외처리 + if (dto.getStatus() == MatchStatus.ACCEPTED) { + notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED); + } // 매칭 보낸 사람에게 매칭이 수락되었다는 알림 전송 + + if (dto.getStatus() == MatchStatus.REJECTED) { + notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED); + } // 매칭 보낸 사람에게 매칭이 거절되었다는 알림 전송 + findMatch.updateStatus(dto.getStatus()); } diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index 1a95bf8..0138321 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -2,6 +2,8 @@ import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyRequestDto; import com.example.gamemate.domain.reply.dto.ReplyResponseDto; import com.example.gamemate.domain.reply.entity.Reply; @@ -21,6 +23,7 @@ public class ReplyService { private final ReplyRepository replyRepository; private final CommentRepository commentRepository; + private final NotificationService notificationService; /** * 대댓글 생성 메서드 @@ -39,6 +42,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest if(requestDto.getParentReplyId()==null){ newReply = new Reply(requestDto.getContent(), findComment, loginUser); Reply createReply = replyRepository.save(newReply); + createCommentNotification(findComment.getBoard().getUser(), findComment.getUser()); return new ReplyResponseDto( createReply.getReplyId(), @@ -51,8 +55,9 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest //대댓글 조회 Reply findParentReply = replyRepository.findById(requestDto.getParentReplyId()) .orElseThrow(()-> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); - newReply = new Reply(requestDto.getContent(), findComment,loginUser, findParentReply); + newReply = new Reply(requestDto.getContent(), findComment, loginUser, findParentReply); Reply createReply = replyRepository.save(newReply); + createCommentNotification(findComment.getBoard().getUser(), findComment.getUser(), findParentReply.getUser()); return new ReplyResponseDto( createReply.getReplyId(), @@ -102,4 +107,17 @@ public void deleteReply(User loginUser, Long id) { replyRepository.delete(findReply); } + + // 대댓글 알림 전송 + private void createCommentNotification(User board, User comment) { + notificationService.createNotification(board, NotificationType.NEW_COMMENT); + notificationService.createNotification(comment, NotificationType.NEW_COMMENT); + } + + // 대댓글 알림 전송 + private void createCommentNotification(User board, User comment, User reply) { + notificationService.createNotification(board, NotificationType.NEW_COMMENT); + notificationService.createNotification(comment, NotificationType.NEW_COMMENT); + notificationService.createNotification(reply, NotificationType.NEW_COMMENT); + } } From c6595621a87df391bfe073dabf29d07bb423b0cc Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 17 Jan 2025 14:49:26 +0900 Subject: [PATCH 115/215] =?UTF-8?q?refactor=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매 3분 -> 매 10분 으로 간격 조정 --- .../domain/notification/service/NotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 2faca2d..b8cb3ae 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -44,7 +44,7 @@ public List findAllNotification() { } // 알림 발송 (이메일) - @Scheduled(cron = "0 0/3 * * * *") + @Scheduled(cron = "0 0/10 * * * *") public void scheduleNotificationEmail() { log.info("스케쥴링 활성화"); From 3a314ba0f9fbd19ba61917881ebcac0b7cf1b5c0 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 17 Jan 2025 14:53:52 +0900 Subject: [PATCH 116/215] =?UTF-8?q?docs=20:=20yml=20=ED=8C=8C=EC=9D=BC=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 중복되는 구글 이메일 주소과 앱 비밀번호 수정 --- src/main/resources/application.yml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a218596..4aae4e3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,16 +8,4 @@ cloud: region: static: ap-northeast-2 stack: - auto: false - -spring: - mail: - host: smtp.gmail.com - port: 587 - username: ${GMAIL_ACCOUNT} - password: ${APP_PASSWORD} - properties: - mail.smtp.debug: true - mail.smtp.connectionTimeout: 1000 #1초 - mail.starttls.enable: true - mail.smtp.auth: true + auto: false \ No newline at end of file From 9184847488288681090e372cd72403601446b694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Fri, 17 Jan 2025 14:38:24 +0900 Subject: [PATCH 117/215] =?UTF-8?q?feat:=20access=20token=20=EB=B8=94?= =?UTF-8?q?=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 15 ++++++++ .../domain/auth/service/TokenService.java | 38 +++++++++++++++++++ .../filter/JwtAuthenticationFilter.java | 4 +- .../global/provider/JwtTokenProvider.java | 4 ++ src/main/resources/application.properties | 29 ++++++++++++-- 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index e3ff377..73b1b47 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -13,6 +13,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + import java.util.Optional; @Service @@ -85,6 +87,11 @@ public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { } public void logout(User user, HttpServletRequest request, HttpServletResponse response) { + String accessToken = extractToken(request); + if(accessToken != null) { + tokenService.blacklistToken(accessToken); + } + String refreshToken = tokenService.extractRefreshTokenFromCookie(request); if(refreshToken != null) { user.removeRefreshToken(); @@ -93,4 +100,12 @@ public void logout(User user, HttpServletRequest request, HttpServletResponse re } } + private String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java index 1ac17d1..78e8db5 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -11,6 +11,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + @Service @RequiredArgsConstructor @Transactional @@ -19,6 +23,10 @@ public class TokenService { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; + // 블랙리스트 저장 + private final Set blacklist = new ConcurrentHashMap().newKeySet(); + private final Map tokenExpirations = new ConcurrentHashMap<>(); + public EmailLoginResponseDto generateLoginTokens(User user, HttpServletResponse response) { String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); @@ -58,4 +66,34 @@ public void removeRefreshTokenCookie(HttpServletResponse response) { response.addCookie(cookie); } + public void blacklistToken(String token) { + long expirationTime = jwtTokenProvider.getExpirationFromToken(token); + blacklist.add(token); + tokenExpirations.put(token, expirationTime); + removeExpiredTokens(); + } + + public boolean isBlacklisted(String token) { + removeExpiredTokens(); + return blacklist.contains(token); + } + + public boolean validateToken(String token) { + if (isBlacklisted(token)) { + return false; + } + return jwtTokenProvider.validateToken(token); + } + + private void removeExpiredTokens() { + long currentTime = System.currentTimeMillis(); + tokenExpirations.entrySet().removeIf(entry -> { + if (entry.getValue() < currentTime) { + blacklist.remove(entry.getKey()); + return true; + } + return false; + }); + } + } diff --git a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java index 57abed7..14e821a 100644 --- a/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/gamemate/global/filter/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.example.gamemate.global.filter; import ch.qos.logback.core.util.StringUtil; +import com.example.gamemate.domain.auth.service.TokenService; import com.example.gamemate.global.config.auth.CustomUserDetails; import com.example.gamemate.global.provider.JwtTokenProvider; import jakarta.servlet.FilterChain; @@ -34,6 +35,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserDetailsService userDetailsService; + private final TokenService tokenService; @Override @@ -53,7 +55,7 @@ protected void doFilterInternal(HttpServletRequest request, String token = extractToken(request); // 토큰이 유효한 경우에만 인증 처리 - if (token != null && jwtTokenProvider.validateToken(token)) { + if (token != null && tokenService.validateToken(token)) { String email = jwtTokenProvider.getEmailFromToken(token); Authentication authentication = createAuthentication(email); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java index 26b3a86..9bfdf2c 100644 --- a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -71,6 +71,10 @@ public String getEmailFromToken(String token) { return getTokenClaims(token).getSubject(); } + public long getExpirationFromToken(String token) { + return getTokenClaims(token).getExpiration().getTime(); + } + private Key generateSigningKey() { return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ace2608..0b5c64b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,7 +22,14 @@ spring.jwt.secret=${JWT_SECRET} # Google OAuth2 spring.security.oauth2.client.registration.google.client-id=${OAUTH2_GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${OAUTH2_GOOGLE_CLIENT_SECRET} -spring.security.oauth2.client.registration.google.scope=email,profile +spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://openidconnect.googleapis.com/v1/userinfo +spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs +spring.security.oauth2.client.provider.google.user-name-attribute=sub # Kakao OAuth2 spring.security.oauth2.client.registration.kakao.client-id=${OAUTH2_KAKAO_CLIENT_ID} @@ -31,7 +38,7 @@ spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8 spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email spring.security.oauth2.client.registration.kakao.client-name=Kakao -spring.security.oauth2.client.registration.kakao.client-authentication-method=POST +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post # Kakao Provider spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize @@ -39,7 +46,9 @@ spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/o spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me spring.security.oauth2.client.provider.kakao.user-name-attribute=id -oauth2.redirect.uri=http://localhost:8080/oauth2/redirect +#oauth2.redirect.uri=http://localhost:3000/oauth2/callback +oauth2.success.redirect.uri=http://localhost:8080/oauth2-login-success.html +oauth2.failure.redirect.uri=http://localhost:8080/oauth2-login-failure.html # EMAIL spring.mail.host=smtp.gmail.com @@ -48,4 +57,16 @@ spring.mail.username=${EMAIL_USERNAME} spring.mail.password=${EMAIL_APP_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.timeout=5000 -spring.mail.properties.mail.smtp.starttls.enable=true \ No newline at end of file +spring.mail.properties.mail.smtp.starttls.enable=true + +# DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.servlet=DEBUG +logging.level.org.springframework.security.oauth2=TRACE +logging.level.org.springframework.web.client=TRACE +logging.level.com.example.gamemate=DEBUG + +# Gemini +gemini.api.url=${GEMINI_URL} +gemini.api.key=${GEMINI_KEY} \ No newline at end of file From 34ea92a2687455f1e537d08ff40bf9eb2549c828 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 17 Jan 2025 15:01:39 +0900 Subject: [PATCH 118/215] =?UTF-8?q?refactor=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/controller/NotificationController.java | 8 ++++++-- .../domain/notification/service/NotificationService.java | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index dd50213..6f1999e 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -2,9 +2,11 @@ import com.example.gamemate.domain.notification.dto.NotificationResponseDto; import com.example.gamemate.domain.notification.service.NotificationService; +import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -22,9 +24,11 @@ public class NotificationController { * @return NotificationResponseDtoList */ @GetMapping - public ResponseEntity> findAllNotification() { + public ResponseEntity> findAllNotification( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { - List NotificationResponseDtoList = notificationService.findAllNotification(); + List NotificationResponseDtoList = notificationService.findAllNotification(customUserDetails.getUser()); return new ResponseEntity<>(NotificationResponseDtoList, HttpStatus.OK); } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index b8cb3ae..4f29821 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -32,10 +32,9 @@ public void createNotification(User user, NotificationType type) { } // 알림 전체 보기 - // todo 현재 로그인이 구현되어 있지 않아 1번 유저의 알림 목록을 불러오게 설정, 추후 로그인 구현시 로그인한 유저의 id값을 넣도록 변경 - public List findAllNotification() { + public List findAllNotification(User loginUser) { - List notificationList = notificationRepository.findAllByUserId(1L); + List notificationList = notificationRepository.findAllByUserId(loginUser.getId()); return notificationList .stream() From aaad4234a5ffca3debe687af46968dc036edf953 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 17 Jan 2025 16:33:31 +0900 Subject: [PATCH 119/215] =?UTF-8?q?refactor=20:=20=ED=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20NotificationController=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/controller/NotificationController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index 6f1999e..bcac031 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -28,7 +28,7 @@ public ResponseEntity> findAllNotification( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - List NotificationResponseDtoList = notificationService.findAllNotification(customUserDetails.getUser()); - return new ResponseEntity<>(NotificationResponseDtoList, HttpStatus.OK); + List notificationResponseDtoList = notificationService.findAllNotification(customUserDetails.getUser()); + return new ResponseEntity<>(notificationResponseDtoList, HttpStatus.OK); } } From c544c8b49037983532d7f9d82275c2bed874dfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 15 Jan 2025 14:05:16 +0900 Subject: [PATCH 120/215] =?UTF-8?q?fix:=20OAuth2=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/global/config/auth/OAuth2FailureHandler.java | 2 +- .../gamemate/global/config/auth/OAuth2SuccessHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java index cf2bcda..7f34561 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java @@ -17,7 +17,7 @@ @RequiredArgsConstructor public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler { - @Value("${oauth2.redirect-uri}") + @Value("${oauth2.redirect.uri}") private String redirectUri; @Override diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java index 25b90c1..60989cd 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -69,7 +69,7 @@ private void addRefreshTokenCookie(HttpServletResponse response, String refreshT response.addCookie(cookie); } - @Value("${oauth2.redirect-uri}") + @Value("${oauth2.redirect.uri}") private String redirectUri; private String determineTargetUrl(String token) { From 27d7c7a21934eec09134cba20d08fd008fbfc9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Fri, 17 Jan 2025 02:21:15 +0900 Subject: [PATCH 121/215] =?UTF-8?q?feat:=20Oauth2.0=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - html 추가 및 적용 - AuthProvider 예외처리 추가 - OAuth2Service processOAuth2USer 메소드 개선 --- .../domain/auth/dto/OAuthUrlResponseDto.java | 12 ++ .../domain/auth/service/AuthService.java | 1 + .../domain/auth/service/OAuth2Service.java | 106 ++++++++-- .../domain/auth/service/TokenService.java | 8 +- .../gamemate/domain/user/entity/User.java | 2 + .../domain/user/enums/AuthProvider.java | 17 +- .../gamemate/domain/user/enums/Role.java | 2 +- .../domain/user/enums/UserStatus.java | 2 +- .../global/config/SecurityConfig.java | 18 +- .../config/auth/CustomOAuth2UserService.java | 3 +- .../config/auth/OAuth2FailureHandler.java | 14 +- .../config/auth/OAuth2SuccessHandler.java | 34 ++-- src/main/resources/application.properties | 25 ++- src/main/resources/static/favicon.ico | Bin 0 -> 318 bytes .../static/oauth2-login-failure.html | 23 +++ .../static/oauth2-login-success.html | 11 ++ src/main/resources/static/oauth2-login.html | 186 ++++++++++++++++++ 17 files changed, 404 insertions(+), 60 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/OAuthUrlResponseDto.java create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/main/resources/static/oauth2-login-failure.html create mode 100644 src/main/resources/static/oauth2-login-success.html create mode 100644 src/main/resources/static/oauth2-login.html diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/OAuthUrlResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/OAuthUrlResponseDto.java new file mode 100644 index 0000000..791be67 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/OAuthUrlResponseDto.java @@ -0,0 +1,12 @@ +package com.example.gamemate.domain.auth.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OAuthUrlResponseDto { + + private final String authorizationUrl; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index e3ff377..d5041f8 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -29,6 +29,7 @@ public class AuthService { public SignupResponseDto signup(SignupRequestDto requestDto) { // 기존 사용자 중복 체크 Optional findUser = userRepository.findByEmail(requestDto.getEmail()); + if(findUser.isPresent()) { if(findUser.get().getUserStatus() == UserStatus.WITHDRAW) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); diff --git a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java index 688597e..9db4ab0 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java @@ -8,17 +8,27 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriComponentsBuilder; import java.util.Map; +import java.util.Optional; +import java.util.UUID; +@Slf4j @Service @RequiredArgsConstructor @Transactional public class OAuth2Service { private final UserRepository userRepository; + private final OAuth2ClientProperties clientProperties; public OAuth2LoginResponseDto extractOAuth2Attributes(AuthProvider provider, Map attributes) { if(provider == AuthProvider.GOOGLE) { @@ -29,28 +39,86 @@ public OAuth2LoginResponseDto extractOAuth2Attributes(AuthProvider provider, Map throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); } - public User registerOAuth2User(OAuth2LoginResponseDto responseDto) { - User findUser = userRepository.findByEmail(responseDto.getEmail()) - .orElseGet(() -> { - User newUser = new User( - responseDto.getEmail(), - responseDto.getName(), - responseDto.getName(), - responseDto.getProvider(), - responseDto.getProviderId() - ); - return userRepository.save(newUser); - }); - - if (findUser.getUserStatus() == UserStatus.WITHDRAW) { - throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); - } - if (!findUser.getProvider().equals(responseDto.getProvider())) { - throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + public User processOAuth2User(OAuth2LoginResponseDto responseDto) { + // 기존 사용자 조회 + Optional findUser = userRepository.findByEmail(responseDto.getEmail()); + + // 기존 사용자 존재하는 경우 + if (findUser.isPresent()) { + User existingUser = findUser.get(); + + // 탈퇴한 사용자 체크 + if (existingUser.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); + } + + // 다른 OAuth 제공자로 로그인 시도한 경우 + if (!existingUser.getProvider().equals(responseDto.getProvider())) { + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + + return existingUser; } - return findUser; + + // 새로운 사용자 생성 + User newUser = new User( + responseDto.getEmail(), + responseDto.getName(), + responseDto.getName(), + responseDto.getProvider(), + responseDto.getProviderId() + ); + return userRepository.save(newUser); } +// public String generateAuthorizationUrl(String providerName, String redirectUri) { +// +// AuthProvider provider = AuthProvider.fromString(providerName); +// ClientRegistration clientRegistration = getClientRegistration(provider); +// String state = UUID.randomUUID().toString(); +// log.info("Generated OAuth2 State for provider {}: {}", providerName, state); +// +// String authorizationUrl = UriComponentsBuilder +// .fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri()) +// .queryParam("client_id", clientRegistration.getClientId()) +// .queryParam("redirect_uri", redirectUri != null ? redirectUri : clientRegistration.getRedirectUri()) +// .queryParam("response_type", "code") +// .queryParam("scope", String.join(" ", clientRegistration.getScopes())) +// .queryParam("state", state) +// .build() +// .toUriString(); +// log.info("Generated Authorization URL for provider {}: {}", providerName, authorizationUrl); +// return authorizationUrl; +// } + +// private ClientRegistration getClientRegistration(AuthProvider provider) { +// String registrationId = provider.name().toLowerCase(); +// +// OAuth2ClientProperties.Registration registration = +// clientProperties.getRegistration().get(registrationId); +// OAuth2ClientProperties.Provider providerConfig = +// clientProperties.getProvider().get(registrationId); +// +// if (registration == null || providerConfig == null) { +// throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); +// } +// +// return ClientRegistration.withRegistrationId(registrationId) +// .clientId(registration.getClientId()) +// .clientSecret(registration.getClientSecret()) +// .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) +// .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) +// .redirectUri(registration.getRedirectUri()) +// .scope(registration.getScope()) +// .authorizationUri(providerConfig.getAuthorizationUri()) +// .tokenUri(providerConfig.getTokenUri()) +// .userInfoUri(providerConfig.getUserInfoUri()) +// .userNameAttributeName(providerConfig.getUserNameAttribute()) +// .clientName(registrationId) +// .build(); +// } + + private OAuth2LoginResponseDto extractGoogleAttributes(Map attributes) { return new OAuth2LoginResponseDto( getSafeString(attributes.get("sub")), diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java index 1ac17d1..7ab3af3 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -44,11 +44,11 @@ public String extractRefreshTokenFromCookie(HttpServletRequest request) { private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { Cookie cookie = new Cookie("refresh_token", refreshToken); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setPath("/"); + cookie.setHttpOnly(true); // 자바 스크립트에서 접근 불가 + cookie.setSecure(true); // HTTPS에서만 동작 + cookie.setPath("/"); // 모든 경로에서 유효 cookie.setMaxAge(3 * 24 * 60 * 60); // 3일 - response.addCookie(cookie); + response.addCookie(cookie); // 쿠키를 응답에 추가 } public void removeRefreshTokenCookie(HttpServletResponse response) { diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index f1a1270..f7fb2d9 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -31,6 +31,7 @@ public class User extends BaseEntity { @Column(nullable = false) private String nickname; + @Column(nullable = false) private String password; @Enumerated(EnumType.STRING) @@ -73,6 +74,7 @@ public User(String email, String name, String nickname, AuthProvider provider, S this.email = email; this.name = name; this.nickname = nickname; + this.password = "OAUTH2_USER"; this.provider = provider; this.providerId = providerId; this.role = Role.USER; diff --git a/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java index c0b338e..acb0e39 100644 --- a/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java +++ b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java @@ -1,18 +1,29 @@ package com.example.gamemate.domain.user.enums; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; import lombok.Getter; @Getter public enum AuthProvider { EMAIL("email"), - GOOGLE("user"), - KAKAO("admin"); + GOOGLE("google"), + KAKAO("kakao"); - private String name; + private final String name; AuthProvider(String name) { this.name = name; } + public static AuthProvider fromString(String provider) { + for (AuthProvider authProvider : values()) { + if (authProvider.getName().equalsIgnoreCase(provider)) { + return authProvider; + } + } + throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); + } + } diff --git a/src/main/java/com/example/gamemate/domain/user/enums/Role.java b/src/main/java/com/example/gamemate/domain/user/enums/Role.java index e7a9b5b..33d72f0 100644 --- a/src/main/java/com/example/gamemate/domain/user/enums/Role.java +++ b/src/main/java/com/example/gamemate/domain/user/enums/Role.java @@ -8,7 +8,7 @@ public enum Role { USER("user"), ADMIN("admin"); - private String name; + private final String name; Role(String name) { this.name = name; diff --git a/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java b/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java index bda43ce..a02238e 100644 --- a/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java +++ b/src/main/java/com/example/gamemate/domain/user/enums/UserStatus.java @@ -8,7 +8,7 @@ public enum UserStatus { ACTIVE("active"), WITHDRAW("withdraw"); - private String name; + private final String name; UserStatus(String name) { this.name = name; diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index e924722..be32b4e 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -17,6 +17,9 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -44,13 +47,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/v3/api-docs/**", "/swagger-resources/**" ,"/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/auth/signup", "/auth/login", "auth/refresh", "auth/email/**").permitAll() - .requestMatchers("/oauth2/**", "/login/oauth2/code/**").permitAll() + .requestMatchers("/auth/signup", "/auth/login", "/auth/refresh", "/auth/email/**").permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/**", "/auth/oauth2/**").permitAll() + .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html").permitAll() //Todo 관리자 접근 가능 url 수정 .requestMatchers("/관리자관련url").hasRole("admin") .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(endpoint -> endpoint + .baseUri("/oauth2/authorization") + .authorizationRequestRepository(authorizationRequestRepository())) + .redirectionEndpoint(endpoint -> endpoint + .baseUri("/login/oauth2/code/*")) .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService)) .successHandler(oAuth2SuccessHandler) @@ -85,4 +94,9 @@ public RoleHierarchy roleHierarchy() { return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_USER"); } + @Bean + public AuthorizationRequestRepository authorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } + } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java index 78d5f66..19e92e0 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java +++ b/src/main/java/com/example/gamemate/global/config/auth/CustomOAuth2UserService.java @@ -1,7 +1,6 @@ package com.example.gamemate.global.config.auth; import com.example.gamemate.domain.auth.dto.OAuth2LoginResponseDto; -import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.auth.service.OAuth2Service; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.AuthProvider; @@ -32,7 +31,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic oauth2User.getAttributes() ); - User user = oAuth2Service.registerOAuth2User(attributes); + User user = oAuth2Service.processOAuth2User(attributes); return new CustomUserDetails(user, oauth2User.getAttributes()); diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java index 7f34561..3e16350 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java @@ -8,16 +8,18 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +@Slf4j @Component @RequiredArgsConstructor public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler { - @Value("${oauth2.redirect.uri}") + @Value("${oauth2.failure.redirect.uri}") private String redirectUri; @Override @@ -26,10 +28,16 @@ public void onAuthenticationFailure( HttpServletResponse response, AuthenticationException exception) throws IOException { + // 오류 메세지 가져오기 + String errorMessage = exception.getMessage(); + String encodedError = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); + + // 실패 리다이렉트 URL 생성 String targetUrl = UriComponentsBuilder.fromUriString(redirectUri) - .queryParam("error", exception.getMessage()) + .queryParam("error", encodedError) .build().toUriString(); + // 리다이렉트 getRedirectStrategy().sendRedirect(request, response, targetUrl); } } diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java index 4e9f113..3315dba 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -1,32 +1,32 @@ package com.example.gamemate.global.config.auth; -import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; -import com.example.gamemate.domain.user.service.UserService; import com.example.gamemate.global.provider.JwtTokenProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +@Slf4j @Component @RequiredArgsConstructor public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; private final UserRepository userRepository; - private int refreshTokenMaxAge = 1000 * 60 * 60 * 24 * 3; //3일 + private int refreshTokenMaxAge = 60 * 60 * 24 * 3; //3일 - private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token"; + @Value("${oauth2.success.redirect.uri}") + private String redirectUri; @Override public void onAuthenticationSuccess( @@ -34,6 +34,7 @@ public void onAuthenticationSuccess( HttpServletResponse response, Authentication authentication) throws IOException { + log.info("OAuth2 로그인 성공 처리 시작"); CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); User user = userDetails.getUser(); @@ -47,32 +48,23 @@ public void onAuthenticationSuccess( // 쿠키에 Refresh 토큰 저장 addRefreshTokenCookie(response, refreshToken); - // Access 토큰과 함께 리다이렉트 - getRedirectStrategy().sendRedirect( - request, - response, - determineTargetUrl(accessToken) - ); + String targetUrl = UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("token", accessToken) + .build(false).toUriString(); + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } catch (Exception e) { throw new IOException("OAuth2 인증 처리 중 오류가 발생했습니다.", e); } } private void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) { - Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); + Cookie cookie = new Cookie("refresh_token", refreshToken); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); cookie.setMaxAge(refreshTokenMaxAge); response.addCookie(cookie); } - - @Value("${oauth2.redirect-uri}") - private String redirectUri; - - private String determineTargetUrl(String token) { - return UriComponentsBuilder.fromUriString(redirectUri) - .queryParam("token", token) - .build().toUriString(); - } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ace2608..8dc7746 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,7 +22,14 @@ spring.jwt.secret=${JWT_SECRET} # Google OAuth2 spring.security.oauth2.client.registration.google.client-id=${OAUTH2_GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${OAUTH2_GOOGLE_CLIENT_SECRET} -spring.security.oauth2.client.registration.google.scope=email,profile +spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://openidconnect.googleapis.com/v1/userinfo +spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs +spring.security.oauth2.client.provider.google.user-name-attribute=sub # Kakao OAuth2 spring.security.oauth2.client.registration.kakao.client-id=${OAUTH2_KAKAO_CLIENT_ID} @@ -31,7 +38,7 @@ spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8 spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email spring.security.oauth2.client.registration.kakao.client-name=Kakao -spring.security.oauth2.client.registration.kakao.client-authentication-method=POST +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post # Kakao Provider spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize @@ -39,7 +46,9 @@ spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/o spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me spring.security.oauth2.client.provider.kakao.user-name-attribute=id -oauth2.redirect.uri=http://localhost:8080/oauth2/redirect +#oauth2.redirect.uri=http://localhost:3000/oauth2/callback +oauth2.success.redirect.uri=http://localhost:8080/oauth2-login-success.html +oauth2.failure.redirect.uri=http://localhost:8080/oauth2-login-failure.html # EMAIL spring.mail.host=smtp.gmail.com @@ -48,4 +57,12 @@ spring.mail.username=${EMAIL_USERNAME} spring.mail.password=${EMAIL_APP_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.timeout=5000 -spring.mail.properties.mail.smtp.starttls.enable=true \ No newline at end of file +spring.mail.properties.mail.smtp.starttls.enable=true + +# DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.servlet=DEBUG +logging.level.org.springframework.security.oauth2=TRACE +logging.level.org.springframework.web.client=TRACE +logging.level.com.example.gamemate=DEBUG diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..caeb40c95cfc2b0766d47a06df846724a7e3f1c7 GIT binary patch literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|4HXaJKC0wf0mfdN4bN=ix$jwx9T z4FCQyOrJJ`;X6-cX< vr6qysj5Y}E2Bp(GfP97^wKSkSemjA(|Nk>EJYXlN46NV)GekW|4-5kUr8X|b literal 0 HcmV?d00001 diff --git a/src/main/resources/static/oauth2-login-failure.html b/src/main/resources/static/oauth2-login-failure.html new file mode 100644 index 0000000..0060944 --- /dev/null +++ b/src/main/resources/static/oauth2-login-failure.html @@ -0,0 +1,23 @@ + + + + OAuth2 Login Failure + + + +

OAuth2 Login Failure

+

Error: Unknown error occurred

+ + \ No newline at end of file diff --git a/src/main/resources/static/oauth2-login-success.html b/src/main/resources/static/oauth2-login-success.html new file mode 100644 index 0000000..5d25cb3 --- /dev/null +++ b/src/main/resources/static/oauth2-login-success.html @@ -0,0 +1,11 @@ + + + + + OAuth2 Login Success + + +

OAuth2 Login Success!

+

테스트를 계속 진행하세요.

+ + \ No newline at end of file diff --git a/src/main/resources/static/oauth2-login.html b/src/main/resources/static/oauth2-login.html new file mode 100644 index 0000000..44a0338 --- /dev/null +++ b/src/main/resources/static/oauth2-login.html @@ -0,0 +1,186 @@ + + + + + + + GM 로그인 페이지 + + + +

Game Mate 로그인

+ + + + + +
+

로그인 성공!

+

환영합니다.

+ +
+ + + + \ No newline at end of file From fcd18d2125191ecd23cae456b4b604b8a14c4f92 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 20 Jan 2025 10:55:13 +0900 Subject: [PATCH 122/215] =?UTF-8?q?build:=20Dockerfile,=20docker-compose.y?= =?UTF-8?q?ml,=20github-actions.yml=20=ED=8C=8C=EC=9D=BC=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 --- .../workflows/docker-multi-stage-build.yml | 89 +++++++++++++++++++ Dockerfile | 29 ++++++ docker-compose.yml | 39 ++++++++ 3 files changed, 157 insertions(+) create mode 100644 .github/workflows/docker-multi-stage-build.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml new file mode 100644 index 0000000..69e4b1b --- /dev/null +++ b/.github/workflows/docker-multi-stage-build.yml @@ -0,0 +1,89 @@ +name: docker multi-stage build + +on: + push: + branches: + - '**' + +jobs: + docker-build-and-push: + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v6 + with: + file: ./Dockerfile + push: true + tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest + + deploy-to-ec2: + needs: docker-build-and-push + runs-on: ubuntu-latest + steps: + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_KEY }} + script: | + # 기존 컨테이너 정리 + echo "Stopping and removing existing containers..." + sudo docker-compose down || true + + # docker-compose.yml 파일 생성 + echo "Creating docker-compose.yml..." + cat < docker-compose.yml + services: + app: + image: "${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest" + platform: linux/amd64 + container_name: app + ports: + - "8080:8080" + environment: + MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" + MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" + MYSQL_URL: "${{ secrets.MYSQL_URL }}" + JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" + JWT_SECRET: "${{ secrets.JWT_SECRET }}" + YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" + YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" + YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" + OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" + OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" + OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" + OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" + EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" + EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" + GEMINI_URL: "${{ secrets.GEMINI_URL }}" + GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" + depends_on: + - db + + db: + image: mysql:8.0 + container_name: db + environment: + MYSQL_ROOT_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" + volumes: + - db-data:/var/lib/mysql + ports: + - "3306:3306" + + volumes: + db-data: + EOF + + # docker-compose 실행 + echo "Starting services with docker-compose..." + sudo docker-compose up -d diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..893185b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build 스테이지 +FROM gradle:8.10.2-jdk17 AS builder + +# 작업 디렉토리 설정 +WORKDIR /apps + +# 빌더 이미지에서 애플리케이션 빌드 +COPY . /apps +#RUN gradle clean build --no-daemon --parallel +RUN gradle clean build -x test --no-daemon --parallel + +# 실행 스테이지 +# OpenJDK 17 slim 기반 이미지 사용 +FROM openjdk:17-jdk-slim + +# 이미지에 레이블 추가 +LABEL type="application" + +# 작업 디렉토리 설정 +WORKDIR /apps + +# 애플리케이션 jar 파일을 컨테이너로 복사 +COPY --from=builder /apps/build/libs/*-SNAPSHOT.jar /apps/app.jar + +# 애플리케이션이 사용할 포트 노출 +EXPOSE 8080 + +# 애플리케이션을 실행하기 위한 엔트리포인트 정의 +ENTRYPOINT ["java", "-jar", "/apps/app.jar"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4dbb5db --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + app: + image: "${DOCKERHUB_USERNAME}/${DOCKER_IMAGE_TAG_NAME}:latest" + platform: linux/amd64 + container_name: app + ports: + - "8080:8080" + environment: + MYSQL_USERNAME: "${MYSQL_USERNAME}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD}" + MYSQL_URL: "${MYSQL_URL}" + JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" + JWT_SECRET: "${JWT_SECRET}" + YOUR_ACCESS_KEY: "${YOUR_ACCESS_KEY}" + YOUR_SECRET_KEY: "${YOUR_SECRET_KEY}" + YOUR_BUCKET_NAME: "${YOUR_BUCKET_NAME}" + OAUTH2_GOOGLE_CLIENT_ID: "${OAUTH2_GOOGLE_CLIENT_ID}" + OAUTH2_GOOGLE_CLIENT_SECRET: "${OAUTH2_GOOGLE_CLIENT_SECRET}" + OAUTH2_KAKAO_CLIENT_ID : "${OAUTH2_KAKAO_CLIENT_ID}" + OAUTH2_KAKAO_CLIENT_SECRET: "${OAUTH2_KAKAO_CLIENT_SECRET}" + EMAIL_USERNAME: "${EMAIL_USERNAME}" + EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" + GEMINI_URL: "${ secrets.GEMINI_URL }" + GEMINI_KEY: "${ secrets.GEMINI_KEY }" + depends_on: + - db + + db: + image: mysql:8.0 + container_name: db + environment: + MYSQL_ROOT_PASSWORD: "${MYSQL_PASSWORD}" + volumes: + - db-data:/var/lib/mysql + ports: + - "3306:3306" + +volumes: + db-data: From ef7aa1af04f177d1aaeb98dc529359149325401f Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 20 Jan 2025 15:14:27 +0900 Subject: [PATCH 123/215] =?UTF-8?q?docs:=20README.md=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. README.md 업데이트 --- README.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ad52b09..d645d3b 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# GameMate \ No newline at end of file + +
+ +# 👀 ![image](https://github.com/user-attachments/assets/e3bfae8c-8698-4b6f-ab7a-5fe5d3ccd9e8) 👀 + +### 다양한 게임 정보와 리뷰를 공유하고 게임을 같이 할 친구를 찾을 수 있는 커뮤니티 사이트 +
+ +## 👨‍👩‍👧‍👦 Our Team + +|이예지|전수연|고강혁|양제훈| +|:---:|:---:|:---:|:---:| +|BE|BE|BE|BE| + +
+ +## 프로젝트 기능 + +### 🛡 OAuth2 소셜로그인 (kakao, google) + +> * Kakao와 google 통한 간편 로그인이 가능합니다. + +### 📧 이메일 인증 + +> * 이메일 인증 기능을 지원합니다. + + +### 👥 게임 매칭 시스템 제공 + +> * 원하는 게임에 대해 매칭 시스템을 통해 친구를 구할 수 있습니다. +> * 매칭이 완료되면 스케줄링을 통해 일정 시간 후 알림을 제공합니다. + + +### 🎮 게임 정보 확인 + +> * 다양한 게임에 대한 정보를 얻을 수 있고 리뷰를 확인 할 수 있습니다. + + +### ❗️ 게임 추천 + +> * 내가 원하는 성향을 가진 게임을 추천하는 서비스를 제공합니다. +> * 1. Gemini API 사용 + +### 📖 게임 커뮤니티 + +> * 게임에 대한 게시글을 작성하고, 게시글에 댓글 대댓글을 작성할 수 있습니다. + + +

+ + +## 적용 기술 + + + +

+ +## 🚨 Trouble Shooting + + +

+ + +## [📋 ERD Diagram] +## ![📋 ERD Diagram](https://github.com/user-attachments/assets/90506e5f-ecbc-4a9c-b748-02767a68140d) + + + +
+ +## 📝 Technologies & Tools 📝 + + + + + + + + + + + +



From f7b65809d89c991a6ef25a03bd889373871bb170 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 21 Jan 2025 11:03:17 +0900 Subject: [PATCH 124/215] =?UTF-8?q?docs:=20README.md=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 아키텍처 플로우 추가 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d645d3b..77c653f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ### ❗️ 게임 추천 > * 내가 원하는 성향을 가진 게임을 추천하는 서비스를 제공합니다. -> * 1. Gemini API 사용 +> * Gemini API 사용 ### 📖 게임 커뮤니티 @@ -65,6 +65,13 @@ ## ![📋 ERD Diagram](https://github.com/user-attachments/assets/90506e5f-ecbc-4a9c-b748-02767a68140d) +
+ +## 🌐 Architecture + +![image](https://github.com/user-attachments/assets/bbb0be82-e50b-4d66-800f-bdd7ebc01266) + +
From 775d7a66cbc096399fffb7a0ae6028f34925de12 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 21 Jan 2025 11:20:59 +0900 Subject: [PATCH 125/215] =?UTF-8?q?docs:=20README.md=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. WBS 캡쳐본 업로드 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 77c653f..4524e7b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ ![image](https://github.com/user-attachments/assets/bbb0be82-e50b-4d66-800f-bdd7ebc01266) +## 📆 일정 관리 (WBS) +![image](https://github.com/user-attachments/assets/3cbe94f2-3236-470e-8e43-31ceacb65367) +
From d2b7ae7b0e5334dc85b3857adcf812fda4378ead Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 21 Jan 2025 18:02:00 +0900 Subject: [PATCH 126/215] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=EC=9D=B4?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=EC=97=90=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/controller/NotificationController.java | 6 ++++-- .../notification/service/AsyncNotificationService.java | 1 + src/main/resources/application.yml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index bcac031..0d98d8f 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -20,8 +20,10 @@ public class NotificationController { private final NotificationService notificationService; /** - * 알림 전체 보기 - * @return NotificationResponseDtoList + * 로그인한 사용자의 전체 알림을 조회합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 로그인한 사용자의 전체 알림을 담은 ResponseEntity */ @GetMapping public ResponseEntity> findAllNotification( diff --git a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java index 6342f6c..7b44ff1 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java @@ -22,6 +22,7 @@ public class AsyncNotificationService { private final JavaMailSender javaMailSender; private final NotificationRepository notificationRepository; + // 알림 메일 전송 @Async public void sendNotificationMail(User user, List notifications) { try { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4aae4e3..7601d74 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,4 +8,4 @@ cloud: region: static: ap-northeast-2 stack: - auto: false \ No newline at end of file + auto: false From 88f55065bc50c51ff6ac8c09c457809ca8a867af Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 21 Jan 2025 18:19:36 +0900 Subject: [PATCH 127/215] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=EC=9D=B4?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=EC=97=90=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../follow/controller/FollowController.java | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java index 4bd9af2..c1cd1f3 100644 --- a/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java +++ b/src/main/java/com/example/gamemate/domain/follow/controller/FollowController.java @@ -11,6 +11,9 @@ import java.util.List; +/** + * 팔로우 기능을 처리하는 컨트롤러 클래스입니다. + */ @RestController @RequiredArgsConstructor @RequestMapping("/follows") @@ -19,9 +22,11 @@ public class FollowController { private final FollowService followService; /** - * 팔로우 하기 - * @param dto FollowCreateRequestDto - * @return followResponseDto + * 사용자간의 팔로우를 생성을 처리합니다. + * + * @param dto FollowCreateRequestDto 팔로우할 상대방의 email + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 팔로우 처리 결과를 담은 ResponseEntity */ @PostMapping public ResponseEntity createFollow( @@ -34,9 +39,11 @@ public ResponseEntity createFollow( } /** - * 팔로우 취소 - * @param id 취소할 팔로우 식별자 - * @return NO_CONTENT + * 사용자간의 팔로우 취소를 처리합니다. + * + * @param id 취소할 팔로우 ID + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT 성공했지만 반환값이 없음 */ @DeleteMapping("/{id}") public ResponseEntity deleteFollow( @@ -49,10 +56,11 @@ public ResponseEntity deleteFollow( } /** - * 팔로우 상태 확인 (loginUser 가 followee 를 팔로우 했는지 확인) - * @param customUserDetails 로그인한 유저 + * 팔로우 상태를 확인합니다. (loginUser 가 followee 를 팔로우 했는지 확인) + * + * @param customUserDetails 현재 인증된 사용자 정보 * @param email 팔로우 상태를 확인할 상대방 이메일 - * @return followBooleanResponseDto + * @return 로그인한 사용자가 상대방을 팔로우 했을시 true, 아닐시 false */ @GetMapping("/status") public ResponseEntity findFollow( @@ -65,9 +73,10 @@ public ResponseEntity findFollow( } /** - * 팔로우 목록 보기 - * @param email 팔로우 목록을 보고 싶은 유저 email - * @return followFindResponseDtoList + * 특정 유저의 팔로워 목록를 조회합니다. + * + * @param email 팔로워 목록을 보고 싶은 유저 email + * @return 특정 유저의 팔로워 목록을 담은 ResponseEntity */ @GetMapping("/followers") public ResponseEntity> findFollowers( @@ -79,9 +88,10 @@ public ResponseEntity> findFollowers( } /** - * 팔로잉 목록 보기 + * 특정 유저의 팔로잉 목록을 조회합니다. + * * @param email 팔로잉 목록을 보고 싶은 유저 email - * @return followFindResponseDtoList + * @return 특정 유저의 팔로잉 목록을 담은 ResponseEntity */ @GetMapping("/following") public ResponseEntity> findFollowing( From 6c38638af3b7b8457c0b53ea9f5fae2d9539795b Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 21 Jan 2025 18:38:46 +0900 Subject: [PATCH 128/215] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=EC=9D=B4?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=EC=97=90=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/controller/MatchController.java | 85 ++++++++++++------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java index 6c007c0..fda01d6 100644 --- a/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java +++ b/src/main/java/com/example/gamemate/domain/match/controller/MatchController.java @@ -12,6 +12,10 @@ import java.util.List; +/** + * 매칭 기능을 처리하는 컨트롤러 클래스입니다. + * 사용자 간의 매칭 기능을 제공합니다. + */ @RestController @RequiredArgsConstructor @RequestMapping("/matches") @@ -19,9 +23,11 @@ public class MatchController { private final MatchService matchService; /** - * 매칭 요청 생성 - * @param dto MatchCreateRequestDto 상대 유저 id, 상대방에게 전할 메세지 - * @return matchCreateResponseDto + * 사용자 간의 매칭 요청을 생성합니다. + * + * @param dto 매칭을 원하는 상대방 ID, 상대방에게 보낼 메세지를 포함합니다. + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 매칭 요청 처리 결과를 담은 ResponseEntity */ @PostMapping public ResponseEntity createMatch( @@ -34,10 +40,12 @@ public ResponseEntity createMatch( } /** - * 매칭 수락/거절하기 - * @param id 매칭 id - * @param dto MatchUpdateRequestDto 수락/거절 - * @return 204 NO CONTENT + * 받은 매칭 요청의 수락/거절을 처리합니다. + * + * @param id 수락/거절할 매칭 요청 ID + * @param dto status (ACCEPTED 수락 / REJECTED 거절) + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT 성공했지만 반환값이 없음 */ @PatchMapping("/{id}") public ResponseEntity updateMatch( @@ -50,9 +58,12 @@ public ResponseEntity updateMatch( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** - * 받은 매칭 전체 조회 - * @return MatchResponseDtoList + * 사용자가 받은 매칭 요청을 조회합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 사용자의 받은 매칭 요청 목록을 담은 ResponseEntity */ @GetMapping("/received-matches") public ResponseEntity> findAllReceivedMatch( @@ -64,8 +75,10 @@ public ResponseEntity> findAllReceivedMatch( } /** - * 보낸 매칭 전체 조회 - * @return MatchResponseDtoList + * 사용자가 보낸 매칭 요청을 조회합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 사용자가 보낸 매칭 요청 목록을 담은 ResponseEntity */ @GetMapping("/sent-matches") public ResponseEntity> findAllSentMatch( @@ -77,9 +90,11 @@ public ResponseEntity> findAllSentMatch( } /** - * 매칭 삭제(취소) - * @param id 매칭 id - * @return NO_CONTENT + * 사용자가 보낸 매칭 요청을 취소합니다. + * + * @param id 취소할 매칭 요청 ID + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT 성공했지만 반환값 없음 */ @DeleteMapping("/{id}") public ResponseEntity deleteMatch( @@ -92,9 +107,11 @@ public ResponseEntity deleteMatch( } /** - * 매칭을 위해 내 정보 입력하기, 매칭 정보 입력시 매칭추천에서 검색됨 - * @param dto MatchInfoCreateRequestDto - * @return matchInfoResponseDto + * 매칭을 위한 정보를 입력합니다. + * + * @param dto 매칭을 위해 자신의 정보를 입력합니다. + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 사용자의 정보가 처리된 ResponseEntity */ @PostMapping("/my-info") public ResponseEntity createMyInfo( @@ -107,8 +124,10 @@ public ResponseEntity createMyInfo( } /** - * 매칭 내정보 조회하기 - * @return MatchInfoResponseDto + * 매칭을 위해 입력한 내 정보를 확인합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 내 정보를 담은 ResponseEntity */ @GetMapping("/my-info") public ResponseEntity findMyInfo( @@ -120,9 +139,11 @@ public ResponseEntity findMyInfo( } /** - * 매칭 상대방 정보 조회하기 - * @param id 매치 id - * @return MatchInfoResponseDto 상대방 정보 + * 매칭 상대방의 입력한 정보를 확인합니다. + * + * @param id 확인할 매칭 요청 ID + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 매칭 요청 ID의 상대방이 입력한 정보를 담은 ResponseEntity */ @GetMapping("/{id}/opponent-info") public ResponseEntity findOpponentInfo( @@ -135,9 +156,11 @@ public ResponseEntity findOpponentInfo( } /** - * 매칭 내정보 수정하기 - * @param dto MatchInfoUpdateRequestDto - * @return 204 NO_CONTENT + * 입력한 내 정보를 수정합니다. + * + * @param dto 수정할 정보를 입력합니다. + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT 성공했지만 반환값 없음 */ @PutMapping("/my-info") public ResponseEntity updateMyInfo( @@ -150,8 +173,10 @@ public ResponseEntity updateMyInfo( } /** - * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않음 - * @return 204 NO_CONTENT + * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않습니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT 성공했지만 반환값 없음 */ @DeleteMapping("/my-info") public ResponseEntity deleteMyInfo( @@ -164,8 +189,10 @@ public ResponseEntity deleteMyInfo( /** * 매칭 추천 받기 - * @param dto MatchSearchConditionDto 매칭 조건 설정 - * @return recommendationList 매칭 로직을 통해 가장 점수가 높은 최대 5명 리스트 + * + * @param dto 원하는 매칭 조건을 설정합니다. + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 원하는 매칭 조건을 토대로 매칭 로직을 통해 가장 점수가 높은 5명을 추천해줍니다. */ @PostMapping("/recommendations") public ResponseEntity> findRecommendation( From 4b8b8b0adf55f1f7050b4c7c244be0e709d26c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 21 Jan 2025 11:43:39 +0900 Subject: [PATCH 129/215] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20=EC=97=86=EC=9D=84=20?= =?UTF-8?q?=EC=8B=9C=20ErrorCode=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/domain/auth/service/EmailService.java | 2 +- .../java/com/example/gamemate/global/constant/ErrorCode.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java index d48a2cd..7ce3372 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/EmailService.java @@ -57,7 +57,7 @@ public boolean verifyEmail(String email, String code) { //인증 정보가 없는 경우 if (verificationInfo == null) { - throw new ApiException(ErrorCode.VERIFICATION_TIME_EXPIRED); + throw new ApiException(ErrorCode.VERIFICATION_NOT_FOUND); } // 인증 정보가 만료된 경우 diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 90a6784..1845935 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -22,6 +22,7 @@ public enum ErrorCode { INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST,"INVALID_PROVIDER_TYPE", "지원하지 않는 서비스 제공자입니다."), INVALID_OAUTH2_ATTRIBUTE(HttpStatus.BAD_REQUEST, "INVALID_OAUTH2_ATTRIBUTE", "인증 정보가 유효하지 않습니다."), VERIFICATION_TIME_EXPIRED(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 시간이 만료되었습니다."), + VERIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 정보를 찾을 수 없습니다."), INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE", "인증 코드가 일치하지 않습니다."), EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "EMAIL_NOT_VERIFIED", "이메일 인증이 필요합니다"), From c50b47f5fd3ddca08d71e241842a190b6671d44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 21 Jan 2025 12:20:42 +0900 Subject: [PATCH 130/215] =?UTF-8?q?fix:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EA=B0=80=EB=8A=A5=20URL=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 --- .../com/example/gamemate/global/config/SecurityConfig.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index be32b4e..02baf47 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.AuthenticationManager; @@ -50,8 +51,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/signup", "/auth/login", "/auth/refresh", "/auth/email/**").permitAll() .requestMatchers("/oauth2/**", "/login/oauth2/**", "/auth/oauth2/**").permitAll() .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html").permitAll() - //Todo 관리자 접근 가능 url 수정 - .requestMatchers("/관리자관련url").hasRole("admin") + .requestMatchers(HttpMethod.POST,"/games/requests").hasRole("USER") + .requestMatchers("/games", "/games/{id}").hasRole("ADMIN") + .requestMatchers("/games/requests/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 From b0306283c6a3d2f1db14947fdb6b9deb7c8fc7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 22 Jan 2025 15:58:15 +0900 Subject: [PATCH 131/215] =?UTF-8?q?chore:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유저(유저 4, 관리자 2) - 유저별 매칭 정보 - 게임 정보 --- src/main/resources/application.properties | 10 +++ src/main/resources/data.sql | 77 +++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/main/resources/data.sql diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 630b42b..8a13ef7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -71,3 +71,13 @@ logging.level.com.example.gamemate=DEBUG gemini.api.url=${GEMINI_URL} gemini.api.key=${GEMINI_KEY} +#S3 +cloud.aws.credentials.access-key=${AWS_ACCESS_KEY} +cloud.aws.credentials.secret-key=${AWS_SECRET_KEY} +cloud.aws.s3.bucket=${AWS_BUCKET} +cloud.aws.region.static=${AWS_REGION} +cloud.aws.stack.auto=${AWS_STACK_AUTO} + +# Dummy data +spring.jpa.defer-datasource-initialization=true +spring.sql.init.mode=always \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..ff514ab --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,77 @@ +-- User 테이블 데이터 +INSERT INTO user ( + email, + name, + nickname, + password, + role, + is_premium, + user_status, + provider +) VALUES + ('user1@test.com', '유저1', '유저닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'EMAIL'), + ('user2@test.com', '유저2', '유저닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'EMAIL'), + ('user3@test.com', '유저3', '유저닉3', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'EMAIL'), + ('user4@test.com', '유저4', '유저닉4', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'EMAIL'), + ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', true, 'ACTIVE', 'EMAIL'), + ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', false, 'ACTIVE', 'EMAIL'); + +-- MatchUserInfo 테이블 데이터 +INSERT INTO match_user_info ( + user_id, + gender, + game_rank, + skill_level, + mic_usage, + message +) VALUES + (1, 'MALE', 'DIAMOND', 5, true, '같이 트롤하실분? 정글러 구함'), + (2, 'FEMALE', 'BRONZE', 1, true, '그냥 즐겜할래요.'), + (3, 'FEMALE', 'GOLD', 3, true, '랭겜 즐기실분 구합니다.'), + (4, 'MALE', 'SILVER', 2, true, '초보 뉴비 환영합니다!'), + (5, 'FEMALE', 'CHALLENGER', 6, false, '빡겜할 듀오 찾습니다.'), + (6, 'MALE', 'PLATINUM', 4, true, '실버 이상만요!'); + +-- user_lanes 테이블 데이터 +INSERT INTO user_lanes (match_user_info_id, lanes) +VALUES + (1, 'TOP'), (1, 'JUNGLE'), + (2, 'MID'), + (3, 'BOTTOM_AD'), (3, 'BOTTOM_SUPPORTER'), + (4, 'TOP'), (4, 'BOTTOM_SUPPORTER'), + (5, 'JUNGLE'), (5, 'MID'), + (6, 'TOP'), (6, 'JUNGLE'); + +-- user_purposes 테이블 데이터 +INSERT INTO user_purposes (match_user_info_id, purposes) +VALUES + (1, 'NORMAL_GAME'), (1, 'JUST_FOR_FUN'), (1, 'TEAMWORK'), + (2, 'RANK_GAME'), (2, 'DUO_PLAY'), + (3, 'MENTORING'), (3, 'BEGINNER_FRIENDLY'), + (4, 'JUST_FOR_FUN'), (4, 'TEAMWORK'), + (5, 'TRY_HARD'), (5, 'RANK_GAME'), + (6, 'NORMAL_GAME'); + +-- user_play_times 테이블 데이터 +INSERT INTO user_play_times (match_user_info_id, play_time_ranges) +VALUES + (1, 'ZERO_TO_SIX'), (1, 'EIGHTEEN_TO_TWENTY_FOUR'), + (2, 'SIX_TO_TWELVE'), + (3, 'TWELVE_TO_EIGHTEEN'), + (4, 'EIGHTEEN_TO_TWENTY_FOUR'), + (5, 'ZERO_TO_SIX'), + (6, 'SIX_TO_TWELVE'), (6, 'EIGHTEEN_TO_TWENTY_FOUR'); + +-- game 테이블 데이터 +INSERT INTO game (title, genre, platform, description) +VALUES + ('라스트 오브 어스', 'Action', 'PlayStation', '종말 이후의 세계를 배경으로 한 스토리 중심의 서바이벌 게임.'), + ('마인크래프트', 'Sandbox', 'PC', '무한히 생성되는 세계에서 블록을 쌓고 모험을 떠나는 게임.'), + ('오버워치', 'Shooter', 'PC', '독특한 능력을 가진 다양한 영웅들이 등장하는 팀 기반 1인칭 슈팅 게임.'), + ('스타듀 밸리', 'Simulation', 'PC', '할아버지의 오래된 농장을 물려받아 경영하는 농장 시뮬레이션 RPG.'), + ('엘든 링', 'RPG', 'PC', '미야자키 히데타카와 조지 R.R. 마틴이 만든 다크 판타지 오픈 월드 액션 RPG.'), + ('피파 23', 'Sports', 'PlayStation', '인기 축구 시뮬레이션 시리즈의 최신작으로, 업데이트된 팀과 향상된 게임플레이 제공.'), + ('콜 오브 듀티: 워존', 'Shooter', 'PC', '콜 오브 듀티 세계관을 배경으로 한 무료 배틀로얄 게임.'), + ('모여봐요 동물의 숲', 'Simulation', 'Nintendo Switch', '무인도에서 자신만의 낙원을 만들어가는 소셜 시뮬레이션 게임.'), + ('포르자 호라이즌 5', 'Racing', 'Xbox', '멕시코의 생동감 넘치고 끊임없이 변화하는 풍경을 배경으로 한 오픈 월드 레이싱 게임.'), + ('할로우 나이트', 'Adventure', 'PC', '광대하고 서로 연결된 세계를 배경으로 한 도전적인 2D 액션 어드벤처 게임.'); \ No newline at end of file From 5f15e969debdc40811cabe55db6eb2ecb420ff1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 22 Jan 2025 17:57:30 +0900 Subject: [PATCH 132/215] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=20=EA=B2=BD=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 --- .../example/gamemate/domain/auth/controller/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index 4a5afb3..f500683 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -32,7 +32,7 @@ public ResponseEntity signup( return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } - @PostMapping("/email") + @PostMapping("/email/verification-request") public ResponseEntity sendVerificationEmail( @Valid @RequestBody EmailVerificationCodeRequestDto requestDto ) { From 6ec551d27d472be384a158e21524ade307954012 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:27:03 +0900 Subject: [PATCH 133/215] =?UTF-8?q?refact:=20=EC=BD=94=ED=8A=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=9B=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?1=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. dto request/response 패키지 구분 2. s3 > application.properties로 통일 3. 좋아요 조회 -> 보드id 없을 때 not found 수정 4. 리뷰등록 -> 게임을 찾을 수 없음으로 수정 5. 게임추천 조회-> 경로에서 userId 삭제\ 6. 파일 확장자(이미지) 체크 로직, 사이즈 크기 최대 5mb > 크키는 추후 수정 예정 7. controller.service 주석 작성 --- .../game/controller/GameController.java | 33 +++++++----- .../GameEnrollRequestController.java | 7 ++- .../controller/GameRecommendContorller.java | 12 ++--- .../game/dto/GameSearchResponseDto.java | 45 ---------------- .../dto/{ => request}/ChatRequestDto.java | 2 +- .../{ => request}/GameCreateRequestDto.java | 2 +- .../GameEnrollRequestCreateRequestDto.java | 2 +- .../GameEnrollRequestUpdateRequestDto.java | 2 +- .../{ => request}/GameUpdateRequestDto.java | 2 +- .../UserGamePreferenceRequestDto.java | 2 +- .../dto/{ => response}/ChatResponseDto.java | 2 +- .../{ => response}/GameCreateResponseDto.java | 4 +- .../GameEnrollRequestResponseDto.java | 2 +- .../GameFindAllResponseDto.java | 2 +- .../GameFindByIdResponseDto.java | 4 +- .../GameRecommendHistorysResponseDto.java | 3 +- .../GameRecommendationResponseDto.java | 3 +- .../{ => response}/GameUpdateResponseDto.java | 2 +- .../UserGamePreferenceResponseDto.java | 2 +- .../game/entity/GameRecommendHistory.java | 2 +- .../GameRecommendHistoryRepository.java | 3 -- .../service/GameEnrollRequestService.java | 26 ++++----- .../game/service/GameRecommendService.java | 37 +++++++------ .../domain/game/service/GameService.java | 54 +++++++++++++------ .../domain/game/service/GeminiService.java | 7 +-- .../like/controller/LikeController.java | 12 ++--- .../BoardLikeCountResponseDto.java | 2 +- .../{ => response}/BoardLikeResponseDto.java | 3 +- .../ReviewLikeCountResponseDto.java | 2 +- .../{ => response}/ReviewLikeResponseDto.java | 2 +- .../domain/like/service/LikeService.java | 14 +++-- .../review/controller/ReviewController.java | 18 ++----- .../{ => request}/ReviewCreateRequestDto.java | 2 +- .../{ => request}/ReviewUpdateRequestDto.java | 2 +- .../ReviewCreateResponseDto.java | 2 +- .../ReviewFindByAllResponseDto.java | 2 +- .../ReviewUpdateResponseDto.java | 2 +- .../domain/review/service/ReviewService.java | 16 +++--- .../gamemate/global/constant/ErrorCode.java | 2 + .../exception/GlobalExceptionHandler.java | 9 ++++ src/main/resources/application.properties | 10 ++++ src/main/resources/application.yml | 11 ---- 42 files changed, 182 insertions(+), 191 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/ChatRequestDto.java (95%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/GameCreateRequestDto.java (89%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/GameEnrollRequestCreateRequestDto.java (89%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/GameEnrollRequestUpdateRequestDto.java (90%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/GameUpdateRequestDto.java (89%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => request}/UserGamePreferenceRequestDto.java (88%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/ChatResponseDto.java (94%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameCreateResponseDto.java (89%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameEnrollRequestResponseDto.java (95%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameFindAllResponseDto.java (96%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameFindByIdResponseDto.java (89%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameRecommendHistorysResponseDto.java (91%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameRecommendationResponseDto.java (79%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/GameUpdateResponseDto.java (92%) rename src/main/java/com/example/gamemate/domain/game/dto/{ => response}/UserGamePreferenceResponseDto.java (94%) rename src/main/java/com/example/gamemate/domain/like/dto/{ => response}/BoardLikeCountResponseDto.java (83%) rename src/main/java/com/example/gamemate/domain/like/dto/{ => response}/BoardLikeResponseDto.java (80%) rename src/main/java/com/example/gamemate/domain/like/dto/{ => response}/ReviewLikeCountResponseDto.java (83%) rename src/main/java/com/example/gamemate/domain/like/dto/{ => response}/ReviewLikeResponseDto.java (88%) rename src/main/java/com/example/gamemate/domain/review/dto/{ => request}/ReviewCreateRequestDto.java (88%) rename src/main/java/com/example/gamemate/domain/review/dto/{ => request}/ReviewUpdateRequestDto.java (88%) rename src/main/java/com/example/gamemate/domain/review/dto/{ => response}/ReviewCreateResponseDto.java (91%) rename src/main/java/com/example/gamemate/domain/review/dto/{ => response}/ReviewFindByAllResponseDto.java (93%) rename src/main/java/com/example/gamemate/domain/review/dto/{ => response}/ReviewUpdateResponseDto.java (91%) diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java index 6425a6c..535d333 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameController.java @@ -1,6 +1,10 @@ package com.example.gamemate.domain.game.controller; -import com.example.gamemate.domain.game.dto.*; +import com.example.gamemate.domain.game.dto.request.GameCreateRequestDto; +import com.example.gamemate.domain.game.dto.request.GameUpdateRequestDto; +import com.example.gamemate.domain.game.dto.response.GameCreateResponseDto; +import com.example.gamemate.domain.game.dto.response.GameFindAllResponseDto; +import com.example.gamemate.domain.game.dto.response.GameFindByIdResponseDto; import com.example.gamemate.domain.game.service.GameService; import com.example.gamemate.global.config.auth.CustomUserDetails; import com.fasterxml.jackson.core.JsonProcessingException; @@ -8,7 +12,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -87,14 +90,10 @@ public ResponseEntity> findAllGame( } /** - * 특정 ID의 게임 정보를 수정합니다. + * 특정 ID의 게임을 조회합니다. * - * @param id 수정할 게임의 ID - * @param gameDataString 수정할 게임 데이터를 포함한 JSON 문자열 - * @param newFile 새로운 게임 이미지 파일 (선택적) - * @param customUserDetails 인증된 사용자 정보 - * @return 수정 결과를 나타내는 ResponseEntity - * @throws RuntimeException JSON 파싱 오류 시 발생 + * @param id 조회할 게임의 ID + * @return 조회된 게임 정보를 포함한 ResponseEntity */ @GetMapping("/{id}") public ResponseEntity findGameById( @@ -106,11 +105,14 @@ public ResponseEntity findGameById( } /** - * 특정 ID의 게임을 삭제합니다. + * 특정 ID의 게임 정보를 수정합니다. * - * @param id 삭제할 게임의 ID + * @param id 수정할 게임의 ID + * @param gameDataString 수정할 게임 데이터를 포함한 JSON 문자열 + * @param newFile 새로운 게임 이미지 파일 (선택적) * @param customUserDetails 인증된 사용자 정보 - * @return 삭제 결과를 나타내는 ResponseEntity + * @return 수정 결과를 나타내는 ResponseEntity + * @throws RuntimeException JSON 파싱 오류 시 발생 */ @PatchMapping("/{id}") public ResponseEntity updateGame( @@ -131,6 +133,13 @@ public ResponseEntity updateGame( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 특정 ID의 게임을 삭제합니다. + * + * @param id 삭제할 게임의 ID + * @param customUserDetails 인증된 사용자 정보 + * @return 삭제 결과를 나타내는 ResponseEntity + */ @DeleteMapping("/{id}") public ResponseEntity deleteGame( @PathVariable Long id, diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java index 8b0b1bc..a321d55 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameEnrollRequestController.java @@ -1,13 +1,12 @@ package com.example.gamemate.domain.game.controller; -import com.example.gamemate.domain.game.dto.GameEnrollRequestCreateRequestDto; -import com.example.gamemate.domain.game.dto.GameEnrollRequestResponseDto; -import com.example.gamemate.domain.game.dto.GameEnrollRequestUpdateRequestDto; +import com.example.gamemate.domain.game.dto.request.GameEnrollRequestCreateRequestDto; +import com.example.gamemate.domain.game.dto.response.GameEnrollRequestResponseDto; +import com.example.gamemate.domain.game.dto.request.GameEnrollRequestUpdateRequestDto; import com.example.gamemate.domain.game.service.GameEnrollRequestService; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java index 564f88c..6d3d99d 100644 --- a/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java +++ b/src/main/java/com/example/gamemate/domain/game/controller/GameRecommendContorller.java @@ -1,8 +1,8 @@ package com.example.gamemate.domain.game.controller; -import com.example.gamemate.domain.game.dto.GameRecommendHistorysResponseDto; -import com.example.gamemate.domain.game.dto.UserGamePreferenceRequestDto; -import com.example.gamemate.domain.game.dto.UserGamePreferenceResponseDto; +import com.example.gamemate.domain.game.dto.response.GameRecommendHistorysResponseDto; +import com.example.gamemate.domain.game.dto.request.UserGamePreferenceRequestDto; +import com.example.gamemate.domain.game.dto.response.UserGamePreferenceResponseDto; import com.example.gamemate.domain.game.service.GameRecommendService; import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; @@ -44,17 +44,15 @@ public ResponseEntity createUserGamePreference( /** * 특정 사용자의 게임 추천 이력을 조회합니다. * - * @param userId 조회할 사용자의 ID * @param customUserDetails 인증된 사용자 정보 * @return 게임 추천 이력 목록을 포함한 ResponseEntity */ - @GetMapping("/{userId}") + @GetMapping public ResponseEntity> getGameRecommendHistories( - @PathVariable Long userId, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - Page responseDto = gameRecommendService.getGameRecommendHistories(userId, customUserDetails.getUser()); + Page responseDto = gameRecommendService.getGameRecommendHistories(customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java deleted file mode 100644 index d8062c1..0000000 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameSearchResponseDto.java +++ /dev/null @@ -1,45 +0,0 @@ -//package com.example.gamemate.domain.game.dto; -// -//import com.example.gamemate.domain.game.entity.Game; -//import com.example.gamemate.domain.review.entity.Review; -//import lombok.Getter; -// -//import java.time.LocalDateTime; -//import java.util.List; -// -//@Getter -//public class GameSearchResponseDto { -// private final Long id; -// private final String title; -// private final String genre; -// private final String platform; -// private final LocalDateTime createdAt; -// private final LocalDateTime modifiedAt; -// private final Long reviewCount; -// private final Double averageStar; -// -// public GameSearchResponseDto(Game game) { -// // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 -// this.id = game.getId(); -// this.title = game.getTitle(); -// this.genre = game.getGenre(); -// this.platform = game.getPlatform(); -// this.createdAt = game.getCreatedAt(); -// this.modifiedAt = game.getModifiedAt(); -// this.reviewCount = (long) game.getReviews().size(); -// this.averageStar = calculateAverageStar(game.getReviews()); -// } -// -// private Double calculateAverageStar(List reviews) { -// if (reviews.isEmpty()) { -// return 0.0; -// } -// double average = reviews.stream() -// .mapToInt(Review::getStar) -// .average() -// .orElse(0.0); -// -// // 소수점 둘째 자리에서 반올림 -// return Math.round(average * 10.0) / 10.0; -// } -//} diff --git a/src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/ChatRequestDto.java similarity index 95% rename from src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/ChatRequestDto.java index 8ca1248..e0df5af 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/ChatRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/ChatRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import lombok.*; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/GameCreateRequestDto.java similarity index 89% rename from src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/GameCreateRequestDto.java index 4921c52..c579fc4 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/GameCreateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestCreateRequestDto.java similarity index 89% rename from src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestCreateRequestDto.java index 0431070..cc09aa5 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestCreateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestUpdateRequestDto.java similarity index 90% rename from src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestUpdateRequestDto.java index 24f75e8..f65e42a 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/GameEnrollRequestUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/GameUpdateRequestDto.java similarity index 89% rename from src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/GameUpdateRequestDto.java index 87a43bb..1919372 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/GameUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java b/src/main/java/com/example/gamemate/domain/game/dto/request/UserGamePreferenceRequestDto.java similarity index 88% rename from src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/request/UserGamePreferenceRequestDto.java index 7e07b1d..6036872 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/request/UserGamePreferenceRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.request; import com.example.gamemate.domain.user.entity.User; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/ChatResponseDto.java similarity index 94% rename from src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/ChatResponseDto.java index 3824370..3dae15e 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/ChatResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/ChatResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import lombok.*; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameCreateResponseDto.java similarity index 89% rename from src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameCreateResponseDto.java index c9cca19..02214b9 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameCreateResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.Game; import lombok.Getter; @@ -18,7 +18,7 @@ public class GameCreateResponseDto { private final String imageUrl; public GameCreateResponseDto(Game game) { - // game 객체의 필드들을 이용해 DTO의 필드들을 초기화 + this.id = game.getId(); this.title = game.getTitle(); this.genre = game.getGenre(); diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameEnrollRequestResponseDto.java similarity index 95% rename from src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameEnrollRequestResponseDto.java index c0da8b1..eae671b 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameEnrollRequestResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameEnrollRequestResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.GamaEnrollRequest; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameFindAllResponseDto.java similarity index 96% rename from src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameFindAllResponseDto.java index b3e8c60..15d342d 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameFindAllResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.review.entity.Review; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameFindByIdResponseDto.java similarity index 89% rename from src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameFindByIdResponseDto.java index ec1e63d..a8ab3a1 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameFindByIdResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameFindByIdResponseDto.java @@ -1,10 +1,8 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.Game; -import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.Getter; -import org.springframework.data.domain.Page; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java similarity index 91% rename from src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java index 09d0b39..f2579fe 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendHistorysResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java @@ -1,7 +1,6 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.GameRecommendHistory; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java similarity index 79% rename from src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java index 12e3373..d169b45 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameRecommendationResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,4 +14,5 @@ public class GameRecommendationResponseDto { private Double star; private Double matchingScore; private String reasonForRecommendation; + private Double metacriticScore; } diff --git a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameUpdateResponseDto.java similarity index 92% rename from src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/GameUpdateResponseDto.java index 7ac7a14..ed33b63 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/GameUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameUpdateResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.Game; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/UserGamePreferenceResponseDto.java similarity index 94% rename from src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java rename to src/main/java/com/example/gamemate/domain/game/dto/response/UserGamePreferenceResponseDto.java index 4cf769f..06228cb 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/UserGamePreferenceResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/UserGamePreferenceResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.game.dto; +package com.example.gamemate.domain.game.dto.response; import com.example.gamemate.domain.game.entity.UserGamePreference; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java index eb7f24b..0b1efd6 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java @@ -37,7 +37,6 @@ public class GameRecommendHistory extends BaseEntity { @JoinColumn(name = "preferences_id") private UserGamePreference userGamePreference; - // 새로운 생성자 추가 public GameRecommendHistory(User user, String title, String description, Double matchingScore, String reasonForRecommendation, Double star, UserGamePreference userGamePreference) { this.user = user; this.title = title; @@ -46,6 +45,7 @@ public GameRecommendHistory(User user, String title, String description, Double this.reasonForRecommendation = reasonForRecommendation; this.star = star; this.userGamePreference = userGamePreference; + } } diff --git a/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java b/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java index e6eef2e..931904c 100644 --- a/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java +++ b/src/main/java/com/example/gamemate/domain/game/repository/GameRecommendHistoryRepository.java @@ -1,13 +1,10 @@ package com.example.gamemate.domain.game.repository; -import com.example.gamemate.domain.game.dto.GameRecommendHistorysResponseDto; import com.example.gamemate.domain.game.entity.GameRecommendHistory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface GameRecommendHistoryRepository extends JpaRepository { Page findByUserId(Long userId, Pageable pageable); } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index f66d7fa..adc9004 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -1,8 +1,8 @@ package com.example.gamemate.domain.game.service; -import com.example.gamemate.domain.game.dto.GameEnrollRequestCreateRequestDto; -import com.example.gamemate.domain.game.dto.GameEnrollRequestResponseDto; -import com.example.gamemate.domain.game.dto.GameEnrollRequestUpdateRequestDto; +import com.example.gamemate.domain.game.dto.request.GameEnrollRequestCreateRequestDto; +import com.example.gamemate.domain.game.dto.response.GameEnrollRequestResponseDto; +import com.example.gamemate.domain.game.dto.request.GameEnrollRequestUpdateRequestDto; import com.example.gamemate.domain.game.entity.GamaEnrollRequest; import com.example.gamemate.domain.game.entity.Game; @@ -28,6 +28,7 @@ public class GameEnrollRequestService { private final GameRepository gameRepository; private final GameEnrollRequestRepository gameEnrollRequestRepository; + //게임등록요청 생성 @Transactional public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto, User userId) { GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( @@ -41,10 +42,10 @@ public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCre return new GameEnrollRequestResponseDto(saveEnrollRequest); } + //게임등록요청 다건조회(only Role.ADMIN) public Page findAllGameEnrollRequest(User loginUser) { - //관리자만 게임등록요청 조회 가능함(조회) - if (!loginUser.getRole().equals(Role.ADMIN)){ + if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -53,11 +54,10 @@ public Page findAllGameEnrollRequest(User loginUse return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); } - + //게임등록요청 단건조회(only Role.ADMIN) public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User loginUser) { - //관리자만 게임등록요청 조회 가능함(조회) - if (!loginUser.getRole().equals(Role.ADMIN)){ + if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -67,11 +67,11 @@ public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User logi return new GameEnrollRequestResponseDto(gamaEnrollRequest); } + //게임등록요청 수정 & 게임등록 (only Role.ADMIN) @Transactional public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto, User loginUser) { - //관리자만 게임등록요청 수정 가능함(수정) - if (!loginUser.getRole().equals(Role.ADMIN)){ + if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -88,7 +88,7 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD gameEnrollRequestRepository.save(gamaEnrollRequest); - // 만약에 관리자가 true로 바꾸면 게임등록도 함께 진행됨 + // IsAccepted = true > 게임등록 Boolean accepted = requestDto.getIsAccepted(); if (accepted == true) { Game game = new Game( @@ -101,11 +101,11 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD } } + //게임등록요청 삭제 (only Role.ADMIN) @Transactional public void deleteGameEnroll(Long id, User loginUser) { - //관리자만 게임등록요청 삭제 가능함(삭제) - if (!loginUser.getRole().equals(Role.ADMIN)){ + if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java index 1bbfa63..33bd093 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java @@ -1,6 +1,9 @@ package com.example.gamemate.domain.game.service; -import com.example.gamemate.domain.game.dto.*; +import com.example.gamemate.domain.game.dto.response.GameRecommendHistorysResponseDto; +import com.example.gamemate.domain.game.dto.response.GameRecommendationResponseDto; +import com.example.gamemate.domain.game.dto.request.UserGamePreferenceRequestDto; +import com.example.gamemate.domain.game.dto.response.UserGamePreferenceResponseDto; import com.example.gamemate.domain.game.entity.GameRecommendHistory; import com.example.gamemate.domain.game.entity.UserGamePreference; import com.example.gamemate.domain.game.repository.GameRecommendHistoryRepository; @@ -31,6 +34,7 @@ public class GameRecommendService { private final GameRecommendHistoryRepository gameRecommendHistoryRepository; private final GeminiService geminiService; + // 게임 추천 요청 및 응답 @Transactional public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreferenceRequestDto requestDto, User loginUser) { @@ -49,14 +53,18 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference UserGamePreference saveData = userGamePreferenceRepository.save(userGamePreference); - // 저장된 선호도를 기반으로 Gemini API 호출 String prompt = String.format( "나에게 맞는 게임 3개 추천해줘 선호하는 장르는 %s이고 플레이 스타일은 %s 정도고 플레이 타임은 %s 정도고 난이도는 %s 그리고 플랫폼은 %s이고 추가적인 요청은 %s 야 " + - "응답은 제목(title), 간단한 내용(description), 평점(star), 나와의 매칭점수(matchingScore), 추천 이유(reasonForRecommendation)를 적어주고 " + - "응답은 순수 JSON 배열로 알려", + "응답은 " + + "한글(영어)로 된 제목(title), " + + "간단한 내용(description)," + + "metacriticScore 점수(metacriticScore)," + + "나와의 매칭점수(matchingScore)," + + "추천 이유(reasonForRecommendation)를 적어주고 " + + "응답은 순수 JSON 배열로 알려줘", userGamePreference.getPreferredGenres(), - userGamePreference.getPlayTime(), userGamePreference.getPlayStyle(), + userGamePreference.getPlayTime(), userGamePreference.getDifficulty(), userGamePreference.getPlatform(), userGamePreference.getExtraRequest() @@ -64,12 +72,12 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference String recommendation = geminiService.getContents(prompt); log.info(recommendation); - // 추천 JSON 문자열을 구조화된 객체로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); List gameRecommendations; try { - // 응답 문자열에서 JSON 배열 추출 + String jsonArray = recommendation .replaceAll("(?s)^.*?```json\\s*", "") // 시작 부분의 ``` .replaceAll("\\s*```\\s*$", "") // 끝 부분의 ``` @@ -81,7 +89,6 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference throw new ApiException(ErrorCode.RECOMMENDATION_NOT_FOUND); } - // GameRecommendHistory 엔티티 생성 및 저장 List gameRecommendHistories = new ArrayList<>(); for (GameRecommendationResponseDto responseDto : gameRecommendations) { GameRecommendHistory history = new GameRecommendHistory( @@ -90,27 +97,23 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference responseDto.getDescription(), responseDto.getMatchingScore(), responseDto.getReasonForRecommendation(), - responseDto.getStar(), + responseDto.getMetacriticScore(), saveData ); gameRecommendHistories.add(history); } - // 저장 + gameRecommendHistoryRepository.saveAll(gameRecommendHistories); return new UserGamePreferenceResponseDto(saveData, gameRecommendations); } - public Page getGameRecommendHistories(Long userId, User loginUser) { - - if (userId != loginUser.getId()){ - throw new ApiException(ErrorCode.FORBIDDEN); - } + //게임 추천 조회 + public Page getGameRecommendHistories( User loginUser) { Pageable pageable = PageRequest.of(0, 15); - Page histories = gameRecommendHistoryRepository.findByUserId(userId, pageable); + Page histories = gameRecommendHistoryRepository.findByUserId(loginUser.getId(), pageable); - // return histories.map(gameRecommendHistory -> new GameRecommendHistorysResponseDto(gameRecommendHistory)); return histories.map(GameRecommendHistorysResponseDto::new); } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 808e63d..54708c4 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -1,13 +1,14 @@ package com.example.gamemate.domain.game.service; -import com.example.gamemate.domain.game.dto.*; - +import com.example.gamemate.domain.game.dto.request.GameCreateRequestDto; +import com.example.gamemate.domain.game.dto.request.GameUpdateRequestDto; +import com.example.gamemate.domain.game.dto.response.GameCreateResponseDto; +import com.example.gamemate.domain.game.dto.response.GameFindAllResponseDto; +import com.example.gamemate.domain.game.dto.response.GameFindByIdResponseDto; import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.entity.GameImage; import com.example.gamemate.domain.game.repository.GameImageRepository; import com.example.gamemate.domain.game.repository.GameRepository; -import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; -import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.Role; @@ -19,13 +20,13 @@ 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.Arrays; import java.util.List; @@ -34,18 +35,16 @@ @RequiredArgsConstructor public class GameService { private final GameRepository gameRepository; - private final ReviewRepository reviewRepository; private final S3Service s3Service; - private final GameImageRepository gameImageRepository; + //게임 등록(생성) (only Role.ADMIN) @Transactional public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gameCreateRequestDto, MultipartFile file) { - //관리자만 가능함(생성) if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } - // 게임 엔티티 생성 + Game game = new Game( gameCreateRequestDto.getTitle(), gameCreateRequestDto.getGenre(), @@ -55,6 +54,15 @@ public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gam if (file != null && !file.isEmpty()) { try { + + String fileName = file.getOriginalFilename(); + String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); + List allowedExtensions = Arrays.asList("jpg", "jpeg"); + + if (!allowedExtensions.contains(fileExtension)) { + throw new ApiException(ErrorCode.INVALID_FILE_EXTENSION); + } + String fileUrl = s3Service.uploadFile(file); GameImage gameImage = new GameImage( file.getOriginalFilename(), @@ -62,23 +70,26 @@ public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gam fileUrl, game ); + game.addImage(gameImage); } catch (IOException e) { - throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e); + throw new ApiException(ErrorCode.FILE_UPLOAD_ERROR); } + } Game savedGame = gameRepository.save(game); return new GameCreateResponseDto(savedGame); } + //게임 다건 조회 public Page findAllGame(int page, int size) { Pageable pageable = PageRequest.of(page, size); return gameRepository.findAll(pageable).map(GameFindAllResponseDto::new); } - + //게임 단건 조회 public GameFindByIdResponseDto findGameById(Long id) { Game game = gameRepository.findGameById(id) @@ -87,8 +98,10 @@ public GameFindByIdResponseDto findGameById(Long id) { return new GameFindByIdResponseDto(game); } + //게임 정보 수정 (only Role.ADMIN) @Transactional public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile, User loginUser) { + if (loginUser == null || !loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -106,24 +119,34 @@ public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile n requestDto.getDescription() ); - gameRepository.save(game); + gameRepository.save(game); } + //게임 이미지 삭제 private void deleteExistingImages(Game game) { for (GameImage image : game.getImages()) { try { s3Service.deleteFile(image.getFilePath()); } catch (Exception e) { - // 로그 기록 후 계속 진행 log.error("Failed to delete file: {}", image.getFilePath(), e); } } game.getImages().clear(); } + //게임 이미지 등록 private void uploadNewImage(Game game, MultipartFile newFile) { if (newFile != null && !newFile.isEmpty()) { try { + + String fileName = newFile.getOriginalFilename(); + String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); + List allowedExtensions = Arrays.asList("jpg", "jpeg"); + + if (!allowedExtensions.contains(fileExtension)) { + throw new ApiException(ErrorCode.INVALID_FILE_EXTENSION); + } + String fileUrl = s3Service.uploadFile(newFile); GameImage gameImage = new GameImage( newFile.getOriginalFilename(), @@ -138,11 +161,10 @@ private void uploadNewImage(Game game, MultipartFile newFile) { } } - + //게임 삭제 (only Role.ADMIN) @Transactional public void deleteGame(Long id, User loginUser) { - //관리자만 가능함(삭제) if (!loginUser.getRole().equals(Role.ADMIN)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -150,7 +172,6 @@ public void deleteGame(Long id, User loginUser) { Game game = gameRepository.findGameById(id) .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); - // 게임에 연결된 모든 이미지 삭제 if (!game.getImages().isEmpty()) { for (GameImage image : game.getImages()) { s3Service.deleteFile(image.getFilePath()); @@ -160,6 +181,7 @@ public void deleteGame(Long id, User loginUser) { gameRepository.delete(game); } + //게임 검색 public Page searchGame(String keyword, String genre, String platform, int page, int size) { log.info("Searching games with parameters - keyword: {}, genre: {}, platform: {}", diff --git a/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java index 1eb6ada..d00dc95 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java @@ -1,7 +1,7 @@ package com.example.gamemate.domain.game.service; -import com.example.gamemate.domain.game.dto.ChatRequestDto; -import com.example.gamemate.domain.game.dto.ChatResponseDto; +import com.example.gamemate.domain.game.dto.request.ChatRequestDto; +import com.example.gamemate.domain.game.dto.response.ChatResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -23,9 +23,10 @@ public class GeminiService { @Value("${gemini.api.key}") private String geminiApiKey; + // Gemini에 요청 전송 public String getContents(String prompt) { - // Gemini에 요청 전송 + String requestUrl = apiUrl + "?key=" + geminiApiKey; ChatRequestDto request = new ChatRequestDto(prompt); diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java index d8f3052..434dbdb 100644 --- a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -1,9 +1,9 @@ package com.example.gamemate.domain.like.controller; -import com.example.gamemate.domain.like.dto.BoardLikeCountResponseDto; -import com.example.gamemate.domain.like.dto.BoardLikeResponseDto; -import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; -import com.example.gamemate.domain.like.dto.ReviewLikeResponseDto; +import com.example.gamemate.domain.like.dto.response.BoardLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.response.BoardLikeResponseDto; +import com.example.gamemate.domain.like.dto.response.ReviewLikeCountResponseDto; +import com.example.gamemate.domain.like.dto.response.ReviewLikeResponseDto; import com.example.gamemate.domain.like.service.LikeService; import com.example.gamemate.global.config.auth.CustomUserDetails; @@ -28,7 +28,7 @@ public class LikeController { * 리뷰에 대한 좋아요를 처리합니다. * * @param reviewId 좋아요를 누를 리뷰의 ID - * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소) + * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소, -1:싫어요) * @param customUserDetails 현재 인증된 사용자 정보 * @return 좋아요 처리 결과를 담은 ResponseEntity */ @@ -46,7 +46,7 @@ public ResponseEntity reviewLikeUp( * 게시글에 대한 좋아요를 처리합니다. * * @param boardId 좋아요를 누를 게시글의 ID - * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소) + * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소, -1:싫어요) * @param customUserDetails 현재 인증된 사용자 정보 * @return 좋아요 처리 결과를 담은 ResponseEntity */ diff --git a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeCountResponseDto.java similarity index 83% rename from src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java rename to src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeCountResponseDto.java index 8ef30e2..1079ca9 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeCountResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeCountResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.like.dto; +package com.example.gamemate.domain.like.dto.response; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java similarity index 80% rename from src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java rename to src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java index 2b0a652..d5aaaf6 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/BoardLikeResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java @@ -1,7 +1,6 @@ -package com.example.gamemate.domain.like.dto; +package com.example.gamemate.domain.like.dto.response; import com.example.gamemate.domain.like.entity.BoardLike; -import com.example.gamemate.domain.like.entity.ReviewLike; import lombok.Getter; @Getter diff --git a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeCountResponseDto.java similarity index 83% rename from src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java rename to src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeCountResponseDto.java index 9d08405..b2611e7 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeCountResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeCountResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.like.dto; +package com.example.gamemate.domain.like.dto.response; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java similarity index 88% rename from src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java rename to src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java index 3341229..32e10dc 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/ReviewLikeResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.like.dto; +package com.example.gamemate.domain.like.dto.response; import com.example.gamemate.domain.like.entity.ReviewLike; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 505ce2b..56fc81b 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -1,9 +1,8 @@ package com.example.gamemate.domain.like.service; import com.example.gamemate.domain.board.repository.BoardRepository; -import com.example.gamemate.domain.like.dto.BoardLikeResponseDto; -import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; -import com.example.gamemate.domain.like.dto.ReviewLikeResponseDto; +import com.example.gamemate.domain.like.dto.response.BoardLikeResponseDto; +import com.example.gamemate.domain.like.dto.response.ReviewLikeResponseDto; import com.example.gamemate.domain.like.entity.BoardLike; import com.example.gamemate.domain.like.entity.ReviewLike; import com.example.gamemate.domain.like.repository.BoardLikeRepository; @@ -29,6 +28,7 @@ public class LikeService { private final BoardRepository boardRepository; private final NotificationService notificationService; + //리뷰 좋아요 생성 취소 수정 @Transactional public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User loginUser) { @@ -51,6 +51,7 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo return new ReviewLikeResponseDto(reviewLike); } + //게시물 좋아요 생성 취소 수정 @Transactional public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User loginUser) { @@ -74,10 +75,17 @@ public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User login } public Long getBoardLikeCount(Long boardId) { + + boardRepository.findById(boardId) + .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)); return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, 1); } public Long getReivewLikeCount(Long reviewId) { + + reviewRepository.findById(reviewId) + .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); + return reviewLikeRepository.countByReviewIdAndStatus(reviewId, 1); } } diff --git a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java index 9cfec15..b7f8709 100644 --- a/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/gamemate/domain/review/controller/ReviewController.java @@ -1,30 +1,20 @@ package com.example.gamemate.domain.review.controller; -import com.example.gamemate.domain.review.dto.*; +import com.example.gamemate.domain.review.dto.request.ReviewCreateRequestDto; +import com.example.gamemate.domain.review.dto.request.ReviewUpdateRequestDto; +import com.example.gamemate.domain.review.dto.response.ReviewCreateResponseDto; +import com.example.gamemate.domain.review.dto.response.ReviewFindByAllResponseDto; import com.example.gamemate.domain.review.service.ReviewService; -import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.config.auth.CustomUserDetails; -import com.example.gamemate.global.constant.ErrorCode; -import com.example.gamemate.global.exception.ApiException; -import com.example.gamemate.global.provider.JwtTokenProvider; import jakarta.validation.Valid; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -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.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.*; -import static com.example.gamemate.global.constant.ErrorCode.USER_NOT_FOUND; - /** * 게임 리뷰 관련 API를 처리하는 컨트롤러 클래스입니다. * 이 컨트롤러는 리뷰의 생성, 수정, 삭제 및 조회 기능을 제공합니다. diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/review/dto/request/ReviewCreateRequestDto.java similarity index 88% rename from src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/request/ReviewCreateRequestDto.java index b002560..dd102b6 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/request/ReviewCreateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.review.dto; +package com.example.gamemate.domain.review.dto.request; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java b/src/main/java/com/example/gamemate/domain/review/dto/request/ReviewUpdateRequestDto.java similarity index 88% rename from src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/request/ReviewUpdateRequestDto.java index 10896f4..0cf8915 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.review.dto; +package com.example.gamemate.domain.review.dto.request; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewCreateResponseDto.java similarity index 91% rename from src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/response/ReviewCreateResponseDto.java index 3dd8dad..7334063 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewCreateResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.review.dto; +package com.example.gamemate.domain.review.dto.response; import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewFindByAllResponseDto.java similarity index 93% rename from src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/response/ReviewFindByAllResponseDto.java index f45de45..a54de24 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewFindByAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewFindByAllResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.review.dto; +package com.example.gamemate.domain.review.dto.response; import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewUpdateResponseDto.java similarity index 91% rename from src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java rename to src/main/java/com/example/gamemate/domain/review/dto/response/ReviewUpdateResponseDto.java index 4684bb3..f56ef5c 100644 --- a/src/main/java/com/example/gamemate/domain/review/dto/ReviewUpdateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/review/dto/response/ReviewUpdateResponseDto.java @@ -1,4 +1,4 @@ -package com.example.gamemate.domain.review.dto; +package com.example.gamemate.domain.review.dto.response; import com.example.gamemate.domain.review.entity.Review; import lombok.Getter; diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index a8a3b96..f5b5329 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -3,10 +3,10 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.repository.GameRepository; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; -import com.example.gamemate.domain.review.dto.ReviewCreateRequestDto; -import com.example.gamemate.domain.review.dto.ReviewCreateResponseDto; -import com.example.gamemate.domain.review.dto.ReviewFindByAllResponseDto; -import com.example.gamemate.domain.review.dto.ReviewUpdateRequestDto; +import com.example.gamemate.domain.review.dto.request.ReviewCreateRequestDto; +import com.example.gamemate.domain.review.dto.response.ReviewCreateResponseDto; +import com.example.gamemate.domain.review.dto.response.ReviewFindByAllResponseDto; +import com.example.gamemate.domain.review.dto.request.ReviewUpdateRequestDto; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; @@ -31,6 +31,7 @@ public class ReviewService { private final UserRepository userRepository; private final ReviewLikeRepository reviewLikeRepository; + //리뷰 생성 @Transactional public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewCreateRequestDto requestDto) { @@ -39,14 +40,13 @@ public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewC User user = userRepository.findById(userId) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); - // 사용자가 이미 해당 게임에 대한 리뷰를 작성했는지 확인 boolean hasReview = reviewRepository.existsByUserIdAndGameId(userId, gameId); if (hasReview) { throw new ApiException(ErrorCode.REVIEW_ALREADY_EXISTS); } Game game = gameRepository.findById(gameId) - .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); + .orElseThrow(() -> new ApiException(ErrorCode.GAME_NOT_FOUND)); Review review = new Review( requestDto.getContent(), @@ -59,6 +59,7 @@ public ReviewCreateResponseDto createReview(User loginUser, Long gameId, ReviewC return new ReviewCreateResponseDto(saveReview); } + //리뷰 수정 @Transactional public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateRequestDto requestDto) { @@ -67,7 +68,6 @@ public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateReque Review review = reviewRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); - // 리뷰 작성자와 현재 사용자가 같은지 확인 if (!review.getUser().getId().equals(userId)) { throw new ApiException(ErrorCode.FORBIDDEN); } @@ -80,6 +80,7 @@ public void updateReview(User loginUser, Long gameId, Long id, ReviewUpdateReque reviewRepository.save(review); } + //리뷰 삭제 @Transactional public void deleteReview(User loginUser, Long id) { @@ -96,6 +97,7 @@ public void deleteReview(User loginUser, Long id) { } + //리뷰 조회(게임별 다건 조회) public Page ReviewFindAllByGameId(Long gameId, User loginUser) { Game game = gameRepository.findGameById(gameId) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 90a6784..07ecb5f 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -24,6 +24,8 @@ public enum ErrorCode { VERIFICATION_TIME_EXPIRED(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 시간이 만료되었습니다."), INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE", "인증 코드가 일치하지 않습니다."), EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "EMAIL_NOT_VERIFIED", "이메일 인증이 필요합니다"), + INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST,"INVALID_FILE_EXTENSION", "허용되지 않는 파일 형식입니다."), + FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST,"FILE_SIZE_EXCEEDED","파일 크기가 초과되었습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 18327ad..37ed920 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.coyote.Response; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -143,4 +145,11 @@ private ErrorResponse makeErrorResponse(ErrorCode errorCode, String message) { .build(); } + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity handleMaxSizeException(MaxUploadSizeExceededException exc) { + log.warn("File size limit exceeded", exc); + ErrorCode errorCode = ErrorCode.FILE_SIZE_EXCEEDED; + return handleExceptionInternal(errorCode); + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 630b42b..3ff0c41 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -71,3 +71,13 @@ logging.level.com.example.gamemate=DEBUG gemini.api.url=${GEMINI_URL} gemini.api.key=${GEMINI_KEY} +#S3 +cloud.aws.credentials.access-key=${AWS_ACCESS_KEY} +cloud.aws.credentials.secret-key=${AWS_SECRET_KEY} +cloud.aws.s3.bucket=${AWS_BUCKET} +cloud.aws.region.static=${AWS_REGION} +cloud.aws.stack.auto=${AWS_STACK_AUTO} + +#multipart +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4aae4e3..e69de29 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,11 +0,0 @@ -cloud: - aws: - credentials: - access-key: YOUR_ACCESS_KEY - secret-key: YOUR_SECRET_KEY - s3: - bucket: YOUR_BUCKET_NAME - region: - static: ap-northeast-2 - stack: - auto: false \ No newline at end of file From a1842f663fefb77358312aadbc52b749351ccc2d Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:22:37 +0900 Subject: [PATCH 134/215] =?UTF-8?q?refact:=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=9D=91=EB=8B=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. matchingScore 항목 삭제 2. star 항목 > metacriticScore 수정 (신뢰도) --- .../dto/response/GameRecommendHistorysResponseDto.java | 8 ++++---- .../dto/response/GameRecommendationResponseDto.java | 5 ++--- .../domain/game/entity/GameRecommendHistory.java | 10 +++++----- .../domain/game/service/GameRecommendService.java | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java index f2579fe..36e5338 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendHistorysResponseDto.java @@ -10,8 +10,8 @@ public class GameRecommendHistorysResponseDto { private Long userId; private String title; private String description; - private Double star; - private Double matchingScore; + private Double metacriticScore; + //private Double matchingScore; private String reasonForRecommendation; public GameRecommendHistorysResponseDto(GameRecommendHistory gameRecommendHistory) { @@ -19,8 +19,8 @@ public GameRecommendHistorysResponseDto(GameRecommendHistory gameRecommendHistor this.userId = gameRecommendHistory.getUser().getId(); this.title = gameRecommendHistory.getTitle(); this.description = gameRecommendHistory.getDescription(); - this.star = gameRecommendHistory.getStar(); - this.matchingScore = gameRecommendHistory.getMatchingScore(); + this.metacriticScore = gameRecommendHistory.getMetacriticScore(); + //this.matchingScore = gameRecommendHistory.getMatchingScore(); this.reasonForRecommendation = gameRecommendHistory.getReasonForRecommendation(); } } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java index d169b45..4230c53 100644 --- a/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/game/dto/response/GameRecommendationResponseDto.java @@ -11,8 +11,7 @@ public class GameRecommendationResponseDto { private String title; private String description; - private Double star; - private Double matchingScore; - private String reasonForRecommendation; private Double metacriticScore; + //private Double matchingScore; + private String reasonForRecommendation; } diff --git a/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java index 0b1efd6..d2985d5 100644 --- a/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java +++ b/src/main/java/com/example/gamemate/domain/game/entity/GameRecommendHistory.java @@ -26,24 +26,24 @@ public class GameRecommendHistory extends BaseEntity { @Column(length = 255) private String description; - private Double matchingScore; + //private Double matchingScore; @Column(length = 255) private String reasonForRecommendation; - private Double star; + private Double metacriticScore; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "preferences_id") private UserGamePreference userGamePreference; - public GameRecommendHistory(User user, String title, String description, Double matchingScore, String reasonForRecommendation, Double star, UserGamePreference userGamePreference) { + public GameRecommendHistory(User user, String title, String description, String reasonForRecommendation, Double metacriticScore, UserGamePreference userGamePreference) { this.user = user; this.title = title; this.description = description; - this.matchingScore = matchingScore; + //this.matchingScore = matchingScore; this.reasonForRecommendation = reasonForRecommendation; - this.star = star; + this.metacriticScore = metacriticScore; this.userGamePreference = userGamePreference; } diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java index 33bd093..50b9af8 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java @@ -59,7 +59,7 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference "한글(영어)로 된 제목(title), " + "간단한 내용(description)," + "metacriticScore 점수(metacriticScore)," + - "나와의 매칭점수(matchingScore)," + + //"나와의 매칭점수(matchingScore)," + "추천 이유(reasonForRecommendation)를 적어주고 " + "응답은 순수 JSON 배열로 알려줘", userGamePreference.getPreferredGenres(), @@ -95,7 +95,7 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference loginUser, responseDto.getTitle(), responseDto.getDescription(), - responseDto.getMatchingScore(), + //responseDto.getMatchingScore(), responseDto.getReasonForRecommendation(), responseDto.getMetacriticScore(), saveData From a67928fa36fafd0aa2e497662c6896e1150cf750 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:33:19 +0900 Subject: [PATCH 135/215] =?UTF-8?q?refact:=20=EB=A6=AC=EB=B7=B0,=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20enum=20?= =?UTF-8?q?=EC=9C=BC=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 1. 리뷰,게시물 좋아요 enum 으로 변경 --- .../like/controller/LikeController.java | 7 ++--- .../dto/response/BoardLikeResponseDto.java | 3 ++- .../dto/response/ReviewLikeResponseDto.java | 3 ++- .../domain/like/entity/BoardLike.java | 8 +++--- .../domain/like/entity/ReviewLike.java | 8 +++--- .../domain/like/enums/LikeStatus.java | 26 +++++++++++++++++++ .../like/repository/BoardLikeRepository.java | 3 ++- .../like/repository/ReviewLikeRepository.java | 3 ++- .../domain/like/service/LikeService.java | 9 ++++--- .../domain/review/service/ReviewService.java | 3 ++- 10 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java index 434dbdb..7fe142a 100644 --- a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -4,6 +4,7 @@ import com.example.gamemate.domain.like.dto.response.BoardLikeResponseDto; import com.example.gamemate.domain.like.dto.response.ReviewLikeCountResponseDto; import com.example.gamemate.domain.like.dto.response.ReviewLikeResponseDto; +import com.example.gamemate.domain.like.enums.LikeStatus; import com.example.gamemate.domain.like.service.LikeService; import com.example.gamemate.global.config.auth.CustomUserDetails; @@ -28,14 +29,14 @@ public class LikeController { * 리뷰에 대한 좋아요를 처리합니다. * * @param reviewId 좋아요를 누를 리뷰의 ID - * @param status 좋아요 상태 (1: 좋아요, 0: 좋아요 취소, -1:싫어요) + * @param status 좋아요 상태 * @param customUserDetails 현재 인증된 사용자 정보 * @return 좋아요 처리 결과를 담은 ResponseEntity */ @PostMapping("/reviews/{reviewId}") public ResponseEntity reviewLikeUp( @PathVariable Long reviewId, - @RequestBody Integer status, + @RequestBody LikeStatus status, @AuthenticationPrincipal CustomUserDetails customUserDetails) { ReviewLikeResponseDto responseDto = likeService.reviewLikeUp(reviewId, status, customUserDetails.getUser()); @@ -53,7 +54,7 @@ public ResponseEntity reviewLikeUp( @PostMapping("/boards/{boardId}") public ResponseEntity boardLikeUp( @PathVariable Long boardId, - @RequestBody Integer status, + @RequestBody LikeStatus status, @AuthenticationPrincipal CustomUserDetails customUserDetails) { BoardLikeResponseDto responseDto = likeService.boardLikeUp(boardId, status, customUserDetails.getUser()); diff --git a/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java index d5aaaf6..212a571 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java @@ -1,13 +1,14 @@ package com.example.gamemate.domain.like.dto.response; import com.example.gamemate.domain.like.entity.BoardLike; +import com.example.gamemate.domain.like.enums.LikeStatus; import lombok.Getter; @Getter public class BoardLikeResponseDto { private Long boardId; private Long userId; - private Integer status; + private LikeStatus status; public BoardLikeResponseDto(BoardLike boardLike){ diff --git a/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java index 32e10dc..366d449 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/ReviewLikeResponseDto.java @@ -1,13 +1,14 @@ package com.example.gamemate.domain.like.dto.response; import com.example.gamemate.domain.like.entity.ReviewLike; +import com.example.gamemate.domain.like.enums.LikeStatus; import lombok.Getter; @Getter public class ReviewLikeResponseDto { private Long reviewId; private Long userId; - private Integer status; + private LikeStatus status; public ReviewLikeResponseDto(ReviewLike reviewLike){ diff --git a/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java b/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java index e745b37..58ad698 100644 --- a/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java +++ b/src/main/java/com/example/gamemate/domain/like/entity/BoardLike.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.like.entity; import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.like.enums.LikeStatus; import com.example.gamemate.domain.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; @@ -14,8 +15,9 @@ public class BoardLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false) - private Integer status; // 1: 좋아요, -1: 싫어요, 0: 무반응 + private LikeStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @@ -25,14 +27,14 @@ public class BoardLike { @JoinColumn(name = "board_id") private Board board; - public BoardLike(Integer status, User user, Board board) { + public BoardLike(LikeStatus status, User user, Board board) { this.status = status; this.user = user; this.board = board; } // 좋아요 상태 변경을 위한 메서드 - public void changeStatus(Integer status) { + public void changeStatus(LikeStatus status) { this.status = status; } } diff --git a/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java b/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java index 8f0027d..68902fa 100644 --- a/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java +++ b/src/main/java/com/example/gamemate/domain/like/entity/ReviewLike.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.like.entity; +import com.example.gamemate.domain.like.enums.LikeStatus; import com.example.gamemate.domain.review.entity.Review; import com.example.gamemate.domain.user.entity.User; import jakarta.persistence.*; @@ -14,8 +15,9 @@ public class ReviewLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false) - private Integer status; // 1: 좋아요, -1: 싫어요, 0: 무반응 + private LikeStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @@ -25,14 +27,14 @@ public class ReviewLike { @JoinColumn(name = "review_id") private Review review; - public ReviewLike(Integer status, User user, Review review) { + public ReviewLike(LikeStatus status, User user, Review review) { this.status = status; this.user = user; this.review = review; } // 좋아요 상태 변경을 위한 메서드 - public void changeStatus(Integer status) { + public void changeStatus(LikeStatus status) { this.status = status; } } diff --git a/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java b/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java new file mode 100644 index 0000000..6bf1466 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java @@ -0,0 +1,26 @@ +package com.example.gamemate.domain.like.enums; + +public enum LikeStatus { + LIKE("like"), + DISLIKE("disLike"), + NEUTRAL("neutral"); + + private final String value; + + LikeStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static LikeStatus fromValue(String value) { + for (LikeStatus status : values()) { + if (status.equals(value)) { + return status; + } + } + throw new IllegalArgumentException("Invalid LikeStatus value: " + value); + } +} diff --git a/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java index a7515f7..5d470fe 100644 --- a/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java +++ b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.like.repository; import com.example.gamemate.domain.like.entity.BoardLike; +import com.example.gamemate.domain.like.enums.LikeStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -12,7 +13,7 @@ public interface BoardLikeRepository extends JpaRepository { @Query("SELECT bl FROM BoardLike bl WHERE bl.board.boardId = :boardId AND bl.user.id = :userId") Optional findByBoardIdAndUserId(@Param("boardId") Long boardId, @Param("userId") Long userId); - Long countByBoardBoardIdAndStatus(Long boardId, Integer status); + Long countByBoardBoardIdAndStatus(Long boardId, LikeStatus status); } diff --git a/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java b/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java index fb486cd..c4fbc82 100644 --- a/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java +++ b/src/main/java/com/example/gamemate/domain/like/repository/ReviewLikeRepository.java @@ -1,6 +1,7 @@ package com.example.gamemate.domain.like.repository; import com.example.gamemate.domain.like.entity.ReviewLike; +import com.example.gamemate.domain.like.enums.LikeStatus; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,6 +11,6 @@ public interface ReviewLikeRepository extends JpaRepository { Optional findByReviewIdAndUserId(Long reviewId, Long userId); - Long countByReviewIdAndStatus(Long reviewId, Integer status); + Long countByReviewIdAndStatus(Long reviewId, LikeStatus status); } diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 56fc81b..65159b1 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -5,6 +5,7 @@ import com.example.gamemate.domain.like.dto.response.ReviewLikeResponseDto; import com.example.gamemate.domain.like.entity.BoardLike; import com.example.gamemate.domain.like.entity.ReviewLike; +import com.example.gamemate.domain.like.enums.LikeStatus; import com.example.gamemate.domain.like.repository.BoardLikeRepository; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; import com.example.gamemate.domain.notification.enums.NotificationType; @@ -30,7 +31,7 @@ public class LikeService { //리뷰 좋아요 생성 취소 수정 @Transactional - public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User loginUser) { + public ReviewLikeResponseDto reviewLikeUp(Long reviewId, LikeStatus status, User loginUser) { ReviewLike reviewLike = reviewLikeRepository.findByReviewIdAndUserId(reviewId, loginUser.getId()). orElse(new ReviewLike( @@ -53,7 +54,7 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo //게시물 좋아요 생성 취소 수정 @Transactional - public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User loginUser) { + public BoardLikeResponseDto boardLikeUp(Long boardId, LikeStatus status, User loginUser) { BoardLike boardLike = boardLikeRepository.findByBoardIdAndUserId(boardId, loginUser.getId()). orElse(new BoardLike( @@ -78,7 +79,7 @@ public Long getBoardLikeCount(Long boardId) { boardRepository.findById(boardId) .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)); - return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, 1); + return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, LikeStatus.LIKE); } public Long getReivewLikeCount(Long reviewId) { @@ -86,6 +87,6 @@ public Long getReivewLikeCount(Long reviewId) { reviewRepository.findById(reviewId) .orElseThrow(() -> new ApiException(ErrorCode.REVIEW_NOT_FOUND)); - return reviewLikeRepository.countByReviewIdAndStatus(reviewId, 1); + return reviewLikeRepository.countByReviewIdAndStatus(reviewId, LikeStatus.LIKE); } } diff --git a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java index f5b5329..48d7b2c 100644 --- a/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/gamemate/domain/review/service/ReviewService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.game.entity.Game; import com.example.gamemate.domain.game.repository.GameRepository; +import com.example.gamemate.domain.like.enums.LikeStatus; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; import com.example.gamemate.domain.review.dto.request.ReviewCreateRequestDto; import com.example.gamemate.domain.review.dto.response.ReviewCreateResponseDto; @@ -107,7 +108,7 @@ public Page ReviewFindAllByGameId(Long gameId, User Page reviewPage = reviewRepository.findAllByGame(game, pageable); return reviewPage.map(review -> { - Long likeCount = reviewLikeRepository.countByReviewIdAndStatus(review.getId(), 1); + Long likeCount = reviewLikeRepository.countByReviewIdAndStatus(review.getId(), LikeStatus.LIKE); return new ReviewFindByAllResponseDto(review, loginUser.getNickname(), likeCount); }); } From ac0d642c5050393983c28c0193a8b3e6aba6f722 Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:03:43 +0900 Subject: [PATCH 136/215] =?UTF-8?q?refact:=20=EB=A6=AC=EB=B7=B0,=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20Dto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 리뷰,게시물 좋아요 수정 --- .../domain/like/controller/LikeController.java | 9 +++++---- .../domain/like/dto/request/LikeRequestDto.java | 16 ++++++++++++++++ .../gamemate/domain/like/enums/LikeStatus.java | 10 +++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/like/dto/request/LikeRequestDto.java diff --git a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java index 7fe142a..7e751b0 100644 --- a/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java +++ b/src/main/java/com/example/gamemate/domain/like/controller/LikeController.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.like.controller; +import com.example.gamemate.domain.like.dto.request.LikeRequestDto; import com.example.gamemate.domain.like.dto.response.BoardLikeCountResponseDto; import com.example.gamemate.domain.like.dto.response.BoardLikeResponseDto; import com.example.gamemate.domain.like.dto.response.ReviewLikeCountResponseDto; @@ -36,10 +37,10 @@ public class LikeController { @PostMapping("/reviews/{reviewId}") public ResponseEntity reviewLikeUp( @PathVariable Long reviewId, - @RequestBody LikeStatus status, + @RequestBody LikeRequestDto requestDto, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - ReviewLikeResponseDto responseDto = likeService.reviewLikeUp(reviewId, status, customUserDetails.getUser()); + ReviewLikeResponseDto responseDto = likeService.reviewLikeUp(reviewId, requestDto.getStatus(), customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } @@ -54,10 +55,10 @@ public ResponseEntity reviewLikeUp( @PostMapping("/boards/{boardId}") public ResponseEntity boardLikeUp( @PathVariable Long boardId, - @RequestBody LikeStatus status, + @RequestBody LikeRequestDto requestDto, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - BoardLikeResponseDto responseDto = likeService.boardLikeUp(boardId, status, customUserDetails.getUser()); + BoardLikeResponseDto responseDto = likeService.boardLikeUp(boardId, requestDto.getStatus(), customUserDetails.getUser()); return new ResponseEntity<>(responseDto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/like/dto/request/LikeRequestDto.java b/src/main/java/com/example/gamemate/domain/like/dto/request/LikeRequestDto.java new file mode 100644 index 0000000..d7f34a7 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/like/dto/request/LikeRequestDto.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.like.dto.request; + +import com.example.gamemate.domain.like.enums.LikeStatus; + +public class LikeRequestDto { + private final LikeStatus status; + + public LikeRequestDto(LikeStatus status) { + this.status = status; + } + + public LikeStatus getStatus() { + return status; + } +} + diff --git a/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java b/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java index 6bf1466..8811fb8 100644 --- a/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java +++ b/src/main/java/com/example/gamemate/domain/like/enums/LikeStatus.java @@ -1,5 +1,8 @@ package com.example.gamemate.domain.like.enums; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + public enum LikeStatus { LIKE("like"), DISLIKE("disLike"), @@ -11,13 +14,18 @@ public enum LikeStatus { this.value = value; } + @JsonValue public String getValue() { return value; } + @JsonCreator public static LikeStatus fromValue(String value) { + if (value == null) { + return null; + } for (LikeStatus status : values()) { - if (status.equals(value)) { + if (status.value.equalsIgnoreCase(value)) { return status; } } From 07f5df69f55bd60f379caac2d2a0e57a7e781041 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 24 Jan 2025 20:38:55 +0900 Subject: [PATCH 137/215] =?UTF-8?q?feat=20:=20Sse=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=86=A0=EC=BD=9C=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=9C=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=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 --- build.gradle | 2 +- .../comment/service/CommentService.java | 7 +- .../domain/follow/service/FollowService.java | 2 +- .../domain/like/service/LikeService.java | 4 +- .../domain/match/service/MatchService.java | 8 +- .../controller/NotificationController.java | 13 +++ .../dto/NotificationResponseDto.java | 6 +- .../notification/entity/Notification.java | 21 +++-- .../repository/EmitterRepository.java | 26 ++++++ .../repository/NotificationRepository.java | 4 +- .../service/AsyncNotificationService.java | 50 +++++------ .../service/NotificationService.java | 85 ++++++++++++++----- .../domain/reply/service/ReplyService.java | 23 ++--- .../gamemate/global/config/MailConfig.java | 69 --------------- src/main/resources/application.properties | 6 ++ src/main/resources/application.yml | 11 --- 16 files changed, 180 insertions(+), 157 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java delete mode 100644 src/main/java/com/example/gamemate/global/config/MailConfig.java delete mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index 2a52670..8ea053b 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index 0454aaa..46ebd18 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -52,7 +52,12 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq Comment comment = new Comment(requestDto.getContent(), findBoard, loginUser); Comment createComment = commentRepository.save(comment); - notificationService.createNotification(findBoard.getUser(), NotificationType.NEW_COMMENT); + + notificationService.sendNotification( + findBoard.getUser(), + NotificationType.NEW_COMMENT, + "/comments/" + createComment.getCommentId() + ); return new CommentResponseDto( createComment.getCommentId(), diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 26f3913..a67f1ee 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -47,7 +47,7 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser Follow follow = new Follow(loginUser, followee); followRepository.save(follow); - notificationService.createNotification(followee, NotificationType.NEW_FOLLOWER); + notificationService.sendNotification(followee, NotificationType.NEW_FOLLOWER, "/users/" + loginUser.getId()); return new FollowResponseDto( follow.getId(), diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 505ce2b..77ba0aa 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -43,7 +43,7 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo if (reviewLike.getId() == null) { reviewLikeRepository.save(reviewLike); - notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE); + notificationService.sendNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/likes/reviews/" + reviewId); } else { reviewLike.changeStatus(status); } @@ -65,7 +65,7 @@ public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User login if (boardLike.getId() == null) { boardLikeRepository.save(boardLike); - notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE); + notificationService.sendNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/likes/boards/" + boardId); } else { boardLike.changeStatus(status); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 9fcc04e..5fe60ce 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -60,8 +60,8 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { } // 이미 보낸 요청이 있을때 예외처리 Match match = new Match(dto.getMessage(), loginUser, receiver); - matchRepository.save(match); - notificationService.createNotification(receiver, NotificationType.NEW_MATCH); + Match savedMatch = matchRepository.save(match); + notificationService.sendNotification(receiver, NotificationType.NEW_MATCH, "/matches/" + savedMatch.getId()); return MatchResponseDto.toDto(match); } @@ -82,11 +82,11 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { } // 로그인한 유저가 매칭의 받는 사람이 아닐때 예외처리 if (dto.getStatus() == MatchStatus.ACCEPTED) { - notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED); + notificationService.sendNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + findMatch.getId()); } // 매칭 보낸 사람에게 매칭이 수락되었다는 알림 전송 if (dto.getStatus() == MatchStatus.REJECTED) { - notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED); + notificationService.sendNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + findMatch.getId()); } // 매칭 보낸 사람에게 매칭이 거절되었다는 알림 전송 findMatch.updateStatus(dto.getStatus()); diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index 0d98d8f..bc77a60 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -5,11 +5,14 @@ import com.example.gamemate.global.config.auth.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; @@ -19,6 +22,16 @@ public class NotificationController { private final NotificationService notificationService; + @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseEntity connect( + @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + SseEmitter sseEmitter = notificationService.subscribe(customUserDetails.getUser(), lastEventId); + return new ResponseEntity<>(sseEmitter, HttpStatus.OK); + } + /** * 로그인한 사용자의 전체 알림을 조회합니다. * diff --git a/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java index 09d44d5..66c0236 100644 --- a/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java @@ -9,14 +9,16 @@ public class NotificationResponseDto { private Long id; private String content; private NotificationType type; + private String relatedUrl; - public NotificationResponseDto(Long id, String content, NotificationType type) { + public NotificationResponseDto(Long id, String content, NotificationType type, String relatedUrl) { this.id = id; this.content = content; this.type = type; + this.relatedUrl = relatedUrl; } public static NotificationResponseDto toDto(Notification notification) { - return new NotificationResponseDto(notification.getId(), notification.getContent(), notification.getType()); + return new NotificationResponseDto(notification.getId(), notification.getContent(), notification.getType(), notification.getRelatedUrl()); } } diff --git a/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java index 839cea4..a090951 100644 --- a/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java +++ b/src/main/java/com/example/gamemate/domain/notification/entity/Notification.java @@ -5,7 +5,6 @@ import com.example.gamemate.global.common.BaseCreatedEntity; import jakarta.persistence.*; import lombok.Getter; -import lombok.Setter; @Entity @Getter @@ -17,28 +16,32 @@ public class Notification extends BaseCreatedEntity { @Column private String content; + @Column + private String relatedUrl; + @Enumerated(EnumType.STRING) @Column private NotificationType type; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "receiver_id") + private User receiver; @Column - private boolean sentStatus; + private boolean isRead; public Notification() { } - public Notification(String content, NotificationType type, User user) { + public Notification(String content, String relatedUrl, NotificationType type, User receiver) { this.content = content; + this.relatedUrl = relatedUrl; this.type = type; - this.user = user; - this.sentStatus = false; + this.receiver = receiver; + this.isRead = false; } - public void updateSentStatus(boolean sentStatus) { - this.sentStatus = sentStatus; + public void updateIsRead(boolean isRead) { + this.isRead = isRead; } } diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java new file mode 100644 index 0000000..664453b --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java @@ -0,0 +1,26 @@ +package com.example.gamemate.domain.notification.repository; + +import org.springframework.stereotype.Repository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class EmitterRepository { + private final Map emitters = new ConcurrentHashMap<>(); + private final Map eventCache = new ConcurrentHashMap<>(); + + public SseEmitter findById(Long userId) { + return emitters.get(userId); + } + + public SseEmitter save(Long userId, SseEmitter sseEmitter) { + emitters.put(userId, sseEmitter); + return emitters.get(userId); + } + + public void deleteById(Long userId) { + emitters.remove(userId); + } +} diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index 0dea4a7..84de06d 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -9,7 +9,7 @@ @Repository public interface NotificationRepository extends JpaRepository { - List findAllByUserId(Long userId); - List findAllBySentStatus(boolean sentStatus); + List findAllByReceiverId(Long receiverId); + List findAllByIsRead(boolean isRead); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java index 7b44ff1..69e4a3f 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java @@ -22,29 +22,29 @@ public class AsyncNotificationService { private final JavaMailSender javaMailSender; private final NotificationRepository notificationRepository; - // 알림 메일 전송 - @Async - public void sendNotificationMail(User user, List notifications) { - try { - SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); - simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 - simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 - simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 - simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 - - javaMailSender.send(simpleMailMessage); - log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); - - updateNotificationStatus(notifications); - } catch (Exception e) { - log.error("알림 메일 전송 실패: {}", user.getEmail(), e); - } - } - - // 알림 전송 후 notified(false -> true) 상태 변경 - @Transactional - public void updateNotificationStatus(List notifications) { - notifications.forEach(notification -> notification.updateSentStatus(true)); - notificationRepository.saveAll(notifications); - } +// // 알림 메일 전송 +// @Async +// public void sendNotificationMail(User user, List notifications) { +// try { +// SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); +// simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 +// simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 +// simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 +// simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 +// +// javaMailSender.send(simpleMailMessage); +// log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); +// +// updateNotificationStatus(notifications); +// } catch (Exception e) { +// log.error("알림 메일 전송 실패: {}", user.getEmail(), e); +// } +// } +// +// // 알림 전송 후 notified(false -> true) 상태 변경 +// @Transactional +// public void updateNotificationStatus(List notifications) { +// notifications.forEach(notification -> notification.updateIsRead(true)); +// notificationRepository.saveAll(notifications); +// } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 4f29821..00c5604 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -3,15 +3,18 @@ import com.example.gamemate.domain.notification.dto.NotificationResponseDto; import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.repository.EmitterRepository; import com.example.gamemate.domain.notification.repository.NotificationRepository; import com.example.gamemate.domain.user.entity.User; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -23,18 +26,19 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final AsyncNotificationService asyncNotificationService; + private final EmitterRepository emitterRepository; // 알림 생성 @Transactional - public void createNotification(User user, NotificationType type) { - Notification notification = new Notification(type.getContent(), type, user); + public void createNotification(User user, NotificationType type, String relatedUrl) { + Notification notification = new Notification(type.getContent(), relatedUrl, type, user); notificationRepository.save(notification); } // 알림 전체 보기 public List findAllNotification(User loginUser) { - List notificationList = notificationRepository.findAllByUserId(loginUser.getId()); + List notificationList = notificationRepository.findAllByReceiverId(loginUser.getId()); return notificationList .stream() @@ -42,28 +46,69 @@ public List findAllNotification(User loginUser) { .toList(); } - // 알림 발송 (이메일) - @Scheduled(cron = "0 0/10 * * * *") - public void scheduleNotificationEmail() { - log.info("스케쥴링 활성화"); +// // 알림 발송 (이메일) +// @Scheduled(cron = "0 0/10 * * * *") +// public void scheduleNotificationEmail() { +// log.info("스케쥴링 활성화"); +// +// List unnotifiedNotificationList = notificationRepository.findAllByIsRead(false); +// +// if (unnotifiedNotificationList.isEmpty()) { +// log.info("전송할 알림이 없습니다."); +// return; +// } +// +// Map> notificationMap = +// unnotifiedNotificationList +// .stream() +// .collect(Collectors.groupingBy(Notification::getReceiver)); +// +// for (Map.Entry> entry : notificationMap.entrySet()) { +// User user = entry.getKey(); +// List notifications = entry.getValue(); +// +// asyncNotificationService.sendNotificationMail(user, notifications); +// } +// } - List unnotifiedNotificationList = notificationRepository.findAllBySentStatus(false); + public SseEmitter subscribe(User loginUser, String lastEventId) { + Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + SseEmitter sseEmitter = emitterRepository.save(loginUser.getId(), new SseEmitter(DEFAULT_TIMEOUT)); - if (unnotifiedNotificationList.isEmpty()) { - log.info("전송할 알림이 없습니다."); - return; + sseEmitter.onCompletion(() -> emitterRepository.deleteById(loginUser.getId())); + sseEmitter.onTimeout(() -> emitterRepository.deleteById(loginUser.getId())); + + try { + sseEmitter.send( + SseEmitter.event() + .id(loginUser.getId().toString()) + .name("connect") + .data("connected!") + ); + } catch (IOException e) { + emitterRepository.deleteById(loginUser.getId()); + throw new RuntimeException("SSE 연결 오류 발생"); } - Map> notificationMap = - unnotifiedNotificationList - .stream() - .collect(Collectors.groupingBy(Notification::getUser)); + return sseEmitter; + } - for (Map.Entry> entry : notificationMap.entrySet()) { - User user = entry.getKey(); - List notifications = entry.getValue(); + @Transactional + public void sendNotification(User user, NotificationType type, String relatedUrl) { + SseEmitter sseEmitter = emitterRepository.findById(user.getId()); + Notification notification = new Notification(type.getContent(), relatedUrl, type, user); + notificationRepository.save(notification); - asyncNotificationService.sendNotificationMail(user, notifications); + try { + sseEmitter.send( + SseEmitter.event() + .id(user.getId().toString()) + .name(notification.getType().getName()) + .data(NotificationResponseDto.toDto(notification)) + ); + } catch (IOException e) { + emitterRepository.deleteById(user.getId()); + throw new RuntimeException("SSE 연결 오류 발생"); } } } diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index 0138321..d6bc67d 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.reply.service; +import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; import com.example.gamemate.domain.notification.enums.NotificationType; @@ -42,7 +43,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest if(requestDto.getParentReplyId()==null){ newReply = new Reply(requestDto.getContent(), findComment, loginUser); Reply createReply = replyRepository.save(newReply); - createCommentNotification(findComment.getBoard().getUser(), findComment.getUser()); + sendCommentNotification(findComment.getBoard(), findComment, createReply); return new ReplyResponseDto( createReply.getReplyId(), @@ -57,7 +58,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest .orElseThrow(()-> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); newReply = new Reply(requestDto.getContent(), findComment, loginUser, findParentReply); Reply createReply = replyRepository.save(newReply); - createCommentNotification(findComment.getBoard().getUser(), findComment.getUser(), findParentReply.getUser()); + sendReplyNotification(findComment.getBoard(), findComment, findParentReply); return new ReplyResponseDto( createReply.getReplyId(), @@ -109,15 +110,17 @@ public void deleteReply(User loginUser, Long id) { } // 대댓글 알림 전송 - private void createCommentNotification(User board, User comment) { - notificationService.createNotification(board, NotificationType.NEW_COMMENT); - notificationService.createNotification(comment, NotificationType.NEW_COMMENT); + @Transactional + public void sendCommentNotification(Board board, Comment comment, Reply reply) { + notificationService.sendNotification(board.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); + notificationService.sendNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); } - // 대댓글 알림 전송 - private void createCommentNotification(User board, User comment, User reply) { - notificationService.createNotification(board, NotificationType.NEW_COMMENT); - notificationService.createNotification(comment, NotificationType.NEW_COMMENT); - notificationService.createNotification(reply, NotificationType.NEW_COMMENT); + // 대대댓글 알림 전송 + @Transactional + public void sendReplyNotification(Board board, Comment comment, Reply reply) { + notificationService.sendNotification(board.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); + notificationService.sendNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); } } diff --git a/src/main/java/com/example/gamemate/global/config/MailConfig.java b/src/main/java/com/example/gamemate/global/config/MailConfig.java deleted file mode 100644 index 92fa5d4..0000000 --- a/src/main/java/com/example/gamemate/global/config/MailConfig.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.gamemate.global.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.JavaMailSenderImpl; - -import java.util.Properties; - -@Configuration -@RequiredArgsConstructor -public class MailConfig { - - private static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; - private static final String MAIL_DEBUG = "mail.smtp.debug"; - private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectionTimeout"; - private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable"; - - // SMTP 서버 - @Value("${spring.mail.host}") - private String host; - - // 계정 - @Value("${spring.mail.username}") - private String username; - - // 비밀번호 - @Value("${spring.mail.password}") - private String password; - - // 포트번호 - @Value("${spring.mail.port}") - private int port; - - @Value("${spring.mail.properties.mail.smtp.auth}") - private boolean auth; - - @Value("${spring.mail.properties.mail.smtp.debug}") - private boolean debug; - - @Value("${spring.mail.properties.mail.smtp.connectionTimeout}") - private int connectionTimeout; - - @Value("${spring.mail.properties.mail.starttls.enable}") - private boolean startTlsEnable; - - @Bean - public JavaMailSender javaMailService() { - JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); - javaMailSender.setHost(host); - javaMailSender.setUsername(username); - javaMailSender.setPassword(password); - javaMailSender.setPort(port); - - Properties properties = javaMailSender.getJavaMailProperties(); - properties.put(MAIL_SMTP_AUTH, auth); - properties.put(MAIL_DEBUG, debug); - properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout); - properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable); - - javaMailSender.setJavaMailProperties(properties); - javaMailSender.setDefaultEncoding("UTF-8"); - - return javaMailSender; - } -} - diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 630b42b..a7b2c22 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -71,3 +71,9 @@ logging.level.com.example.gamemate=DEBUG gemini.api.url=${GEMINI_URL} gemini.api.key=${GEMINI_KEY} +#S3 +cloud.aws.credentials.access-key=${AWS_ACCESS_KEY} +cloud.aws.credentials.secret-key=${AWS_SECRET_KEY} +cloud.aws.s3.bucket=${AWS_BUCKET} +cloud.aws.region.static=${AWS_REGION} +cloud.aws.stack.auto=${AWS_STACK_AUTO} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 7601d74..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,11 +0,0 @@ -cloud: - aws: - credentials: - access-key: YOUR_ACCESS_KEY - secret-key: YOUR_SECRET_KEY - s3: - bucket: YOUR_BUCKET_NAME - region: - static: ap-northeast-2 - stack: - auto: false From 18ce62f4081472c53cf47b436faec10b61e9c9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 11:37:16 +0900 Subject: [PATCH 138/215] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/global/constant/ErrorCode.java | 2 +- .../global/exception/GlobalExceptionHandler.java | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 1845935..ab8dcb9 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -28,7 +28,7 @@ public enum ErrorCode { /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), - NO_SESSION(HttpStatus.UNAUTHORIZED, "NO_SESSION","로그인이 필요합니다."), + NO_TOKEN(HttpStatus.UNAUTHORIZED, "NO_TOKEN","로그인이 필요합니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "INVALID_TOKEN", "유효하지 않은 토큰입니다."), /* 403 권한 없음 */ diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 18327ad..66336dc 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -4,27 +4,20 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; import lombok.extern.slf4j.Slf4j; -import org.apache.coyote.Response; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import javax.security.sasl.AuthenticationException; import java.security.SignatureException; import java.util.List; import java.util.stream.Collectors; -import static com.example.gamemate.global.constant.ErrorCode.NO_SESSION; - @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @@ -52,7 +45,7 @@ public ResponseEntity handleIllegalArgument(IllegalArgumentException e) @ExceptionHandler(RuntimeException.class) public ResponseEntity handleIRuntime(RuntimeException e) { log.warn("handleIRuntime", e); - ErrorCode errorCode = ErrorCode.NO_SESSION; + ErrorCode errorCode = ErrorCode.UNAUTHORIZED; return handleExceptionInternal(errorCode, errorCode.getMessage()); } From 39f9aa071a8060201c921c65d90ecbc699caa891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 13:40:43 +0900 Subject: [PATCH 139/215] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일 로그인 명칭 로컬 로그인으로 변경 - 소셜 로그인 후 비밀번호 설정을 통해 로컬 로그인 가능 - 비밀번호 설정 html --- .../auth/controller/AuthController.java | 19 +- ...uestDto.java => LocalLoginRequestDto.java} | 3 +- ...nseDto.java => LocalLoginResponseDto.java} | 2 +- .../auth/dto/OAuth2PasswordSetRequestDto.java | 16 ++ .../domain/auth/service/AuthService.java | 22 ++- .../domain/auth/service/OAuth2Service.java | 9 +- .../domain/auth/service/TokenService.java | 6 +- .../gamemate/domain/user/entity/User.java | 6 +- .../domain/user/enums/AuthProvider.java | 2 +- .../global/config/SecurityConfig.java | 3 +- .../config/auth/OAuth2FailureHandler.java | 2 +- .../config/auth/OAuth2SuccessHandler.java | 20 +- .../gamemate/global/constant/ErrorCode.java | 5 +- src/main/resources/application.properties | 5 +- src/main/resources/data.sql | 12 +- src/main/resources/static/oauth2-login.html | 2 + .../resources/static/oauth2-set-password.html | 171 ++++++++++++++++++ 17 files changed, 267 insertions(+), 38 deletions(-) rename src/main/java/com/example/gamemate/domain/auth/dto/{EmailLoginRequestDto.java => LocalLoginRequestDto.java} (82%) rename src/main/java/com/example/gamemate/domain/auth/dto/{EmailLoginResponseDto.java => LocalLoginResponseDto.java} (83%) create mode 100644 src/main/java/com/example/gamemate/domain/auth/dto/OAuth2PasswordSetRequestDto.java create mode 100644 src/main/resources/static/oauth2-set-password.html diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index f500683..0d71fd7 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -4,8 +4,6 @@ import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.auth.service.EmailService; import com.example.gamemate.domain.auth.service.OAuth2Service; -import com.example.gamemate.domain.auth.service.TokenService; -import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -50,14 +48,23 @@ public ResponseEntity verifyEmail( @PostMapping("/login") - public ResponseEntity emailLogin( - @Valid @RequestBody EmailLoginRequestDto requestDto, + public ResponseEntity localLogin( + @Valid @RequestBody LocalLoginRequestDto requestDto, HttpServletResponse response ) { - EmailLoginResponseDto responseDto = authService.emailLogin(requestDto, response); + LocalLoginResponseDto responseDto = authService.localLogin(requestDto, response); return new ResponseEntity<>(responseDto, HttpStatus.OK); } + @PostMapping("/oauth2/set-password") + public ResponseEntity setPassword( + @Valid @RequestBody OAuth2PasswordSetRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + authService.setOAuth2Password(customUserDetails.getUser(), requestDto.getPassword()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + @PostMapping("/logout") public ResponseEntity logout( @AuthenticationPrincipal CustomUserDetails customUserDetails, @@ -76,4 +83,4 @@ public ResponseEntity refreshToken( return new ResponseEntity<>(responseDto, HttpStatus.OK); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginRequestDto.java similarity index 82% rename from src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java rename to src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginRequestDto.java index d27fb61..885417e 100644 --- a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginRequestDto.java @@ -1,13 +1,12 @@ package com.example.gamemate.domain.auth.dto; -import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor -public class EmailLoginRequestDto { +public class LocalLoginRequestDto { @NotBlank(message = "이메일을 입력해주세요.") private final String email; diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java similarity index 83% rename from src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java rename to src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java index 018193c..606c9b4 100644 --- a/src/main/java/com/example/gamemate/domain/auth/dto/EmailLoginResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java @@ -5,7 +5,7 @@ @Getter @RequiredArgsConstructor -public class EmailLoginResponseDto { +public class LocalLoginResponseDto { private final String accessToken; diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2PasswordSetRequestDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2PasswordSetRequestDto.java new file mode 100644 index 0000000..299fbdb --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/dto/OAuth2PasswordSetRequestDto.java @@ -0,0 +1,16 @@ +package com.example.gamemate.domain.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OAuth2PasswordSetRequestDto { + + @NotBlank(message = "비밀번호를 입력해주세요.") +// @Size(min = 8, message = "비밀번호는 8글자 이상으로 입력해주세요.") +// @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*[!?@#$%^&*+=-])(?=.*\\d).{8,15}$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자 최소 1글자씩 입력해주세요.") + private final String password; + +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index 2f23351..85ea800 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -2,6 +2,7 @@ import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; @@ -10,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -55,7 +57,7 @@ public SignupResponseDto signup(SignupRequestDto requestDto) { return new SignupResponseDto(savedUser); } - public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto, HttpServletResponse response) { + public LocalLoginResponseDto localLogin(LocalLoginRequestDto requestDto, HttpServletResponse response) { User findUser = userRepository.findByEmail(requestDto.getEmail()) .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -64,12 +66,30 @@ public EmailLoginResponseDto emailLogin(EmailLoginRequestDto requestDto, HttpSer throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } + if (findUser.getPassword().equals("OAUTH2_USER")) { + throw new ApiException(ErrorCode.SOCIAL_PASSWORD_REQUIRED); + } + if(!passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) { throw new ApiException(ErrorCode.INVALID_PASSWORD); } return tokenService.generateLoginTokens(findUser, response); } + public void setOAuth2Password(User user, String password) { + if(user.getProvider() == AuthProvider.LOCAL) { + throw new ApiException(ErrorCode.SOCIAL_PASSWORD_FORBIDDEN); + } + + if(!"OAUTH2_USER".equals(user.getPassword())) { + throw new ApiException(ErrorCode.SOCIAL_PASSWORD_ALREADY_SET); + } + + String encodedPassword = passwordEncoder.encode(password); + user.updatePassword(encodedPassword); + userRepository.save(user); + } + public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { if(!jwtTokenProvider.validateToken(refreshToken)) { throw new ApiException(ErrorCode.INVALID_TOKEN); diff --git a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java index 9db4ab0..11fc774 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java @@ -9,17 +9,13 @@ import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.util.UriComponentsBuilder; import java.util.Map; import java.util.Optional; -import java.util.UUID; @Slf4j @Service @@ -28,7 +24,6 @@ public class OAuth2Service { private final UserRepository userRepository; - private final OAuth2ClientProperties clientProperties; public OAuth2LoginResponseDto extractOAuth2Attributes(AuthProvider provider, Map attributes) { if(provider == AuthProvider.GOOGLE) { diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java index 49761fc..114b503 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -1,6 +1,6 @@ package com.example.gamemate.domain.auth.service; -import com.example.gamemate.domain.auth.dto.EmailLoginResponseDto; +import com.example.gamemate.domain.auth.dto.LocalLoginResponseDto; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.provider.JwtTokenProvider; @@ -27,7 +27,7 @@ public class TokenService { private final Set blacklist = new ConcurrentHashMap().newKeySet(); private final Map tokenExpirations = new ConcurrentHashMap<>(); - public EmailLoginResponseDto generateLoginTokens(User user, HttpServletResponse response) { + public LocalLoginResponseDto generateLoginTokens(User user, HttpServletResponse response) { String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); @@ -35,7 +35,7 @@ public EmailLoginResponseDto generateLoginTokens(User user, HttpServletResponse userRepository.save(user); addRefreshTokenToCookie(response, refreshToken); - return new EmailLoginResponseDto(accessToken); + return new LocalLoginResponseDto(accessToken); } public String extractRefreshTokenFromCookie(HttpServletRequest request) { diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index f7fb2d9..6a29543 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -61,7 +61,7 @@ public User(String email, String name, String nickname, String password) { this.name = name; this.nickname = nickname; this.password = password; - this.provider = AuthProvider.EMAIL; + this.provider = AuthProvider.LOCAL; this.providerId = null; this.role = Role.USER; this.isPremium = false; @@ -106,4 +106,8 @@ public void removeRefreshToken() { this.refreshToken = null; } + public void setOAuthPassword(String password) { + this.password = password; + } + } diff --git a/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java index acb0e39..04a45f7 100644 --- a/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java +++ b/src/main/java/com/example/gamemate/domain/user/enums/AuthProvider.java @@ -7,7 +7,7 @@ @Getter public enum AuthProvider { - EMAIL("email"), + LOCAL("local"), GOOGLE("google"), KAKAO("kakao"); diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index 02baf47..e502417 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; @@ -50,7 +51,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/v3/api-docs/**", "/swagger-resources/**" ,"/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/auth/signup", "/auth/login", "/auth/refresh", "/auth/email/**").permitAll() .requestMatchers("/oauth2/**", "/login/oauth2/**", "/auth/oauth2/**").permitAll() - .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html").permitAll() + .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html", "/oauth2-set-password.html").permitAll() .requestMatchers(HttpMethod.POST,"/games/requests").hasRole("USER") .requestMatchers("/games", "/games/{id}").hasRole("ADMIN") .requestMatchers("/games/requests/**").hasRole("ADMIN") diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java index 3e16350..ad271e0 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2FailureHandler.java @@ -19,7 +19,7 @@ @RequiredArgsConstructor public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler { - @Value("${oauth2.failure.redirect.uri}") + @Value("${oauth2.failure.redirect-uri}") private String redirectUri; @Override diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java index 3315dba..935c841 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -25,8 +25,11 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final UserRepository userRepository; private int refreshTokenMaxAge = 60 * 60 * 24 * 3; //3일 - @Value("${oauth2.success.redirect.uri}") - private String redirectUri; + @Value("${oauth2.success.redirect-uri}") + private String successRedirectUri; + + @Value("${oauth2.set-password.redirect-uri}") + private String passwordSetupRedirectUri; @Override public void onAuthenticationSuccess( @@ -48,9 +51,16 @@ public void onAuthenticationSuccess( // 쿠키에 Refresh 토큰 저장 addRefreshTokenCookie(response, refreshToken); - String targetUrl = UriComponentsBuilder.fromUriString(redirectUri) - .queryParam("token", accessToken) - .build(false).toUriString(); + String targetUrl; + if("OAUTH2_USER".equals(user.getPassword())) { + targetUrl = UriComponentsBuilder.fromUriString(passwordSetupRedirectUri) + .queryParam("token", accessToken) + .build(false).toUriString(); + } else { + targetUrl = UriComponentsBuilder.fromUriString(successRedirectUri) + .queryParam("token", accessToken) + .build(false).toUriString(); + } getRedirectStrategy().sendRedirect(request, response, targetUrl); diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index ab8dcb9..09e3e8f 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -22,9 +22,11 @@ public enum ErrorCode { INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST,"INVALID_PROVIDER_TYPE", "지원하지 않는 서비스 제공자입니다."), INVALID_OAUTH2_ATTRIBUTE(HttpStatus.BAD_REQUEST, "INVALID_OAUTH2_ATTRIBUTE", "인증 정보가 유효하지 않습니다."), VERIFICATION_TIME_EXPIRED(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 시간이 만료되었습니다."), - VERIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "VERIFICATION_TIME_EXPIRED", "인증 정보를 찾을 수 없습니다."), + VERIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "VERIFICATION_NOT_FOUND", "인증 정보를 찾을 수 없습니다."), INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE", "인증 코드가 일치하지 않습니다."), EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "EMAIL_NOT_VERIFIED", "이메일 인증이 필요합니다"), + SOCIAL_PASSWORD_ALREADY_SET(HttpStatus.BAD_REQUEST, "SOCIAL_PASSWORD_ALREADY_SET", "이미 비밀번호가 설정되었습니다."), + SOCIAL_PASSWORD_REQUIRED(HttpStatus.BAD_REQUEST, "SOCIAL_PASSWORD_REQUIRED", "소셜 로그인 계정은 비밀번호 설정이 필요합니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), @@ -33,6 +35,7 @@ public enum ErrorCode { /* 403 권한 없음 */ FORBIDDEN(HttpStatus.FORBIDDEN, "FORBIDDEN", "권한이 없습니다."), + SOCIAL_PASSWORD_FORBIDDEN(HttpStatus.FORBIDDEN, "SOCIAL_PASSWORD_FORBIDDEN", "소셜 로그인 계정의 비밀번호를 설정할 수 없습니다."), /* 404 찾을 수 없음 */ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "회원을 찾을 수 없습니다."), diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8a13ef7..c999a47 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -47,8 +47,9 @@ spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.co spring.security.oauth2.client.provider.kakao.user-name-attribute=id #oauth2.redirect.uri=http://localhost:3000/oauth2/callback -oauth2.success.redirect.uri=http://localhost:8080/oauth2-login-success.html -oauth2.failure.redirect.uri=http://localhost:8080/oauth2-login-failure.html +oauth2.success.redirect-uri=http://localhost:8080/oauth2-login-success.html +oauth2.failure.redirect-uri=http://localhost:8080/oauth2-login-failure.html +oauth2.set-password.redirect-uri=http://localhost:8080/oauth2-set-password.html # EMAIL spring.mail.host=smtp.gmail.com diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index ff514ab..9f4d7c0 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -9,12 +9,12 @@ INSERT INTO user ( user_status, provider ) VALUES - ('user1@test.com', '유저1', '유저닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'EMAIL'), - ('user2@test.com', '유저2', '유저닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'EMAIL'), - ('user3@test.com', '유저3', '유저닉3', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'EMAIL'), - ('user4@test.com', '유저4', '유저닉4', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'EMAIL'), - ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', true, 'ACTIVE', 'EMAIL'), - ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', false, 'ACTIVE', 'EMAIL'); + ('user1@test.com', '유저1', '유저닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'LOCAL'), + ('user2@test.com', '유저2', '유저닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'LOCAL'), + ('user3@test.com', '유저3', '유저닉3', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'LOCAL'), + ('user4@test.com', '유저4', '유저닉4', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'LOCAL'), + ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', true, 'ACTIVE', 'LOCAL'), + ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', false, 'ACTIVE', 'LOCAL'); -- MatchUserInfo 테이블 데이터 INSERT INTO match_user_info ( diff --git a/src/main/resources/static/oauth2-login.html b/src/main/resources/static/oauth2-login.html index 44a0338..529b7b9 100644 --- a/src/main/resources/static/oauth2-login.html +++ b/src/main/resources/static/oauth2-login.html @@ -4,10 +4,12 @@ + GM 로그인 페이지 + + +
+

비밀번호 설정

+
+
+ + +
+ 비밀번호는 8~20자의 영문, 숫자, 특수문자를 포함해야 합니다. +
+
+
+
+ + +
+
+ +
+
+ + + + \ No newline at end of file From 14956d6898eb5eabd8d035e9c221558717100c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 13:48:56 +0900 Subject: [PATCH 140/215] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로컬 계정 사용자가 같은 이메일로 소셜 로그인 시도 시 계정 통합 --- .../example/gamemate/domain/auth/service/OAuth2Service.java | 6 ++++++ .../java/com/example/gamemate/domain/user/entity/User.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java index 11fc774..438f7a1 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/OAuth2Service.java @@ -47,6 +47,12 @@ public User processOAuth2User(OAuth2LoginResponseDto responseDto) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } + // 이메일로 가입한 계정이 소셜 로그인 시도한 경우 + if (existingUser.getProvider() == AuthProvider.LOCAL) { + existingUser.integrateOAuthProvider(responseDto.getProvider(), responseDto.getProviderId()); + return userRepository.save(existingUser); + } + // 다른 OAuth 제공자로 로그인 시도한 경우 if (!existingUser.getProvider().equals(responseDto.getProvider())) { throw new ApiException(ErrorCode.INVALID_PROVIDER_TYPE); diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index 6a29543..e9ec8fd 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -106,6 +106,11 @@ public void removeRefreshToken() { this.refreshToken = null; } + public void integrateOAuthProvider(AuthProvider provider, String providerId) { + this.provider = provider; + this.providerId = providerId; + } + public void setOAuthPassword(String password) { this.password = password; } From 5d94cf236e6b0f858f7f198e0d13d74b83ed0f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 22 Jan 2025 18:52:05 +0900 Subject: [PATCH 141/215] =?UTF-8?q?refactor:=20markDeletedAt=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=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/gamemate/domain/user/entity/User.java | 4 ---- .../com/example/gamemate/domain/user/service/UserService.java | 1 - .../java/com/example/gamemate/global/common/BaseEntity.java | 4 ---- 3 files changed, 9 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index f7fb2d9..f1ed5b5 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -94,10 +94,6 @@ public void updateUserStatus(UserStatus status) { this.userStatus = status; } - public void deleteSoftly() { - markDeletedAt(); - } - public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index afc4934..48dfeb1 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -65,7 +65,6 @@ public void updatePassword(Long id, String oldPassword, String newPassword, User public void withdrawUser(User loginUser) { - loginUser.deleteSoftly(); loginUser.updateUserStatus(UserStatus.WITHDRAW); loginUser.removeRefreshToken(); diff --git a/src/main/java/com/example/gamemate/global/common/BaseEntity.java b/src/main/java/com/example/gamemate/global/common/BaseEntity.java index f1a29c6..2ae2ec7 100644 --- a/src/main/java/com/example/gamemate/global/common/BaseEntity.java +++ b/src/main/java/com/example/gamemate/global/common/BaseEntity.java @@ -22,8 +22,4 @@ public abstract class BaseEntity { @LastModifiedDate private LocalDateTime modifiedAt; - public void markDeletedAt() { - this.modifiedAt = LocalDateTime.now(); - } - } From 66c1dae58f4a80aeaa51c5e385cc52777c3b826c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 14:56:46 +0900 Subject: [PATCH 142/215] =?UTF-8?q?docs:=20JavaDoc=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserController - UserService --- .../user/controller/UserController.java | 33 +++++++++++++++++++ .../domain/user/service/UserService.java | 32 ++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index 45256c8..2729e5a 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -15,6 +15,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +/** + * 사용자와 관련된 요청을 처리하는 컨트롤러입니다. + * 프로필 조회, 프로필 수정, 비밀번호 변경, 회원 탈퇴 기능을 제공합니다. + */ @RestController @RequiredArgsConstructor @RequestMapping("/users") @@ -22,6 +26,12 @@ public class UserController { private final UserService userService; private final AuthService authService; + /** + * 사용자 프로필을 조회합니다. + * @param id 조회할 사용자의 ID + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 조회된 사용자 프로필 정보 + */ @GetMapping("/{id}") public ResponseEntity findProfile( @PathVariable Long id, @@ -31,6 +41,14 @@ public ResponseEntity findProfile( return new ResponseEntity<>(responseDto, HttpStatus.OK); } + /** + * 사용자의 프로필을 수정합니다. + * @param id 수정할 사용자의 ID + * @param requestDto 프로필 수정 요청 정보 + * (새 닉네임) + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 처리 결과 상태 코드 + */ @PatchMapping("/{id}") public ResponseEntity updateProfile( @PathVariable Long id, @@ -41,6 +59,14 @@ public ResponseEntity updateProfile( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 사용자의 비밀번호를 변경합니다. + * @param id 변경할 사용자의 ID + * @param requestDto 비밀번호 변경 요청 정보 + * (기존 비밀번호, 새 비밀번호) + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 처리 결과 상태 코드 + */ @PatchMapping("/{id}/password") public ResponseEntity updatePassword( @PathVariable Long id, @@ -51,6 +77,13 @@ public ResponseEntity updatePassword( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 사용자의 탈퇴 요청을 처리합니다. + * @param customUserDetails 현재 인증된 사용자 정보 + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + * @return 처리 결과 상태 코드 + */ @DeleteMapping("/withdraw") public ResponseEntity withdraw( @AuthenticationPrincipal CustomUserDetails customUserDetails, diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index 48dfeb1..32fc3dc 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -13,6 +13,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/** + * 사용자 관련 비즈니스 로직을 처리하는 서비스 클래스입니다. + * 프로필 조회, 프로필 수정, 비밀번호 변경, 회원 탈퇴 등의 작업을 수행합니다. + */ @Service @RequiredArgsConstructor @Transactional @@ -23,6 +27,12 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final AuthService authService; + /** + * 사용자의 프로필을 조회합니다. + * @param id 조회할 사용자의 ID + * @param loginUser 현재 로그인한 사용자 + * @return 조회된 사용자의 프로필 정보 + */ @Transactional(readOnly = true) public ProfileResponseDto findProfile(Long id, User loginUser) { @@ -35,6 +45,12 @@ public ProfileResponseDto findProfile(Long id, User loginUser) { return new ProfileResponseDto(findUser); } + /** + * 사용자의 프로필을 수정합니다. + * @param id 수정할 사용자의 ID + * @param newNickname 새 닉네임 + * @param loginUser 현재 로그인한 사용자 + */ public void updateProfile(Long id, String newNickname, User loginUser) { User findUser = userRepository.findById(id) @@ -47,6 +63,13 @@ public void updateProfile(Long id, String newNickname, User loginUser) { } + /** + * 사용자의 비밀번호를 변경합니다. + * @param id 변경할 사용자의 ID + * @param oldPassword 기존 비밀번호 + * @param newPassword 새 비밀번호 + * @param loginUser 현재 로그인한 사용자 + */ public void updatePassword(Long id, String oldPassword, String newPassword, User loginUser) { User findUser = userRepository.findById(id) @@ -63,6 +86,10 @@ public void updatePassword(Long id, String oldPassword, String newPassword, User userRepository.save(findUser); } + /** + * 사용자의 탈퇴 요청을 처리합니다. + * @param loginUser 현재 로그인한 사용자 + */ public void withdrawUser(User loginUser) { loginUser.updateUserStatus(UserStatus.WITHDRAW); @@ -78,6 +105,11 @@ public void withdrawUser(User loginUser) { // } // } + /** + * 사용자가 일치하는지 확인합니다. + * @param user 확인할 사용자 + * @param loginUser 현재 로그인한 사용자 + */ private void validateOwner(User user, User loginUser) { if(!user.getEmail().equals(loginUser.getEmail())) { throw new ApiException(ErrorCode.FORBIDDEN); From 4b97df202a9b2a038f0b2155b2f55cb22cfefa49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 15:15:05 +0900 Subject: [PATCH 143/215] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=B3=80=EC=88=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/domain/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index 32fc3dc..48b2422 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -59,7 +59,7 @@ public void updateProfile(Long id, String newNickname, User loginUser) { validateOwner(findUser, loginUser); findUser.updateProfile(newNickname); - User savedUser = userRepository.save(findUser); + userRepository.save(findUser); } From 2fca9aebb6a137c2b574d98ad5c662519196716f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 16:04:54 +0900 Subject: [PATCH 144/215] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20MailConfig=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/global/config/MailConfig.java | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/global/config/MailConfig.java diff --git a/src/main/java/com/example/gamemate/global/config/MailConfig.java b/src/main/java/com/example/gamemate/global/config/MailConfig.java deleted file mode 100644 index 92fa5d4..0000000 --- a/src/main/java/com/example/gamemate/global/config/MailConfig.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.gamemate.global.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.JavaMailSenderImpl; - -import java.util.Properties; - -@Configuration -@RequiredArgsConstructor -public class MailConfig { - - private static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; - private static final String MAIL_DEBUG = "mail.smtp.debug"; - private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectionTimeout"; - private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable"; - - // SMTP 서버 - @Value("${spring.mail.host}") - private String host; - - // 계정 - @Value("${spring.mail.username}") - private String username; - - // 비밀번호 - @Value("${spring.mail.password}") - private String password; - - // 포트번호 - @Value("${spring.mail.port}") - private int port; - - @Value("${spring.mail.properties.mail.smtp.auth}") - private boolean auth; - - @Value("${spring.mail.properties.mail.smtp.debug}") - private boolean debug; - - @Value("${spring.mail.properties.mail.smtp.connectionTimeout}") - private int connectionTimeout; - - @Value("${spring.mail.properties.mail.starttls.enable}") - private boolean startTlsEnable; - - @Bean - public JavaMailSender javaMailService() { - JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); - javaMailSender.setHost(host); - javaMailSender.setUsername(username); - javaMailSender.setPassword(password); - javaMailSender.setPort(port); - - Properties properties = javaMailSender.getJavaMailProperties(); - properties.put(MAIL_SMTP_AUTH, auth); - properties.put(MAIL_DEBUG, debug); - properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout); - properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable); - - javaMailSender.setJavaMailProperties(properties); - javaMailSender.setDefaultEncoding("UTF-8"); - - return javaMailSender; - } -} - From 47ba864c16f00ace58b4a4139a8b550a70232044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 16:42:33 +0900 Subject: [PATCH 145/215] =?UTF-8?q?fix:=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=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 --- src/main/resources/data.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 9f4d7c0..bf33282 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -9,12 +9,12 @@ INSERT INTO user ( user_status, provider ) VALUES - ('user1@test.com', '유저1', '유저닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'LOCAL'), - ('user2@test.com', '유저2', '유저닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'LOCAL'), - ('user3@test.com', '유저3', '유저닉3', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', false, 'ACTIVE', 'LOCAL'), - ('user4@test.com', '유저4', '유저닉4', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'USER', true, 'ACTIVE', 'LOCAL'), - ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', true, 'ACTIVE', 'LOCAL'), - ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$vY7OYcTQsE8YdrYv4vryzeG0.7LqJHv9LnCVDY7f9QXF0nF.HXK52', 'ADMIN', false, 'ACTIVE', 'LOCAL'); + ('user1@test.com', '유저1', '유저닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL'), + ('user2@test.com', '유저2', '유저닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL'), + ('user3@test.com', '유저3', '유저닉3', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL'), + ('user4@test.com', '유저4', '유저닉4', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL'), + ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', true, 'ACTIVE', 'LOCAL'), + ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', false, 'ACTIVE', 'LOCAL'); -- MatchUserInfo 테이블 데이터 INSERT INTO match_user_info ( From 7655be062e6ea12abd7ff64bf6d72fe96365a716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 27 Jan 2025 17:54:31 +0900 Subject: [PATCH 146/215] =?UTF-8?q?fix:=20=EA=B2=8C=EC=9E=84=20API=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95=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 --- .../com/example/gamemate/global/config/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index e502417..e916311 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -52,9 +52,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/signup", "/auth/login", "/auth/refresh", "/auth/email/**").permitAll() .requestMatchers("/oauth2/**", "/login/oauth2/**", "/auth/oauth2/**").permitAll() .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html", "/oauth2-set-password.html").permitAll() + .requestMatchers(HttpMethod.GET,"/games", "/games/{id}").hasRole("USER") .requestMatchers(HttpMethod.POST,"/games/requests").hasRole("USER") - .requestMatchers("/games", "/games/{id}").hasRole("ADMIN") + .requestMatchers("/games/recommendations/**").hasRole("USER") .requestMatchers("/games/requests/**").hasRole("ADMIN") + .requestMatchers("/games/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 From 89fc1e895870532b7172c29347fe3f142d4bcdd0 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Mon, 27 Jan 2025 19:29:39 +0900 Subject: [PATCH 147/215] =?UTF-8?q?refact:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20/=20=EB=8C=93=EA=B8=80=20/=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81,=20=EC=9E=90=EB=B0=94?= =?UTF-8?q?=EB=8F=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게시글 / 댓글 / 대댓글 자바독 추가 2. entity id로 변경 3. 게시글 단건 조회 시 이미지 정보도 함께 조회되도록 수정 --- .../board/controller/BoardController.java | 48 +++++++++++----- .../board/dto/BoardFindAllResponseDto.java | 2 +- .../board/dto/BoardFindOneResponseDto.java | 26 ++++++++- .../domain/board/dto/BoardRequestDto.java | 1 + .../gamemate/domain/board/entity/Board.java | 2 +- .../domain/board/service/BoardService.java | 55 ++++++++----------- .../controller/BoardImageController.java | 29 +++++----- .../domain/boardImage/entity/BoardImage.java | 2 +- .../boardImage/service/BoardImageService.java | 22 ++++---- .../comment/controller/CommentController.java | 25 ++++++--- .../domain/comment/entity/Comment.java | 2 +- .../comment/service/CommentService.java | 42 ++++++++++---- .../dto/response/BoardLikeResponseDto.java | 2 +- .../like/repository/BoardLikeRepository.java | 6 +- .../domain/like/service/LikeService.java | 2 +- .../reply/controller/ReplyController.java | 21 ++++--- .../gamemate/domain/reply/entity/Reply.java | 2 +- .../domain/reply/service/ReplyService.java | 30 ++++++---- 18 files changed, 197 insertions(+), 122 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index 58ddf5e..1cc2344 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -16,6 +16,10 @@ import java.util.List; +/** + * 게시글 관련 API를 처리하는 컨트롤러 클래스입니다. + * 게시글의 생성, 조회, 수정, 삭제 기능을 제공합니다. + */ @RequiredArgsConstructor @RestController @RequestMapping("/boards") @@ -24,9 +28,11 @@ public class BoardController { private final BoardService boardService; /** - * 게시글 생성 API - * @param dto - * @return + * 게시글 생성 API + * + * @param dto 게시글 생성 dto + * @param customUserDetails 인증 정보 + * @return 생성된 게시글 정보를 포함한 ResponseEntity */ @PostMapping public ResponseEntity createBoard( @@ -39,8 +45,13 @@ public ResponseEntity createBoard( } /** - * 게시글 조회 - * @return + * 게시글 조회하고 검색하는 API + * + * @param page 페이지 번호(기본값 : 0) + * @param category 카테고리 종류 + * @param title 게시글 제목 + * @param content 게시글 내용 + * @return 게시글 목록을 포함한 ResponseEntity */ @GetMapping public ResponseEntity> findAllBoards( @@ -63,9 +74,10 @@ public ResponseEntity> findAllBoards( } /** - * 게시글 단건 조회 API - * @param id - * @return + * 게시글 단건 조회하는 API + * + * @param id 게시글 식별자 + * @return 게시글 ResponseEntity */ @GetMapping("/{id}") public ResponseEntity findBoardById( @@ -78,10 +90,14 @@ public ResponseEntity findBoardById( /** - * 게시글 업데이트 API - * @param id - * @param dto - * @return + * + /** + * 게시글 업데이트하는 API + * + * @param id 게시글 식별자 + * @param dto 게시글 업데이트 dto + * @param customUserDetails 인증 정보 + * @return Void ResponseEntity */ @PatchMapping("/{id}") public ResponseEntity updateBoard( @@ -95,9 +111,11 @@ public ResponseEntity updateBoard( } /** - * 게시글 삭제 API - * @param id - * @return + * 게시글 삭제하는 API + * + * @param id 게시글 식별자 + * @param customUserDetails 인증 정보 + * @return Void ResponseEntity */ @DeleteMapping("/{id}") public ResponseEntity deleteBoard( diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java index 916bd86..f682af3 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindAllResponseDto.java @@ -23,7 +23,7 @@ public BoardFindAllResponseDto(Long id, BoardCategory category, String title, Lo } public BoardFindAllResponseDto(Board board) { - this.id = board.getBoardId(); + this.id = board.getId(); this.category = board.getCategory(); this.title = board.getTitle(); this.createdAt = board.getCreatedAt(); diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java index 66fcefb..1222f69 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardFindOneResponseDto.java @@ -1,6 +1,8 @@ package com.example.gamemate.domain.board.dto; +import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.boardImage.entity.BoardImage; import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; import lombok.Getter; @@ -17,8 +19,10 @@ public class BoardFindOneResponseDto { private final String nickname; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; + private final List fileName; + private final List imageUrl; - public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime modifiedAt) { + public BoardFindOneResponseDto(Long id, BoardCategory category, String title, String content, String nickname, LocalDateTime createdAt, LocalDateTime modifiedAt, List fileName, List imageUrl) { this.id = id; this.category = category; this.title = title; @@ -26,5 +30,25 @@ public BoardFindOneResponseDto(Long id, BoardCategory category, String title, St this.nickname = nickname; this.createdAt = createdAt; this.modifiedAt = modifiedAt; + this.fileName = fileName; + this.imageUrl = imageUrl; + } + + public BoardFindOneResponseDto(Board board) { + this.id = board.getId(); + this.category = board.getCategory(); + this.title = board.getTitle(); + this.content = board.getContent(); + this.nickname = board.getUser().getNickname(); + this.createdAt = board.getCreatedAt(); + this.modifiedAt = board.getModifiedAt(); + this.fileName = board.getBoardImages().isEmpty() ? null : + board.getBoardImages().stream() + .map(BoardImage::getFileName) + .toList(); + this.imageUrl = board.getBoardImages().isEmpty() ? null : + board.getBoardImages().stream() + .map(BoardImage::getFilePath) + .toList(); } } diff --git a/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java b/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java index 96ded7a..8046759 100644 --- a/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java +++ b/src/main/java/com/example/gamemate/domain/board/dto/BoardRequestDto.java @@ -14,6 +14,7 @@ public class BoardRequestDto { @NotBlank(message = "내용을 입력하세요.") private final String content; + public BoardRequestDto(BoardCategory category, String title, String content) { this.category = category; this.title = title; diff --git a/src/main/java/com/example/gamemate/domain/board/entity/Board.java b/src/main/java/com/example/gamemate/domain/board/entity/Board.java index 09a50c3..9aa59af 100644 --- a/src/main/java/com/example/gamemate/domain/board/entity/Board.java +++ b/src/main/java/com/example/gamemate/domain/board/entity/Board.java @@ -18,7 +18,7 @@ public class Board extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long boardId; + private Long id; @Enumerated(EnumType.STRING) private BoardCategory category; diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index ccdfc7b..542639b 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -8,12 +8,6 @@ import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.enums.ListSize; import com.example.gamemate.domain.board.repository.BoardRepository; -import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; -import com.example.gamemate.domain.comment.entity.Comment; -import com.example.gamemate.domain.comment.repository.CommentRepository; -import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; -import com.example.gamemate.domain.reply.entity.Reply; -import com.example.gamemate.domain.reply.repository.ReplyRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; @@ -25,9 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; @@ -36,13 +28,13 @@ public class BoardService { private final BoardRepository boardRepository; - private final CommentRepository commentRepository; - private final ReplyRepository replyRepository; /** * 게시글 생성 메서드 - * @param dto - * @return + * + * @param loginUser 로그인한 유저 + * @param dto 게시글 생성 dto + * @return 게시글 생성 응답 ResponseDto */ @Transactional public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { @@ -50,7 +42,7 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { Board newBoard = new Board(dto.getCategory(),dto.getTitle(),dto.getContent(), loginUser); Board createdBoard = boardRepository.save(newBoard); return new BoardResponseDto( - createdBoard.getBoardId(), + createdBoard.getId(), createdBoard.getCategory(), createdBoard.getTitle(), createdBoard.getContent(), @@ -62,9 +54,12 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { /** * 게시판 리스트 조회 메서드 - * @param page - * @param category - * @return + * + * @param page 페이지 번호 (기본값 : 0) + * @param category 게시글 카테고리 + * @param title 게시글 제목 + * @param content 게시글 내용 + * @return 게시글 리스트 ResponseDto List */ public List findAllBoards(int page, BoardCategory category, String title, String content) { @@ -75,7 +70,7 @@ public List findAllBoards(int page, BoardCategory categ return boardPage.stream() .map(board -> new BoardFindAllResponseDto( - board.getBoardId(), + board.getId(), board.getCategory(), board.getTitle(), board.getCreatedAt(), @@ -86,30 +81,24 @@ public List findAllBoards(int page, BoardCategory categ /** * 게시글 단건 조회 메서드 - * @param id - * @return + * + * @param id 게시글 식별자 + * @return 게시글 조회 ResponseDto */ public BoardFindOneResponseDto findBoardById(Long id) { // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); - return new BoardFindOneResponseDto( - findBoard.getBoardId(), - findBoard.getCategory(), - findBoard.getTitle(), - findBoard.getContent(), - findBoard.getUser().getNickname(), - findBoard.getCreatedAt(), - findBoard.getModifiedAt() - ); + return new BoardFindOneResponseDto(findBoard); } /** * 게시글 업데이트 메서드 - * @param id - * @param dto - * @return + * + * @param loginUser 로그인한 유저 + * @param id 게시글 식별자 + * @param dto 게시글 업데이트 요청 Dto */ @Transactional public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { @@ -128,7 +117,9 @@ public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { /** * 게시글 삭제 메서드 - * @param id + * + * @param loginUser 로그인한 유저 + * @param id 게시글 식별자 */ @Transactional public void deleteBoard(User loginUser, Long id) { diff --git a/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java b/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java index 0524525..ab7d84a 100644 --- a/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java +++ b/src/main/java/com/example/gamemate/domain/boardImage/controller/BoardImageController.java @@ -19,10 +19,11 @@ public class BoardImageController { /** * 게시글 첨부파일 추가 API - * @param boardId - * @param image - * @return - * @throws IOException + * @param boardId 보드 식별자 + * @param image 게시글에 첨부할 이미지 + * @param customUserDetails 인증된 사용자 정보 + * @return String ResponseEntity + * @throws IOException 오류 발생 */ @PostMapping public ResponseEntity createBoardImage( @@ -36,10 +37,11 @@ public ResponseEntity createBoardImage( /** * 게시글 첨부파일 수정 API - * @param id - * @param image - * @param customUserDetails - * @return + * + * @param id 이미지 식별자 + * @param image 수정할 게시글 이미지 + * @param customUserDetails 인증된 사용자 정보 + * @return Void ResponseEntity */ @PutMapping("/{id}") public ResponseEntity updateBoardImage( @@ -53,11 +55,12 @@ public ResponseEntity updateBoardImage( /** - * 이미지 삭제 - * @param id - * @param customUserDetails - * @return - * @throws IOException + * 이미지 삭제 API + * + * @param id 이미지 식별자 + * @param customUserDetails 인증된 사용자 정보 + * @return Void ResponseEntity + * @throws IOException 오류 발생 */ @DeleteMapping("/{id}") public ResponseEntity deleteBoardImage( diff --git a/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java b/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java index aeb855d..6e88cbb 100644 --- a/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java +++ b/src/main/java/com/example/gamemate/domain/boardImage/entity/BoardImage.java @@ -13,7 +13,7 @@ public class BoardImage extends BaseCreatedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long BoardImageId; + private Long id; @ManyToOne @JoinColumn(name = "board_id") diff --git a/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java b/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java index 6bb927b..5fa8aa6 100644 --- a/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java +++ b/src/main/java/com/example/gamemate/domain/boardImage/service/BoardImageService.java @@ -27,10 +27,11 @@ public class BoardImageService { /** * 이미지 업로드 메서드 - * @param loginUser - * @param boardId - * @param image - * @throws IOException + * + * @param loginUser 로그인한 유저 + * @param boardId 게시글 식별자 + * @param image 게시글에 업로드할 이미지 + * @throws IOException 오류 발생 */ @Transactional public void createBoardImage(User loginUser, Long boardId, MultipartFile image) throws IOException { @@ -54,10 +55,11 @@ public void createBoardImage(User loginUser, Long boardId, MultipartFile image) /** * 이미지 업데이트 메서드 - * @param loginUser - * @param id - * @param image - * @throws IOException + * + * @param loginUser 로그인한 유저 + * @param id 이미지 식별자 + * @param image 게시글에 업데이트할 이미지 + * @throws IOException 오류 발생 */ @Transactional public void updateBoardImage(User loginUser, Long id, MultipartFile image) throws IOException { @@ -90,8 +92,8 @@ public void updateBoardImage(User loginUser, Long id, MultipartFile image) throw /** * 이미지 삭제 메서드 - * @param loginUser - * @param id + * @param loginUser 로그인한 유저 + * @param id 이미지 식별자 */ @Transactional public void deleteImage(User loginUser, Long id) { diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index e461219..66924e3 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -22,9 +22,9 @@ public class CommentController { /** * 댓글 생성 API - * @param boardId - * @param requestDto - * @return + * @param boardId 게시글 식별자 + * @param requestDto 댓글 요청 Dto + * @return 생성된 댓글 정보를 포함한 ResponseEntity */ @PostMapping public ResponseEntity createComment( @@ -38,9 +38,9 @@ public ResponseEntity createComment( /** * 댓글/대댓글 조회 - * @param boardId - * @param page - * @return + * @param boardId 댓글 식별자 + * @param page 페이지 번호(기본값 : 0) + * @return 댓글 리스트 ResponseEntity */ @GetMapping public ResponseEntity> getComments( @@ -53,9 +53,10 @@ public ResponseEntity> getComments( /** * 댓글 수정 API - * @param id - * @param requestDto - * @return + * + * @param id 댓글 식별자 + * @param requestDto 업데이트할 댓글 Dto + * @return Void */ @PatchMapping("/{id}") public ResponseEntity updateComment( @@ -67,6 +68,12 @@ public ResponseEntity updateComment( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + /** + * 댓글 삭제 API + * @param id 댓글 식별자 + * @param customUserDetails 인증된 사용자 정보 + * @return Void + */ @DeleteMapping("/{id}") public ResponseEntity deleteComment( @PathVariable Long id, diff --git a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java index bece5f7..2c1bd9c 100644 --- a/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java +++ b/src/main/java/com/example/gamemate/domain/comment/entity/Comment.java @@ -17,7 +17,7 @@ public class Comment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long commentId; + private Long id; @Column(nullable = false) private String content; diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index 0454aaa..a70444a 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -40,9 +40,10 @@ public class CommentService { /** * 댓글 생성 메서드 - * @param boardId - * @param requestDto - * @return + * @param loginUser 로그인한 유저 + * @param boardId 게시글 식별자 + * @param requestDto 댓글 생성할 requestDto + * @return CommentResponseDto */ @Transactional public CommentResponseDto createComment(User loginUser, Long boardId, CommentRequestDto requestDto) { @@ -55,7 +56,7 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq notificationService.createNotification(findBoard.getUser(), NotificationType.NEW_COMMENT); return new CommentResponseDto( - createComment.getCommentId(), + createComment.getId(), createComment.getContent(), createComment.getUser().getNickname(), createComment.getCreatedAt(), @@ -65,8 +66,10 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq /** * 댓글 업데이트 메서드 - * @param id - * @param requestDto + * + * @param loginUser 로그인한 유저 + * @param id 댓글 식별자 + * @param requestDto 업데이트할 댓글 dto */ @Transactional public void updateComment(User loginUser, Long id, CommentRequestDto requestDto) { @@ -85,7 +88,9 @@ public void updateComment(User loginUser, Long id, CommentRequestDto requestDto) /** * 댓글 삭제 메서드 - * @param id + * + * @param loginUser 로그인한 유저 + * @param id 댓글 식별자 */ @Transactional public void deleteComment(User loginUser, Long id) { @@ -103,9 +108,10 @@ public void deleteComment(User loginUser, Long id) { /** * 댓글 조회 메서드 - * @param boardId - * @param page - * @return + * + * @param boardId 게시글 식별자 + * @param page 페이지 번호(기본값 : 0) + * @return Comment 조회 Do */ public List getComments(Long boardId, int page) { // page는 댓글 페이지네이션을 위해 필요 @@ -123,6 +129,12 @@ public List getComments(Long boardId, int page) { .collect(Collectors.toList()); } + /** + * 댓글 Dto 변환 + * + * @param comment comment + * @return 댓글 조회 Dto + */ private CommentFindResponseDto convertCommentDto(Comment comment) { List replyDtos = Optional.ofNullable(replyRepository.findByComment(comment)) .orElse(Collections.emptyList()) @@ -130,7 +142,7 @@ private CommentFindResponseDto convertCommentDto(Comment comment) { .map(this::convertReplyDto) .collect(Collectors.toList()); return new CommentFindResponseDto( - comment.getCommentId(), + comment.getId(), comment.getContent(), comment.getUser().getNickname(), comment.getCreatedAt(), @@ -139,10 +151,16 @@ private CommentFindResponseDto convertCommentDto(Comment comment) { ); } + /** + * 대댓글 Dto 변환 + * + * @param reply 대댓글 + * @return 대댓글 조회 Dto + */ private ReplyFindResponseDto convertReplyDto(Reply reply) { String findUserName = reply.getParentReply() == null ? null : reply.getParentReply().getUser().getNickname(); return new ReplyFindResponseDto( - reply.getReplyId(), + reply.getId(), findUserName, reply.getContent(), reply.getCreatedAt(), diff --git a/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java index 212a571..13dea47 100644 --- a/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/like/dto/response/BoardLikeResponseDto.java @@ -12,7 +12,7 @@ public class BoardLikeResponseDto { public BoardLikeResponseDto(BoardLike boardLike){ - this.boardId = boardLike.getBoard().getBoardId(); + this.boardId = boardLike.getBoard().getId(); this.status = boardLike.getStatus(); this.userId = boardLike.getUser().getId(); } diff --git a/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java index 5d470fe..0cdf292 100644 --- a/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java +++ b/src/main/java/com/example/gamemate/domain/like/repository/BoardLikeRepository.java @@ -10,10 +10,10 @@ public interface BoardLikeRepository extends JpaRepository { - @Query("SELECT bl FROM BoardLike bl WHERE bl.board.boardId = :boardId AND bl.user.id = :userId") - Optional findByBoardIdAndUserId(@Param("boardId") Long boardId, @Param("userId") Long userId); + @Query("SELECT bl FROM BoardLike bl WHERE bl.board.id = :id AND bl.user.id = :userId") + Optional findByBoardIdAndUserId(@Param("id") Long boardId, @Param("userId") Long userId); - Long countByBoardBoardIdAndStatus(Long boardId, LikeStatus status); + Long countByBoardIdAndStatus(Long boardId, LikeStatus status); } diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 65159b1..a6e754e 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -79,7 +79,7 @@ public Long getBoardLikeCount(Long boardId) { boardRepository.findById(boardId) .orElseThrow(() -> new ApiException(ErrorCode.BOARD_NOT_FOUND)); - return boardLikeRepository.countByBoardBoardIdAndStatus(boardId, LikeStatus.LIKE); + return boardLikeRepository.countByBoardIdAndStatus(boardId, LikeStatus.LIKE); } public Long getReivewLikeCount(Long reviewId) { diff --git a/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java index ef1ea52..ddb0a6f 100644 --- a/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java +++ b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java @@ -20,9 +20,11 @@ public class ReplyController { /** * 대댓글 생성 API - * @param commentId - * @param requestDto - * @return + * + * @param commentId 댓글 식별자 + * @param requestDto 대댓글 생성 Dto + * @param customUserDetails 인증된 사용자 + * @return 대댓글 생성 ResponseEntity */ @PostMapping public ResponseEntity createReply( @@ -36,9 +38,11 @@ public ResponseEntity createReply( /** * 대댓글 수정 API - * @param id - * @param requestDto - * @return + * + * @param id 대댓글 식별자 + * @param requestDto 업데이트할 대댓글 Dto + * @param customUserDetails 인증된 사용자 + * @return Void */ @PatchMapping("/{id}") public ResponseEntity updateReply( @@ -52,8 +56,9 @@ public ResponseEntity updateReply( /** * 대댓글 삭제 API - * @param id - * @return + * @param id 대댓글 식별자 + * @param customUserDetails 인증된 사용자 + * @return Void */ @DeleteMapping("/{id}") public ResponseEntity deleteReply( diff --git a/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java b/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java index a542e6f..b970fd9 100644 --- a/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java +++ b/src/main/java/com/example/gamemate/domain/reply/entity/Reply.java @@ -16,7 +16,7 @@ public class Reply extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long replyId; + private Long id; @Column(nullable = false) private String content; diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index 0138321..a4b0e73 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -27,9 +27,11 @@ public class ReplyService { /** * 대댓글 생성 메서드 - * @param commentId - * @param requestDto - * @return + * + * @param loginUser 로그인한 유저 + * @param commentId 댓글 식별자 + * @param requestDto 댓글 생성 Dto + * @return 대댓글 생성 정보 Dto */ @Transactional public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequestDto requestDto) { @@ -45,8 +47,8 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest createCommentNotification(findComment.getBoard().getUser(), findComment.getUser()); return new ReplyResponseDto( - createReply.getReplyId(), - createReply.getComment().getCommentId(), + createReply.getId(), + createReply.getComment().getId(), createReply.getContent(), createReply.getCreatedAt(), createReply.getModifiedAt() @@ -60,9 +62,9 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest createCommentNotification(findComment.getBoard().getUser(), findComment.getUser(), findParentReply.getUser()); return new ReplyResponseDto( - createReply.getReplyId(), - createReply.getComment().getCommentId(), - createReply.getParentReply().getReplyId(), + createReply.getId(), + createReply.getComment().getId(), + createReply.getParentReply().getId(), createReply.getContent(), createReply.getCreatedAt(), createReply.getModifiedAt() @@ -72,8 +74,10 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest /** * 대댓글 업데이트 메서드 - * @param id - * @param requestDto + * + * @param loginUser 로그인한 유저 + * @param id 대댓글 식별자 + * @param requestDto 대댓글 업데이트 Dto */ @Transactional public void updateReply(User loginUser, Long id, ReplyRequestDto requestDto) { @@ -91,8 +95,10 @@ public void updateReply(User loginUser, Long id, ReplyRequestDto requestDto) { } /** - * 대댓글 삭제 메서드 - * @param id + * 대댓글 메서드 + * + * @param loginUser 로그인한 유저 + * @param id 대댓글 식별자 */ @Transactional public void deleteReply(User loginUser, Long id) { From edfdc672144ec85891924210afb1ca94046a6b6c Mon Sep 17 00:00:00 2001 From: cooleHoonst <88084780+89JHoon@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:55:30 +0900 Subject: [PATCH 148/215] =?UTF-8?q?refact:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EC=9E=90=EB=B0=94=EB=8F=85=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 게임 서비스 자바독 작성 2. 게임요청 서비스 자바독 작성 3. 게임추천 서비스 자바독 작성 4. 제미나이AI 서비스 자바독 작성 --- .../service/GameEnrollRequestService.java | 33 +++++++++-- .../game/service/GameRecommendService.java | 13 ++++- .../domain/game/service/GameService.java | 57 ++++++++++++++++--- .../domain/game/service/GeminiService.java | 6 +- 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java index adc9004..3e2052c 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameEnrollRequestService.java @@ -28,7 +28,12 @@ public class GameEnrollRequestService { private final GameRepository gameRepository; private final GameEnrollRequestRepository gameEnrollRequestRepository; - //게임등록요청 생성 + /** + * 새로운 게임 등록 요청을 생성합니다. + * @param requestDto 게임 등록 요청 정보를 담은 DTO + * @param userId 요청을 생성하는 사용자 + * @return 생성된 게임 등록 요청 정보 + */ @Transactional public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCreateRequestDto requestDto, User userId) { GamaEnrollRequest gameEnrollRequest = new GamaEnrollRequest( @@ -42,7 +47,11 @@ public GameEnrollRequestResponseDto createGameEnrollRequest(GameEnrollRequestCre return new GameEnrollRequestResponseDto(saveEnrollRequest); } - //게임등록요청 다건조회(only Role.ADMIN) + /** + * 모든 게임 등록 요청을 조회합니다. 관리자 권한이 필요합니다. + * @param loginUser 현재 로그인한 사용자 + * @return 게임 등록 요청 목록 (페이지네이션 적용) + */ public Page findAllGameEnrollRequest(User loginUser) { if (!loginUser.getRole().equals(Role.ADMIN)) { @@ -54,7 +63,12 @@ public Page findAllGameEnrollRequest(User loginUse return gameEnrollRequestRepository.findAll(pageable).map(GameEnrollRequestResponseDto::new); } - //게임등록요청 단건조회(only Role.ADMIN) + /** + * 특정 ID의 게임 등록 요청을 조회합니다. 관리자 권한이 필요합니다. + * @param id 조회할 게임 등록 요청의 ID + * @param loginUser 현재 로그인한 사용자 + * @return 조회된 게임 등록 요청 정보 + */ public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User loginUser) { if (!loginUser.getRole().equals(Role.ADMIN)) { @@ -67,7 +81,12 @@ public GameEnrollRequestResponseDto findGameEnrollRequestById(Long id, User logi return new GameEnrollRequestResponseDto(gamaEnrollRequest); } - //게임등록요청 수정 & 게임등록 (only Role.ADMIN) + /** + * 게임 등록 요청을 수정하고, 승인 시 게임을 등록합니다. 관리자 권한이 필요합니다. + * @param id 수정할 게임 등록 요청의 ID + * @param requestDto 수정할 게임 등록 요청 정보를 담은 DTO + * @param loginUser 현재 로그인한 사용자 + */ @Transactional public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestDto, User loginUser) { @@ -101,7 +120,11 @@ public void updateGameEnroll(Long id, GameEnrollRequestUpdateRequestDto requestD } } - //게임등록요청 삭제 (only Role.ADMIN) + /** + * 게임 등록 요청을 삭제합니다. 관리자 권한이 필요합니다. + * @param id 삭제할 게임 등록 요청의 ID + * @param loginUser 현재 로그인한 사용자 + */ @Transactional public void deleteGameEnroll(Long id, User loginUser) { diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java index 50b9af8..9dd2ac2 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameRecommendService.java @@ -34,7 +34,12 @@ public class GameRecommendService { private final GameRecommendHistoryRepository gameRecommendHistoryRepository; private final GeminiService geminiService; - // 게임 추천 요청 및 응답 + /** + * 사용자의 게임 선호도를 기반으로 게임을 추천하고, 그 결과를 저장합니다. + * @param requestDto 사용자의 게임 선호도 정보를 담은 DTO + * @param loginUser 현재 로그인한 사용자 + * @return 사용자의 게임 선호도와 추천된 게임 목록을 포함한 응답 DTO + */ @Transactional public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreferenceRequestDto requestDto, User loginUser) { @@ -108,7 +113,11 @@ public UserGamePreferenceResponseDto createUserGamePreference(UserGamePreference return new UserGamePreferenceResponseDto(saveData, gameRecommendations); } - //게임 추천 조회 + /** + * 로그인한 사용자의 게임 추천 기록을 페이지네이션하여 조회합니다. + * @param loginUser 현재 로그인한 사용자 + * @return 사용자의 게임 추천 기록 목록 (페이지네이션 적용) + */ public Page getGameRecommendHistories( User loginUser) { Pageable pageable = PageRequest.of(0, 15); diff --git a/src/main/java/com/example/gamemate/domain/game/service/GameService.java b/src/main/java/com/example/gamemate/domain/game/service/GameService.java index 54708c4..5379fcf 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GameService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GameService.java @@ -37,7 +37,13 @@ public class GameService { private final GameRepository gameRepository; private final S3Service s3Service; - //게임 등록(생성) (only Role.ADMIN) + /** + * 새로운 게임을 생성합니다. 관리자 권한이 필요합니다. + * @param loginUser 로그인한 사용자 정보 + * @param gameCreateRequestDto 게임 생성 요청 데이터 + * @param file 게임 이미지 파일 + * @return 생성된 게임 정보 + */ @Transactional public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gameCreateRequestDto, MultipartFile file) { @@ -82,14 +88,24 @@ public GameCreateResponseDto createGame(User loginUser, GameCreateRequestDto gam return new GameCreateResponseDto(savedGame); } - //게임 다건 조회 + /** + * 모든 게임을 페이지네이션하여 조회합니다. + * @param page 페이지 번호 + * @param size 페이지 크기 + * @return 게임 목록 + */ public Page findAllGame(int page, int size) { Pageable pageable = PageRequest.of(page, size); return gameRepository.findAll(pageable).map(GameFindAllResponseDto::new); } - //게임 단건 조회 + /** + * 특정 ID의 게임을 조회합니다. + * @param id 게임 ID + * @return 조회된 게임 정보 + * @throws ApiException 게임을 찾을 수 없는 경우 + */ public GameFindByIdResponseDto findGameById(Long id) { Game game = gameRepository.findGameById(id) @@ -98,7 +114,13 @@ public GameFindByIdResponseDto findGameById(Long id) { return new GameFindByIdResponseDto(game); } - //게임 정보 수정 (only Role.ADMIN) + /** + * 게임 정보를 수정합니다. 관리자 권한이 필요합니다. + * @param id 수정할 게임의 ID + * @param requestDto 수정할 게임 정보 + * @param newFile 새로운 게임 이미지 파일 + * @param loginUser 로그인한 사용자 정보 + */ @Transactional public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile newFile, User loginUser) { @@ -122,7 +144,10 @@ public void updateGame(Long id, GameUpdateRequestDto requestDto, MultipartFile n gameRepository.save(game); } - //게임 이미지 삭제 + /** + * 기존 게임 이미지를 삭제합니다. + * @param game 이미지를 삭제할 게임 + */ private void deleteExistingImages(Game game) { for (GameImage image : game.getImages()) { try { @@ -134,7 +159,11 @@ private void deleteExistingImages(Game game) { game.getImages().clear(); } - //게임 이미지 등록 + /** + * 새로운 게임 이미지를 업로드합니다. + * @param game 이미지를 업로드할 게임 + * @param newFile 새로운 이미지 파일 + */ private void uploadNewImage(Game game, MultipartFile newFile) { if (newFile != null && !newFile.isEmpty()) { try { @@ -161,7 +190,11 @@ private void uploadNewImage(Game game, MultipartFile newFile) { } } - //게임 삭제 (only Role.ADMIN) + /** + * 게임을 삭제합니다. 관리자 권한이 필요합니다. + * @param id 삭제할 게임의 ID + * @param loginUser 로그인한 사용자 정보 + */ @Transactional public void deleteGame(Long id, User loginUser) { @@ -181,7 +214,15 @@ public void deleteGame(Long id, User loginUser) { gameRepository.delete(game); } - //게임 검색 + /** + * 키워드, 장르, 플랫폼으로 게임을 검색합니다. + * @param keyword 검색 키워드 + * @param genre 게임 장르 + * @param platform 게임 플랫폼 + * @param page 페이지 번호 + * @param size 페이지 크기 + * @return 검색된 게임 목록 + */ public Page searchGame(String keyword, String genre, String platform, int page, int size) { log.info("Searching games with parameters - keyword: {}, genre: {}, platform: {}", diff --git a/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java index d00dc95..509c99a 100644 --- a/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java +++ b/src/main/java/com/example/gamemate/domain/game/service/GeminiService.java @@ -23,7 +23,11 @@ public class GeminiService { @Value("${gemini.api.key}") private String geminiApiKey; - // Gemini에 요청 전송 + /** + * Gemini API에 프롬프트를 전송하고 응답을 받아옵니다. + * @param prompt Gemini API에 전송할 프롬프트 문자열 + * @return Gemini API로부터 받은 응답 메시지 + */ public String getContents(String prompt) { From 7c2e8d2150ddbe2bcc191a135161169b3bf42e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 30 Jan 2025 15:08:43 +0900 Subject: [PATCH 149/215] =?UTF-8?q?fix:=20GlobalExceptionHandler=EC=97=90?= =?UTF-8?q?=EC=84=9C=20JWT=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/global/constant/ErrorCode.java | 1 - .../exception/GlobalExceptionHandler.java | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 99f20fa..a9b1a48 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -32,7 +32,6 @@ public enum ErrorCode { /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), - NO_TOKEN(HttpStatus.UNAUTHORIZED, "NO_TOKEN","로그인이 필요합니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "INVALID_TOKEN", "유효하지 않은 토큰입니다."), /* 403 권한 없음 */ diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 1961e41..508b2a8 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -39,14 +39,19 @@ public ResponseEntity handleResponseStatusException(ResponseStatusExcept @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { log.warn("handleIllegalArgument", e); - ErrorCode errorCode = ErrorCode.INVALID_PARAMETER; + ErrorCode errorCode; + if("유효하지 않은 토큰입니다.".equals(e.getMessage())) { + errorCode = ErrorCode.INVALID_TOKEN; + } else { + errorCode = ErrorCode.INVALID_PARAMETER; + } return handleExceptionInternal(errorCode, errorCode.getMessage()); } @ExceptionHandler(RuntimeException.class) public ResponseEntity handleIRuntime(RuntimeException e) { log.warn("handleIRuntime", e); - ErrorCode errorCode = ErrorCode.UNAUTHORIZED; + ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; return handleExceptionInternal(errorCode, errorCode.getMessage()); } @@ -74,7 +79,6 @@ public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotVali ExpiredJwtException.class, SignatureException.class, MalformedJwtException.class, - AuthenticationException.class }) public ResponseEntity handleJwtException(Exception e) { log.warn("handleJwtException", e); @@ -82,6 +86,13 @@ public ResponseEntity handleJwtException(Exception e) { return handleExceptionInternal(errorCode, errorCode.getMessage()); } + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException e) { + log.warn("Authentication exception", e); + ErrorCode errorCode = ErrorCode.UNAUTHORIZED; + return handleExceptionInternal(errorCode, errorCode.getMessage()); + } + @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException(AccessDeniedException e) { log.warn("handleAccessDeniedException", e); From 66005d3a360c967795d41373f4297624abd64e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 30 Jan 2025 21:13:49 +0900 Subject: [PATCH 150/215] =?UTF-8?q?feat:=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=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 --- .../coupon/controller/CouponController.java | 58 +++++++++ .../coupon/dto/CouponCreateRequestDto.java | 35 ++++++ .../coupon/dto/CouponCreateResponseDto.java | 25 ++++ .../coupon/dto/CouponIssueResponseDto.java | 33 +++++ .../gamemate/domain/coupon/entity/Coupon.java | 67 ++++++++++ .../domain/coupon/entity/UserCoupon.java | 50 ++++++++ .../coupon/repository/CouponRepository.java | 8 ++ .../repository/UserCouponRepository.java | 13 ++ .../domain/coupon/service/CouponService.java | 116 ++++++++++++++++++ .../gamemate/domain/user/entity/User.java | 6 +- .../gamemate/global/constant/ErrorCode.java | 8 ++ 11 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/gamemate/domain/coupon/controller/CouponController.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateRequestDto.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/dto/CouponIssueResponseDto.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java create mode 100644 src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java diff --git a/src/main/java/com/example/gamemate/domain/coupon/controller/CouponController.java b/src/main/java/com/example/gamemate/domain/coupon/controller/CouponController.java new file mode 100644 index 0000000..6fcd1f6 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/controller/CouponController.java @@ -0,0 +1,58 @@ +package com.example.gamemate.domain.coupon.controller; + +import com.example.gamemate.domain.coupon.dto.CouponCreateRequestDto; +import com.example.gamemate.domain.coupon.dto.CouponCreateResponseDto; +import com.example.gamemate.domain.coupon.dto.CouponIssueResponseDto; +import com.example.gamemate.domain.coupon.service.CouponService; +import com.example.gamemate.global.config.auth.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/coupons") +@RequiredArgsConstructor +public class CouponController { + private final CouponService couponService; + + @PostMapping("/create") + public ResponseEntity createCoupon( + @Valid @RequestBody CouponCreateRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + CouponCreateResponseDto responseDto = couponService.createCoupon(requestDto, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + @PostMapping("/{couponId}/issue") + public ResponseEntity issueCoupon( + @PathVariable Long couponId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + CouponIssueResponseDto responseDto = couponService.issueCoupon(couponId, customUserDetails.getUser()); + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @GetMapping("/my") + public ResponseEntity> findMyCoupons( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + List responseDtos = couponService.findMyCoupons(customUserDetails.getUser()); + return new ResponseEntity<>(responseDtos, HttpStatus.OK); + } + + @PostMapping("/user-coupons/{userCouponId}/use") + public ResponseEntity useCoupon( + @PathVariable Long userCouponId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + couponService.useCoupon(userCouponId, customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} + diff --git a/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateRequestDto.java b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateRequestDto.java new file mode 100644 index 0000000..8df4e42 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateRequestDto.java @@ -0,0 +1,35 @@ +package com.example.gamemate.domain.coupon.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class CouponCreateRequestDto { + + @NotBlank(message = "쿠폰 코드를 입력해주세요.") + private final String code; + + @NotBlank(message = "쿠폰 이름을 입력해주세요.") + private final String name; + + @Positive(message = "할인 금액을 양수로 입력해주세요.") + @NotNull(message = "할인 금액을 입력해주세요.") + private final Integer discountAmount; + + @NotNull(message = "시작 시간을 입력해주세요.") + private final LocalDateTime startAt; + + @NotNull(message = "종료 시간을 입력해주세요.") + private final LocalDateTime expiredAt; + + @NotNull(message = "쿠폰 수량을 입력해주세요.") + private final Integer quantity; + +} + diff --git a/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java new file mode 100644 index 0000000..60c82b9 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java @@ -0,0 +1,25 @@ +package com.example.gamemate.domain.coupon.dto; + +import com.example.gamemate.domain.coupon.entity.Coupon; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CouponCreateResponseDto { + private final Long id; + private final String code; + private final String name; + private final Integer discountAmount; + private final LocalDateTime startAt; + private final LocalDateTime expiredAt; + + public CouponCreateResponseDto(Coupon coupon) { + this.id = coupon.getId(); + this.code = coupon.getCode(); + this.name = coupon.getName(); + this.discountAmount = coupon.getDiscountAmount(); + this.startAt = coupon.getStartAt(); + this.expiredAt = coupon.getExpiredAt(); + } +} diff --git a/src/main/java/com/example/gamemate/domain/coupon/dto/CouponIssueResponseDto.java b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponIssueResponseDto.java new file mode 100644 index 0000000..2ea4161 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponIssueResponseDto.java @@ -0,0 +1,33 @@ +package com.example.gamemate.domain.coupon.dto; + +import com.example.gamemate.domain.coupon.entity.Coupon; +import com.example.gamemate.domain.coupon.entity.UserCoupon; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CouponIssueResponseDto { + + private final Long id; + private final String code; + private final String name; + private final Integer discountAmount; + private final LocalDateTime issuedAt; + private final LocalDateTime startAt; + private final LocalDateTime expiredAt; + private final Boolean isUsed; + + public CouponIssueResponseDto(UserCoupon userCoupon) { + Coupon coupon = userCoupon.getCoupon(); + this.id = userCoupon.getId(); + this.code = coupon.getCode(); + this.name = coupon.getName(); + this.discountAmount = coupon.getDiscountAmount(); + this.startAt = coupon.getStartAt(); + this.expiredAt = coupon.getExpiredAt(); + this.issuedAt = userCoupon.getIssuedAt(); + this.isUsed = userCoupon.getIsUsed(); + } +} + diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java new file mode 100644 index 0000000..4bd8f3a --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java @@ -0,0 +1,67 @@ +package com.example.gamemate.domain.coupon.entity; + +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "coupon") +public class Coupon extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private Integer discountAmount; + + @Column(nullable = false) + private Integer quantity; + + @Column(nullable = false) + private Integer issuedQuantity = 0; + + @Column(nullable = false) + private LocalDateTime startAt; + + @Column(nullable = false) + private LocalDateTime expiredAt; + + @OneToMany(mappedBy = "coupon") + private List userCoupons = new ArrayList<>(); + + public Coupon(String code, String name, Integer discountAmount, Integer quantity, LocalDateTime startAt, LocalDateTime expiredAt) { + this.code = code; + this.name = name; + this.discountAmount = discountAmount; + this.quantity = quantity; + this.startAt = startAt; + this.expiredAt = expiredAt; + } + + public boolean isIssuable() { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(startAt) && now.isBefore(expiredAt); + } + + public boolean isExhausted() { + return issuedQuantity >= quantity; + } + + public void incrementIssuedQuantity() { + this.issuedQuantity++; + } +} + diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java new file mode 100644 index 0000000..61f4a96 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java @@ -0,0 +1,50 @@ +package com.example.gamemate.domain.coupon.entity; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "user_coupon") +public class UserCoupon extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Boolean isUsed; + + @Column(nullable = false) + private LocalDateTime issuedAt; + + private LocalDateTime usedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coupon_id", nullable = false) + private Coupon coupon; + + public UserCoupon(User user, Coupon coupon) { + this.user = user; + this.coupon = coupon; + this.issuedAt = LocalDateTime.now(); + } + + public void updateIsUsed(Boolean isUsed) { + this.isUsed = isUsed; + } + + public void updateUsedAt() { + this.usedAt = LocalDateTime.now(); + } +} + diff --git a/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java new file mode 100644 index 0000000..1717df1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java @@ -0,0 +1,8 @@ +package com.example.gamemate.domain.coupon.repository; + +import com.example.gamemate.domain.coupon.entity.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CouponRepository extends JpaRepository { + boolean existsByCode(String code); +} diff --git a/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java b/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java new file mode 100644 index 0000000..98d6dba --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java @@ -0,0 +1,13 @@ +package com.example.gamemate.domain.coupon.repository; + +import com.example.gamemate.domain.coupon.entity.UserCoupon; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserCouponRepository extends JpaRepository { + boolean existsByUserIdAndCouponId(Long userId, Long couponId); + List findByUserId(Long userId); +// Optional findByUserIdAndCouponId(Long userId, Long couponId); +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java new file mode 100644 index 0000000..467f0ea --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -0,0 +1,116 @@ +package com.example.gamemate.domain.coupon.service; + +import com.example.gamemate.domain.coupon.dto.CouponCreateRequestDto; +import com.example.gamemate.domain.coupon.dto.CouponCreateResponseDto; +import com.example.gamemate.domain.coupon.dto.CouponIssueResponseDto; +import com.example.gamemate.domain.coupon.entity.Coupon; +import com.example.gamemate.domain.coupon.entity.UserCoupon; +import com.example.gamemate.domain.coupon.repository.CouponRepository; +import com.example.gamemate.domain.coupon.repository.UserCouponRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class CouponService { + private final CouponRepository couponRepository; + private final UserCouponRepository userCouponRepository; + + public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, User loginUser) { + // 관리자 권한 체크 + if (!loginUser.getRole().equals(Role.ADMIN)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + // 쿠폰 코드 중복 확인 + if (couponRepository.existsByCode(requestDto.getCode())) { + throw new ApiException(ErrorCode.COUPON_CODE_DUPLICATED); + } + + // 쿠폰 사용 기간 유효성 검증 + validateCouponDates(requestDto.getStartAt(), requestDto.getExpiredAt()); + + // 쿠폰 생성 + Coupon coupon = new Coupon(requestDto.getCode(), requestDto.getName(), requestDto.getDiscountAmount(), requestDto.getQuantity(), requestDto.getStartAt(), requestDto.getExpiredAt()); + Coupon savedCoupon = couponRepository.save(coupon); + + return new CouponCreateResponseDto(savedCoupon); + } + + public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); + + // 발급 가능 체크 + if (!coupon.isIssuable()) { + throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); + } + + // 수량 체크 + if (coupon.isExhausted()) { + throw new ApiException(ErrorCode.COUPON_EXHAUSTED); + } + + // 중복 발급 체크 + if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { + throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); + } + + // 쿠폰 발급 + coupon.incrementIssuedQuantity(); + UserCoupon userCoupon = new UserCoupon(loginUser, coupon); + userCoupon.updateIsUsed(false); + + UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); + + return new CouponIssueResponseDto(savedUserCoupon); + } + + @Transactional(readOnly = true) + public List findMyCoupons(User loginUser) { + List userCoupons = userCouponRepository.findByUserId(loginUser.getId()); + return userCoupons.stream() + .map(CouponIssueResponseDto::new) + .collect(Collectors.toList()); + } + + public void useCoupon(Long userCouponId, User loginUser) { + UserCoupon userCoupon = userCouponRepository.findById(userCouponId) + .orElseThrow(()-> new ApiException(ErrorCode.COUPON_NOT_FOUND)); + + // 본인 쿠폰인지 확인 + if (!userCoupon.getUser().getId().equals(loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + + // 이미 사용된 쿠폰인지 확인 + if (userCoupon.getIsUsed()) { + throw new ApiException(ErrorCode.COUPON_ALREADY_USED); + } + + // 쿠폰 유효기간 확인 + if (LocalDateTime.now().isAfter(userCoupon.getCoupon().getExpiredAt())) { + throw new ApiException(ErrorCode.COUPON_EXPIRED); + } + + // 쿠폰 사용 처리 + userCoupon.updateIsUsed(true); + userCoupon.updateUsedAt(); + } + + private void validateCouponDates(LocalDateTime startAt, LocalDateTime expiredAt) { + if (startAt.isAfter(expiredAt)) { + throw new ApiException(ErrorCode.INVALID_COUPON_DATE); + } + } +} diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index b37c7d0..eeb8248 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.user.entity; +import com.example.gamemate.domain.coupon.entity.UserCoupon; import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.domain.user.enums.AuthProvider; import com.example.gamemate.global.common.BaseEntity; @@ -9,6 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; import java.util.List; @Entity @@ -19,7 +21,6 @@ public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") private Long id; @Column(nullable = false, unique = true) @@ -55,6 +56,9 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "followee") private List followerList; + @OneToMany(mappedBy = "user") + private List userCoupons = new ArrayList<>(); + // 이메일 로그인용 생성자 public User(String email, String name, String nickname, String password) { this.email = email; diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index a9b1a48..28fd8f8 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -29,6 +29,13 @@ public enum ErrorCode { FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST,"FILE_SIZE_EXCEEDED","파일 크기가 초과되었습니다."), SOCIAL_PASSWORD_ALREADY_SET(HttpStatus.BAD_REQUEST, "SOCIAL_PASSWORD_ALREADY_SET", "이미 비밀번호가 설정되었습니다."), SOCIAL_PASSWORD_REQUIRED(HttpStatus.BAD_REQUEST, "SOCIAL_PASSWORD_REQUIRED", "소셜 로그인 계정은 비밀번호 설정이 필요합니다."), + COUPON_CODE_DUPLICATED(HttpStatus.BAD_REQUEST, "COUPON_CODE_DUPLICATED", "이미 존재하는 쿠폰 코드입니다."), + INVALID_COUPON_DATE(HttpStatus.BAD_REQUEST, "INVALID_COUPON_DATE", "쿠폰의 사용 기간이 올바르지 않습니다."), + COUPON_EXHAUSTED(HttpStatus.BAD_REQUEST, "COUPON_EXHAUSTED", "쿠폰이 모두 소진되었습니다."), + COUPON_ALREADY_ISSUED(HttpStatus.BAD_REQUEST, "COUPON_ALREADY_ISSUED", "이미 발급된 쿠폰입니다."), + COUPON_NOT_ISSUABLE(HttpStatus.BAD_REQUEST, "COUPON_NOT_ISSUABLE", "쿠폰 발급 기간이 아닙니다."), + COUPON_ALREADY_USED(HttpStatus.BAD_REQUEST, "COUPON_ALREADY_USED", "이미 사용된 쿠폰입니다."), + COUPON_EXPIRED(HttpStatus.BAD_REQUEST, "COUPON_EXPIRED", "쿠폰의 유효 기간이 만료되었습니다."), /* 401 인증 오류 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "로그인이 필요합니다."), @@ -50,6 +57,7 @@ public enum ErrorCode { RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND,"RECOMMENDATION_NOT_FOUND","추천 게임을 찾을 수 없습니다."), MATCH_USER_INFO_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_FOUND", "매칭을 위해 입력된 회원 정보를 찾을 수 없습니다."), MATCH_USER_INFO_NOT_WRITTEN(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_WRITTEN", "매칭을 위해 회원 정보 입력은 필수입니다."), + COUPON_NOT_FOUND(HttpStatus.NOT_FOUND, "COUPON_NOT_FOUND", "쿠폰을 찾을 수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."), From 6a65adf95f2f58cb44a9ae9f2880ac5e3fb9f994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Fri, 31 Jan 2025 00:03:43 +0900 Subject: [PATCH 151/215] =?UTF-8?q?fix:=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=B9=84?= =?UTF-8?q?=EA=B4=80=EC=A0=81=20=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/coupon/entity/Coupon.java | 8 +-- .../coupon/repository/CouponRepository.java | 12 ++++ .../domain/coupon/service/CouponService.java | 58 +++++++++++-------- .../gamemate/global/constant/ErrorCode.java | 3 +- src/main/resources/application.properties | 2 + 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java index 4bd8f3a..25e7645 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java @@ -28,7 +28,7 @@ public class Coupon extends BaseEntity { private Integer discountAmount; @Column(nullable = false) - private Integer quantity; + private Integer totalQuantity; @Column(nullable = false) private Integer issuedQuantity = 0; @@ -42,11 +42,11 @@ public class Coupon extends BaseEntity { @OneToMany(mappedBy = "coupon") private List userCoupons = new ArrayList<>(); - public Coupon(String code, String name, Integer discountAmount, Integer quantity, LocalDateTime startAt, LocalDateTime expiredAt) { + public Coupon(String code, String name, Integer discountAmount, Integer totalQuantity, LocalDateTime startAt, LocalDateTime expiredAt) { this.code = code; this.name = name; this.discountAmount = discountAmount; - this.quantity = quantity; + this.totalQuantity = totalQuantity; this.startAt = startAt; this.expiredAt = expiredAt; } @@ -57,7 +57,7 @@ public boolean isIssuable() { } public boolean isExhausted() { - return issuedQuantity >= quantity; + return issuedQuantity >= totalQuantity; } public void incrementIssuedQuantity() { diff --git a/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java index 1717df1..0fdc3e3 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/example/gamemate/domain/coupon/repository/CouponRepository.java @@ -1,8 +1,20 @@ package com.example.gamemate.domain.coupon.repository; import com.example.gamemate.domain.coupon.entity.Coupon; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface CouponRepository extends JpaRepository { boolean existsByCode(String code); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select c from Coupon c where c.id = :id") + Optional findByIdWithPessimisticLock(@Param("id") Long id); } diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 467f0ea..261edbc 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -12,6 +12,8 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +24,7 @@ @Service @Transactional @RequiredArgsConstructor +@Slf4j public class CouponService { private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; @@ -48,32 +51,37 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U } public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { - Coupon coupon = couponRepository.findById(couponId) - .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); - - // 발급 가능 체크 - if (!coupon.isIssuable()) { - throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); - } - - // 수량 체크 - if (coupon.isExhausted()) { - throw new ApiException(ErrorCode.COUPON_EXHAUSTED); - } - - // 중복 발급 체크 - if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { - throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); + try { + Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId) + .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); + + // 발급 가능 체크 + if (!coupon.isIssuable()) { + throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); + } + + // 수량 체크 + if (coupon.isExhausted()) { + throw new ApiException(ErrorCode.COUPON_EXHAUSTED); + } + + // 중복 발급 체크 + if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { + throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); + } + + // 쿠폰 발급 + coupon.incrementIssuedQuantity(); + UserCoupon userCoupon = new UserCoupon(loginUser, coupon); + userCoupon.updateIsUsed(false); + + UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); + + return new CouponIssueResponseDto(savedUserCoupon); + } catch (PessimisticLockingFailureException e) { + log.error("쿠폰 발급 동시 요청으로 잠금 획득 실패: {}", couponId, e); + throw new ApiException(ErrorCode.COUPON_ISSUE_FAILED); } - - // 쿠폰 발급 - coupon.incrementIssuedQuantity(); - UserCoupon userCoupon = new UserCoupon(loginUser, coupon); - userCoupon.updateIsUsed(false); - - UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); - - return new CouponIssueResponseDto(savedUserCoupon); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index 28fd8f8..1853f5d 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -61,7 +61,8 @@ public enum ErrorCode { /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."), - EMAIL_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "EMAIL_SEND_ERROR", "이메일 전송에 문제가 발생했습니다."); + EMAIL_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "EMAIL_SEND_ERROR", "이메일 전송에 문제가 발생했습니다."), + COUPON_ISSUE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COUPON_ISSUE_FAILED", "쿠폰 발급에 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); private final HttpStatus status; private final String code; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26ee1c8..0de4f11 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -87,3 +87,5 @@ spring.servlet.multipart.max-request-size=5MB spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always +# Lock timeout +spring.jpa.properties.jakarta.persistence.lock.timeout=3000 \ No newline at end of file From 7182ea7d2c2043d1d861c3a8e3a5bc3ff325c3a3 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 31 Jan 2025 02:55:17 +0900 Subject: [PATCH 152/215] =?UTF-8?q?refactor=20:=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9B=90=EC=B9=99(SRP)=20=EC=A4=80=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EC=95=8C=EB=A6=BC=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EA=B3=BC=20=EC=A0=84=EC=86=A1=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentService.java | 8 ++- .../domain/follow/service/FollowService.java | 4 +- .../domain/like/service/LikeService.java | 8 +-- .../domain/match/service/MatchService.java | 10 ++-- .../service/AsyncNotificationService.java | 50 ------------------- .../service/NotificationService.java | 44 ++++------------ .../domain/reply/service/ReplyService.java | 20 +++++--- 7 files changed, 40 insertions(+), 104 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index 46ebd18..7a26c1a 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -8,6 +8,7 @@ import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; @@ -53,11 +54,8 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq Comment comment = new Comment(requestDto.getContent(), findBoard, loginUser); Comment createComment = commentRepository.save(comment); - notificationService.sendNotification( - findBoard.getUser(), - NotificationType.NEW_COMMENT, - "/comments/" + createComment.getCommentId() - ); + Notification notification = notificationService.createNotification(findBoard.getUser(), NotificationType.NEW_COMMENT, "/comments/" + createComment.getCommentId()); + notificationService.sendNotification(findBoard.getUser(), notification); return new CommentResponseDto( createComment.getCommentId(), diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index a67f1ee..ed841fd 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -3,6 +3,7 @@ import com.example.gamemate.domain.follow.dto.*; import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.domain.follow.repository.FollowRepository; +import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; @@ -47,7 +48,8 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser Follow follow = new Follow(loginUser, followee); followRepository.save(follow); - notificationService.sendNotification(followee, NotificationType.NEW_FOLLOWER, "/users/" + loginUser.getId()); + Notification notification = notificationService.createNotification(followee, NotificationType.NEW_FOLLOWER, "/users/" + follow.getFollower().getId()); + notificationService.sendNotification(followee, notification); return new FollowResponseDto( follow.getId(), diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index 77ba0aa..b3af43a 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -2,12 +2,12 @@ import com.example.gamemate.domain.board.repository.BoardRepository; import com.example.gamemate.domain.like.dto.BoardLikeResponseDto; -import com.example.gamemate.domain.like.dto.ReviewLikeCountResponseDto; import com.example.gamemate.domain.like.dto.ReviewLikeResponseDto; import com.example.gamemate.domain.like.entity.BoardLike; import com.example.gamemate.domain.like.entity.ReviewLike; import com.example.gamemate.domain.like.repository.BoardLikeRepository; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; +import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.review.repository.ReviewRepository; @@ -43,7 +43,8 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo if (reviewLike.getId() == null) { reviewLikeRepository.save(reviewLike); - notificationService.sendNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/likes/reviews/" + reviewId); + Notification notification = notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/reviews/" + reviewLike.getReview().getId()); + notificationService.sendNotification(reviewLike.getReview().getUser(), notification); } else { reviewLike.changeStatus(status); } @@ -65,7 +66,8 @@ public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User login if (boardLike.getId() == null) { boardLikeRepository.save(boardLike); - notificationService.sendNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/likes/boards/" + boardId); + Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getBoardId()); + notificationService.sendNotification(boardLike.getBoard().getUser(), notification); } else { boardLike.changeStatus(status); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 5fe60ce..123adc9 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -8,6 +8,7 @@ import com.example.gamemate.domain.match.enums.Priority; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; +import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; @@ -61,7 +62,8 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { Match match = new Match(dto.getMessage(), loginUser, receiver); Match savedMatch = matchRepository.save(match); - notificationService.sendNotification(receiver, NotificationType.NEW_MATCH, "/matches/" + savedMatch.getId()); + Notification notification = notificationService.createNotification(receiver, NotificationType.NEW_MATCH, "/matches/" + savedMatch.getId()); + notificationService.sendNotification(receiver, notification); return MatchResponseDto.toDto(match); } @@ -82,11 +84,13 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { } // 로그인한 유저가 매칭의 받는 사람이 아닐때 예외처리 if (dto.getStatus() == MatchStatus.ACCEPTED) { - notificationService.sendNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + findMatch.getId()); + Notification notification = notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + findMatch.getId()); + notificationService.sendNotification(findMatch.getSender(), notification); } // 매칭 보낸 사람에게 매칭이 수락되었다는 알림 전송 if (dto.getStatus() == MatchStatus.REJECTED) { - notificationService.sendNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + findMatch.getId()); + Notification notification = notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + findMatch.getId()); + notificationService.sendNotification(findMatch.getSender(), notification); } // 매칭 보낸 사람에게 매칭이 거절되었다는 알림 전송 findMatch.updateStatus(dto.getStatus()); diff --git a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java deleted file mode 100644 index 69e4a3f..0000000 --- a/src/main/java/com/example/gamemate/domain/notification/service/AsyncNotificationService.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.gamemate.domain.notification.service; - -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.repository.NotificationRepository; -import com.example.gamemate.domain.user.entity.User; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -@Slf4j -@EnableAsync -public class AsyncNotificationService { - private final JavaMailSender javaMailSender; - private final NotificationRepository notificationRepository; - -// // 알림 메일 전송 -// @Async -// public void sendNotificationMail(User user, List notifications) { -// try { -// SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); -// simpleMailMessage.setTo(user.getEmail()); // 보낼 사람 -// simpleMailMessage.setSubject("[GameMate] 새로운 알림이 있습니다."); // 제목 -// simpleMailMessage.setFrom("newbiekk1126@gmail.com"); // 보내는 사람 -// simpleMailMessage.setText("새로운 알림이 " + notifications.size() + "개 있습니다."); // 내용 -// -// javaMailSender.send(simpleMailMessage); -// log.info("{}님에게 {}개의 알림 메일을 전송했습니다.", user.getEmail(), notifications.size()); -// -// updateNotificationStatus(notifications); -// } catch (Exception e) { -// log.error("알림 메일 전송 실패: {}", user.getEmail(), e); -// } -// } -// -// // 알림 전송 후 notified(false -> true) 상태 변경 -// @Transactional -// public void updateNotificationStatus(List notifications) { -// notifications.forEach(notification -> notification.updateIsRead(true)); -// notificationRepository.saveAll(notifications); -// } -} diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 00c5604..0091bc1 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -9,15 +9,11 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -25,14 +21,14 @@ public class NotificationService { private final NotificationRepository notificationRepository; - private final AsyncNotificationService asyncNotificationService; private final EmitterRepository emitterRepository; // 알림 생성 @Transactional - public void createNotification(User user, NotificationType type, String relatedUrl) { + public Notification createNotification(User user, NotificationType type, String relatedUrl) { Notification notification = new Notification(type.getContent(), relatedUrl, type, user); - notificationRepository.save(notification); + Notification savedNotification = notificationRepository.save(notification); + return savedNotification; } // 알림 전체 보기 @@ -46,31 +42,6 @@ public List findAllNotification(User loginUser) { .toList(); } -// // 알림 발송 (이메일) -// @Scheduled(cron = "0 0/10 * * * *") -// public void scheduleNotificationEmail() { -// log.info("스케쥴링 활성화"); -// -// List unnotifiedNotificationList = notificationRepository.findAllByIsRead(false); -// -// if (unnotifiedNotificationList.isEmpty()) { -// log.info("전송할 알림이 없습니다."); -// return; -// } -// -// Map> notificationMap = -// unnotifiedNotificationList -// .stream() -// .collect(Collectors.groupingBy(Notification::getReceiver)); -// -// for (Map.Entry> entry : notificationMap.entrySet()) { -// User user = entry.getKey(); -// List notifications = entry.getValue(); -// -// asyncNotificationService.sendNotificationMail(user, notifications); -// } -// } - public SseEmitter subscribe(User loginUser, String lastEventId) { Long DEFAULT_TIMEOUT = 60L * 1000 * 60; SseEmitter sseEmitter = emitterRepository.save(loginUser.getId(), new SseEmitter(DEFAULT_TIMEOUT)); @@ -94,10 +65,9 @@ public SseEmitter subscribe(User loginUser, String lastEventId) { } @Transactional - public void sendNotification(User user, NotificationType type, String relatedUrl) { + public void sendNotification(User user, Notification notification) { + long startTime = System.currentTimeMillis(); SseEmitter sseEmitter = emitterRepository.findById(user.getId()); - Notification notification = new Notification(type.getContent(), relatedUrl, type, user); - notificationRepository.save(notification); try { sseEmitter.send( @@ -109,6 +79,10 @@ public void sendNotification(User user, NotificationType type, String relatedUrl } catch (IOException e) { emitterRepository.deleteById(user.getId()); throw new RuntimeException("SSE 연결 오류 발생"); + } finally { + long endTime = System.currentTimeMillis(); + long elapsedTime = endTime - startTime; + log.info("sendNotification 메서드 실행 시간: {}ms", elapsedTime); } } } diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index d6bc67d..9d0fbfd 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -3,6 +3,7 @@ import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.notification.entity.Notification; import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyRequestDto; @@ -58,7 +59,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest .orElseThrow(()-> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); newReply = new Reply(requestDto.getContent(), findComment, loginUser, findParentReply); Reply createReply = replyRepository.save(newReply); - sendReplyNotification(findComment.getBoard(), findComment, findParentReply); + sendReplyNotification(findComment.getBoard(), findComment, findParentReply, createReply); return new ReplyResponseDto( createReply.getReplyId(), @@ -112,15 +113,20 @@ public void deleteReply(User loginUser, Long id) { // 대댓글 알림 전송 @Transactional public void sendCommentNotification(Board board, Comment comment, Reply reply) { - notificationService.sendNotification(board.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); - notificationService.sendNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); + Notification boardNotification = notificationService.createNotification(board.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(board.getUser(), boardNotification); + Notification commentNotification = notificationService.createNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(comment.getUser(), commentNotification); } // 대대댓글 알림 전송 @Transactional - public void sendReplyNotification(Board board, Comment comment, Reply reply) { - notificationService.sendNotification(board.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); - notificationService.sendNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); - notificationService.sendNotification(reply.getUser(), NotificationType.NEW_COMMENT, "/boards/" + board.getBoardId() + "/comments/" + comment.getCommentId() + "/replies/" + reply.getReplyId()); + public void sendReplyNotification(Board board, Comment comment, Reply parentReply, Reply reply) { + Notification boardNotification = notificationService.createNotification(board.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(board.getUser(), boardNotification); + Notification commentNotification = notificationService.createNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(comment.getUser(), commentNotification); + Notification parentReplyNotification = notificationService.createNotification(parentReply.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(parentReply.getUser(), parentReplyNotification); } } From 8ba0a76a03b63be24197687eb376e98b051c8aaa Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 31 Jan 2025 12:48:48 +0900 Subject: [PATCH 153/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=EC=9D=84=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=EB=84=88=EB=A5=BC=20=ED=86=B5=ED=95=9C=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentService.java | 10 +- .../domain/follow/service/FollowService.java | 8 +- .../domain/like/service/LikeService.java | 13 +- .../domain/match/service/MatchService.java | 18 ++- .../controller/NotificationController.java | 3 +- .../service/NotificationService.java | 10 +- .../domain/reply/service/ReplyService.java | 33 +---- .../eventListener/GlobalEventListener.java | 128 ++++++++++++++++++ .../event/BoardLikeCreatedEvent.java | 15 ++ .../event/CommentCreatedEvent.java | 15 ++ .../event/FollowCreatedEvent.java | 15 ++ .../event/MatchAcceptedEvent.java | 15 ++ .../event/MatchCreatedEvent.java | 15 ++ .../event/MatchRejectedEvent.java | 15 ++ .../event/ReplyCreatedEvent.java | 15 ++ .../event/ReviewLikeCreatedEvent.java | 15 ++ 16 files changed, 284 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/BoardLikeCreatedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/CommentCreatedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/FollowCreatedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/MatchAcceptedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/MatchCreatedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/MatchRejectedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/ReplyCreatedEvent.java create mode 100644 src/main/java/com/example/gamemate/global/eventListener/event/ReviewLikeCreatedEvent.java diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index 7a26c1a..8c5b97b 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -8,16 +8,15 @@ import com.example.gamemate.domain.comment.dto.CommentResponseDto; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.enums.NotificationType; -import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; import com.example.gamemate.domain.reply.entity.Reply; import com.example.gamemate.domain.reply.repository.ReplyRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.eventListener.event.CommentCreatedEvent; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -37,7 +36,7 @@ public class CommentService { private final CommentRepository commentRepository; private final ReplyRepository replyRepository; private final BoardRepository boardRepository; - private final NotificationService notificationService; + private final ApplicationEventPublisher publisher; /** * 댓글 생성 메서드 @@ -54,8 +53,7 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq Comment comment = new Comment(requestDto.getContent(), findBoard, loginUser); Comment createComment = commentRepository.save(comment); - Notification notification = notificationService.createNotification(findBoard.getUser(), NotificationType.NEW_COMMENT, "/comments/" + createComment.getCommentId()); - notificationService.sendNotification(findBoard.getUser(), notification); + publisher.publishEvent(new CommentCreatedEvent(this, createComment)); return new CommentResponseDto( createComment.getCommentId(), diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index ed841fd..0752078 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -3,17 +3,17 @@ import com.example.gamemate.domain.follow.dto.*; import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.domain.follow.repository.FollowRepository; -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.eventListener.event.FollowCreatedEvent; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import java.util.List; @@ -26,6 +26,7 @@ public class FollowService { private final UserRepository userRepository; private final FollowRepository followRepository; private final NotificationService notificationService; + private final ApplicationEventPublisher publisher; // 팔로우하기 @Transactional @@ -48,8 +49,7 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser Follow follow = new Follow(loginUser, followee); followRepository.save(follow); - Notification notification = notificationService.createNotification(followee, NotificationType.NEW_FOLLOWER, "/users/" + follow.getFollower().getId()); - notificationService.sendNotification(followee, notification); + publisher.publishEvent(new FollowCreatedEvent(this, follow)); return new FollowResponseDto( follow.getId(), diff --git a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java index b3af43a..9830595 100644 --- a/src/main/java/com/example/gamemate/domain/like/service/LikeService.java +++ b/src/main/java/com/example/gamemate/domain/like/service/LikeService.java @@ -7,15 +7,16 @@ import com.example.gamemate.domain.like.entity.ReviewLike; import com.example.gamemate.domain.like.repository.BoardLikeRepository; import com.example.gamemate.domain.like.repository.ReviewLikeRepository; -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.enums.NotificationType; import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.review.repository.ReviewRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.eventListener.event.BoardLikeCreatedEvent; +import com.example.gamemate.global.eventListener.event.ReviewLikeCreatedEvent; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,7 +28,7 @@ public class LikeService { private final ReviewRepository reviewRepository; private final BoardLikeRepository boardLikeRepository; private final BoardRepository boardRepository; - private final NotificationService notificationService; + private final ApplicationEventPublisher publisher; @Transactional public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User loginUser) { @@ -43,8 +44,7 @@ public ReviewLikeResponseDto reviewLikeUp(Long reviewId, Integer status, User lo if (reviewLike.getId() == null) { reviewLikeRepository.save(reviewLike); - Notification notification = notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/reviews/" + reviewLike.getReview().getId()); - notificationService.sendNotification(reviewLike.getReview().getUser(), notification); + publisher.publishEvent(new ReviewLikeCreatedEvent(this, reviewLike)); } else { reviewLike.changeStatus(status); } @@ -66,8 +66,7 @@ public BoardLikeResponseDto boardLikeUp(Long boardId, Integer status, User login if (boardLike.getId() == null) { boardLikeRepository.save(boardLike); - Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getBoardId()); - notificationService.sendNotification(boardLike.getBoard().getUser(), notification); + publisher.publishEvent(new BoardLikeCreatedEvent(this, boardLike)); } else { boardLike.changeStatus(status); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 123adc9..da3e730 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -8,16 +8,17 @@ import com.example.gamemate.domain.match.enums.Priority; import com.example.gamemate.domain.match.repository.MatchRepository; import com.example.gamemate.domain.match.repository.MatchUserInfoRepository; -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.enums.NotificationType; -import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.UserStatus; import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.eventListener.event.MatchAcceptedEvent; +import com.example.gamemate.global.eventListener.event.MatchCreatedEvent; +import com.example.gamemate.global.eventListener.event.MatchRejectedEvent; import com.example.gamemate.global.exception.ApiException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -35,7 +36,7 @@ public class MatchService { private final UserRepository userRepository; private final MatchRepository matchRepository; private final MatchUserInfoRepository matchUserInfoRepository; - private final NotificationService notificationService; + private final ApplicationEventPublisher publisher; // 매칭 요청 생성 @Transactional @@ -62,8 +63,7 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { Match match = new Match(dto.getMessage(), loginUser, receiver); Match savedMatch = matchRepository.save(match); - Notification notification = notificationService.createNotification(receiver, NotificationType.NEW_MATCH, "/matches/" + savedMatch.getId()); - notificationService.sendNotification(receiver, notification); + publisher.publishEvent(new MatchCreatedEvent(this, savedMatch)); return MatchResponseDto.toDto(match); } @@ -84,13 +84,11 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { } // 로그인한 유저가 매칭의 받는 사람이 아닐때 예외처리 if (dto.getStatus() == MatchStatus.ACCEPTED) { - Notification notification = notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + findMatch.getId()); - notificationService.sendNotification(findMatch.getSender(), notification); + publisher.publishEvent(new MatchAcceptedEvent(this, findMatch)); } // 매칭 보낸 사람에게 매칭이 수락되었다는 알림 전송 if (dto.getStatus() == MatchStatus.REJECTED) { - Notification notification = notificationService.createNotification(findMatch.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + findMatch.getId()); - notificationService.sendNotification(findMatch.getSender(), notification); + publisher.publishEvent(new MatchRejectedEvent(this, findMatch)); } // 매칭 보낸 사람에게 매칭이 거절되었다는 알림 전송 findMatch.updateStatus(dto.getStatus()); diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index bc77a60..dfa7224 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -24,11 +24,10 @@ public class NotificationController { @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity connect( - @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - SseEmitter sseEmitter = notificationService.subscribe(customUserDetails.getUser(), lastEventId); + SseEmitter sseEmitter = notificationService.subscribe(customUserDetails.getUser()); return new ResponseEntity<>(sseEmitter, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 0091bc1..1d4c5f0 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -42,7 +42,7 @@ public List findAllNotification(User loginUser) { .toList(); } - public SseEmitter subscribe(User loginUser, String lastEventId) { + public SseEmitter subscribe(User loginUser) { Long DEFAULT_TIMEOUT = 60L * 1000 * 60; SseEmitter sseEmitter = emitterRepository.save(loginUser.getId(), new SseEmitter(DEFAULT_TIMEOUT)); @@ -64,11 +64,15 @@ public SseEmitter subscribe(User loginUser, String lastEventId) { return sseEmitter; } - @Transactional public void sendNotification(User user, Notification notification) { long startTime = System.currentTimeMillis(); SseEmitter sseEmitter = emitterRepository.findById(user.getId()); + if (sseEmitter == null) { + log.info("User {}는 현재 연결되어 있지 않습니다", user.getId()); + return; + } + try { sseEmitter.send( SseEmitter.event() @@ -78,7 +82,7 @@ public void sendNotification(User user, Notification notification) { ); } catch (IOException e) { emitterRepository.deleteById(user.getId()); - throw new RuntimeException("SSE 연결 오류 발생"); + log.error("알림 전송 실패: {}", e.getMessage()); } finally { long endTime = System.currentTimeMillis(); long elapsedTime = endTime - startTime; diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index 9d0fbfd..6dabe43 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -1,20 +1,19 @@ package com.example.gamemate.domain.reply.service; -import com.example.gamemate.domain.board.entity.Board; import com.example.gamemate.domain.comment.entity.Comment; import com.example.gamemate.domain.comment.repository.CommentRepository; -import com.example.gamemate.domain.notification.entity.Notification; -import com.example.gamemate.domain.notification.enums.NotificationType; -import com.example.gamemate.domain.notification.service.NotificationService; import com.example.gamemate.domain.reply.dto.ReplyRequestDto; import com.example.gamemate.domain.reply.dto.ReplyResponseDto; import com.example.gamemate.domain.reply.entity.Reply; import com.example.gamemate.domain.reply.repository.ReplyRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.eventListener.event.MatchCreatedEvent; +import com.example.gamemate.global.eventListener.event.ReplyCreatedEvent; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,7 +24,7 @@ public class ReplyService { private final ReplyRepository replyRepository; private final CommentRepository commentRepository; - private final NotificationService notificationService; + private final ApplicationEventPublisher publisher; /** * 대댓글 생성 메서드 @@ -44,7 +43,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest if(requestDto.getParentReplyId()==null){ newReply = new Reply(requestDto.getContent(), findComment, loginUser); Reply createReply = replyRepository.save(newReply); - sendCommentNotification(findComment.getBoard(), findComment, createReply); + publisher.publishEvent(new ReplyCreatedEvent(this, createReply)); return new ReplyResponseDto( createReply.getReplyId(), @@ -59,7 +58,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest .orElseThrow(()-> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); newReply = new Reply(requestDto.getContent(), findComment, loginUser, findParentReply); Reply createReply = replyRepository.save(newReply); - sendReplyNotification(findComment.getBoard(), findComment, findParentReply, createReply); + publisher.publishEvent(new ReplyCreatedEvent(this, createReply)); return new ReplyResponseDto( createReply.getReplyId(), @@ -109,24 +108,4 @@ public void deleteReply(User loginUser, Long id) { replyRepository.delete(findReply); } - - // 대댓글 알림 전송 - @Transactional - public void sendCommentNotification(Board board, Comment comment, Reply reply) { - Notification boardNotification = notificationService.createNotification(board.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(board.getUser(), boardNotification); - Notification commentNotification = notificationService.createNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(comment.getUser(), commentNotification); - } - - // 대대댓글 알림 전송 - @Transactional - public void sendReplyNotification(Board board, Comment comment, Reply parentReply, Reply reply) { - Notification boardNotification = notificationService.createNotification(board.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(board.getUser(), boardNotification); - Notification commentNotification = notificationService.createNotification(comment.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(comment.getUser(), commentNotification); - Notification parentReplyNotification = notificationService.createNotification(parentReply.getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(parentReply.getUser(), parentReplyNotification); - } } diff --git a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java new file mode 100644 index 0000000..dd54650 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java @@ -0,0 +1,128 @@ +package com.example.gamemate.global.eventListener; + +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.follow.entity.Follow; +import com.example.gamemate.domain.like.entity.BoardLike; +import com.example.gamemate.domain.like.entity.ReviewLike; +import com.example.gamemate.domain.match.entity.Match; +import com.example.gamemate.domain.notification.entity.Notification; +import com.example.gamemate.domain.notification.enums.NotificationType; +import com.example.gamemate.domain.notification.service.NotificationService; +import com.example.gamemate.domain.reply.entity.Reply; +import com.example.gamemate.global.eventListener.event.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class GlobalEventListener { + + private final NotificationService notificationService; + + @Async + @EventListener + public void handleCreateFollow(FollowCreatedEvent event) { + log.info("새로운 팔로우 알림 전송 시작"); + Follow follow = event.getFollow(); + + Notification notification = notificationService.createNotification(follow.getFollowee(), NotificationType.NEW_FOLLOWER, "/users/" + follow.getFollower().getId()); + notificationService.sendNotification(follow.getFollowee(), notification); + + log.info("새로운 팔로우 알림 전송 완료"); + } + + @Async + @EventListener + public void handleCreateMatch(MatchCreatedEvent event) { + log.info("새로운 매칭 알림 전송 시작"); + Match match = event.getMatch(); + + Notification notification = notificationService.createNotification(match.getReceiver(), NotificationType.NEW_MATCH, "/matches/" + match.getId()); + notificationService.sendNotification(match.getReceiver(), notification); + + log.info("새로운 매칭 알림 전송 완료"); + } + + @Async + @EventListener + public void handleAcceptMatch(MatchAcceptedEvent event) { + log.info("매칭 수락 알림 전송 시작"); + Match match = event.getMatch(); + + Notification notification = notificationService.createNotification(match.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + match.getId()); + notificationService.sendNotification(match.getSender(), notification); + + log.info("매칭 수락 알림 전송 완료"); + } + + @Async + @EventListener + public void handleRejectMatch(MatchRejectedEvent event) { + log.info("매칭 거절 알림 전송 시작"); + Match match = event.getMatch(); + + Notification notification = notificationService.createNotification(match.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + match.getId()); + notificationService.sendNotification(match.getSender(), notification); + + log.info("매칭 거절 알림 전송 완료"); + } + + @Async + @EventListener + public void handleCreateBoardLike(BoardLikeCreatedEvent event) { + log.info("게시글 새로운 좋아요 알림 전송 시작"); + BoardLike boardLike = event.getBoardLike(); + + Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getBoardId()); + notificationService.sendNotification(boardLike.getBoard().getUser(), notification); + + log.info("게시글 새로운 좋아요 알림 전송 완료"); + } + + @Async + @EventListener + public void handleCreateReviewLike(ReviewLikeCreatedEvent event) { + log.info("리뷰 새로운 좋아요 알림 전송 시작"); + ReviewLike reviewLike = event.getReviewLike(); + + Notification notification = notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/reviews/" + reviewLike.getReview().getId()); + notificationService.sendNotification(reviewLike.getReview().getUser(), notification); + + log.info("리뷰 새로운 좋아요 알림 전송 완료"); + } + + @Async + @EventListener + public void handleCreateComment(CommentCreatedEvent event) { + log.info("새로운 댓글 알림 전송 시작"); + Comment comment = event.getComment(); + + Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getCommentId()); + notificationService.sendNotification(comment.getBoard().getUser(), notification); + + log.info("새로운 댓글 알림 전송 완료"); + } + + @Async + @EventListener + public void handleCreateReply(ReplyCreatedEvent event) { + log.info("새로운 대댓글 알림 전송 시작"); + Reply reply = event.getReply(); + + Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getComment().getBoard().getUser(), boardNotification); + Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getComment().getUser(), commentNotification); + + if (reply.getParentReply() != null) { + Notification parentReplyNotification = notificationService.createNotification(reply.getParentReply().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getParentReply().getUser(), parentReplyNotification); + } + + log.info("새로운 대댓글 알림 전송 완료"); + } +} diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/BoardLikeCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/BoardLikeCreatedEvent.java new file mode 100644 index 0000000..98c9015 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/BoardLikeCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.like.entity.BoardLike; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class BoardLikeCreatedEvent extends ApplicationEvent { + private final BoardLike boardLike; + + public BoardLikeCreatedEvent(Object source, BoardLike boardLike) { + super(source); + this.boardLike = boardLike; + } +} diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/CommentCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/CommentCreatedEvent.java new file mode 100644 index 0000000..2044f7c --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/CommentCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.comment.entity.Comment; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class CommentCreatedEvent extends ApplicationEvent { + private final Comment comment; + + public CommentCreatedEvent(Object source, Comment comment) { + super(source); + this.comment = comment; + } +} diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/FollowCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/FollowCreatedEvent.java new file mode 100644 index 0000000..3bbb19b --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/FollowCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.follow.entity.Follow; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class FollowCreatedEvent extends ApplicationEvent { + private final Follow follow; + + public FollowCreatedEvent(Object source, Follow follow) { + super(source); + this.follow = follow; + } +} diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/MatchAcceptedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/MatchAcceptedEvent.java new file mode 100644 index 0000000..a99b258 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/MatchAcceptedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.match.entity.Match; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MatchAcceptedEvent extends ApplicationEvent { + private final Match match; + + public MatchAcceptedEvent(Object source, Match match) { + super(source); + this.match = match; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/MatchCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/MatchCreatedEvent.java new file mode 100644 index 0000000..7d7410c --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/MatchCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.match.entity.Match; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MatchCreatedEvent extends ApplicationEvent { + private final Match match; + + public MatchCreatedEvent(Object source, Match match) { + super(source); + this.match = match; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/MatchRejectedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/MatchRejectedEvent.java new file mode 100644 index 0000000..6b62249 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/MatchRejectedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.match.entity.Match; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MatchRejectedEvent extends ApplicationEvent { + private final Match match; + + public MatchRejectedEvent(Object source, Match match) { + super(source); + this.match = match; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/ReplyCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/ReplyCreatedEvent.java new file mode 100644 index 0000000..ae86ad6 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/ReplyCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.reply.entity.Reply; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class ReplyCreatedEvent extends ApplicationEvent { + private final Reply reply; + + public ReplyCreatedEvent(Object source, Reply reply) { + super(source); + this.reply = reply; + } +} diff --git a/src/main/java/com/example/gamemate/global/eventListener/event/ReviewLikeCreatedEvent.java b/src/main/java/com/example/gamemate/global/eventListener/event/ReviewLikeCreatedEvent.java new file mode 100644 index 0000000..194d16c --- /dev/null +++ b/src/main/java/com/example/gamemate/global/eventListener/event/ReviewLikeCreatedEvent.java @@ -0,0 +1,15 @@ +package com.example.gamemate.global.eventListener.event; + +import com.example.gamemate.domain.like.entity.ReviewLike; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class ReviewLikeCreatedEvent extends ApplicationEvent { + private final ReviewLike reviewLike; + + public ReviewLikeCreatedEvent(Object source, ReviewLike reviewLike) { + super(source); + this.reviewLike = reviewLike; + } +} From 6aaffd62d1736d2952d2726160d30f342cf8e4d0 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Fri, 31 Jan 2025 13:04:35 +0900 Subject: [PATCH 154/215] =?UTF-8?q?fix=20:=20=EB=B3=B8=EC=9D=B8=EC=9D=98?= =?UTF-8?q?=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=EC=9D=B4=EB=82=98=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=97=90=20=EB=8C=93=EA=B8=80,=20=EB=8C=80=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=96=88=EC=9D=84?= =?UTF-8?q?=EB=95=8C=20=EC=95=8C=EB=A6=BC=EC=9D=B4=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=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 --- .../eventListener/GlobalEventListener.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java index dd54650..244b980 100644 --- a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java +++ b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java @@ -16,6 +16,8 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import java.util.Objects; + @Component @RequiredArgsConstructor @Slf4j @@ -101,8 +103,10 @@ public void handleCreateComment(CommentCreatedEvent event) { log.info("새로운 댓글 알림 전송 시작"); Comment comment = event.getComment(); - Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getCommentId()); - notificationService.sendNotification(comment.getBoard().getUser(), notification); + if (!Objects.equals(comment.getUser().getId(), comment.getBoard().getUser().getId())) { + Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getCommentId()); + notificationService.sendNotification(comment.getBoard().getUser(), notification); + } log.info("새로운 댓글 알림 전송 완료"); } @@ -113,12 +117,17 @@ public void handleCreateReply(ReplyCreatedEvent event) { log.info("새로운 대댓글 알림 전송 시작"); Reply reply = event.getReply(); - Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(reply.getComment().getBoard().getUser(), boardNotification); - Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); - notificationService.sendNotification(reply.getComment().getUser(), commentNotification); + if (!Objects.equals(reply.getUser().getId(), reply.getComment().getBoard().getUser().getId())) { + Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getComment().getBoard().getUser(), boardNotification); + } + + if (!Objects.equals(reply.getUser().getId(), reply.getComment().getUser().getId())) { + Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + notificationService.sendNotification(reply.getComment().getUser(), commentNotification); + } - if (reply.getParentReply() != null) { + if (reply.getParentReply() != null && !Objects.equals(reply.getParentReply().getUser().getId(), reply.getUser().getId())) { Notification parentReplyNotification = notificationService.createNotification(reply.getParentReply().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); notificationService.sendNotification(reply.getParentReply().getUser(), parentReplyNotification); } From 996514bee145f7426c7b7341831cdb69e7788439 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 31 Jan 2025 16:43:03 +0900 Subject: [PATCH 155/215] =?UTF-8?q?docs:=20=EC=9E=90=EB=B0=94=EB=8F=85=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/controller/BoardController.java | 12 +++++------- .../gamemate/domain/board/service/BoardService.java | 10 +++++----- .../comment/controller/CommentController.java | 11 +++++++---- .../domain/comment/service/CommentService.java | 13 +++++++------ .../domain/reply/controller/ReplyController.java | 7 ++++--- .../gamemate/domain/reply/service/ReplyService.java | 6 +++--- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index 1cc2344..2c7b72c 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -28,7 +28,7 @@ public class BoardController { private final BoardService boardService; /** - * 게시글 생성 API + * 게시글 생성 API 입니다. * * @param dto 게시글 생성 dto * @param customUserDetails 인증 정보 @@ -45,7 +45,7 @@ public ResponseEntity createBoard( } /** - * 게시글 조회하고 검색하는 API + * 게시글 조회하고 검색하는 API 입니다. * * @param page 페이지 번호(기본값 : 0) * @param category 카테고리 종류 @@ -74,7 +74,7 @@ public ResponseEntity> findAllBoards( } /** - * 게시글 단건 조회하는 API + * 게시글 단건 조회하는 API 입니다. * * @param id 게시글 식별자 * @return 게시글 ResponseEntity @@ -89,10 +89,8 @@ public ResponseEntity findBoardById( } - /** - * /** - * 게시글 업데이트하는 API + * 게시글 업데이트하는 API 입니다. * * @param id 게시글 식별자 * @param dto 게시글 업데이트 dto @@ -111,7 +109,7 @@ public ResponseEntity updateBoard( } /** - * 게시글 삭제하는 API + * 게시글 삭제하는 API 입니다. * * @param id 게시글 식별자 * @param customUserDetails 인증 정보 diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 542639b..96a2538 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -30,7 +30,7 @@ public class BoardService { private final BoardRepository boardRepository; /** - * 게시글 생성 메서드 + * 게시글 생성 메서드입니다. * * @param loginUser 로그인한 유저 * @param dto 게시글 생성 dto @@ -53,7 +53,7 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { } /** - * 게시판 리스트 조회 메서드 + * 게시판 리스트 조회 메서드입니다. * * @param page 페이지 번호 (기본값 : 0) * @param category 게시글 카테고리 @@ -80,7 +80,7 @@ public List findAllBoards(int page, BoardCategory categ } /** - * 게시글 단건 조회 메서드 + * 게시글 단건 조회 메서드입니다. * * @param id 게시글 식별자 * @return 게시글 조회 ResponseDto @@ -94,7 +94,7 @@ public BoardFindOneResponseDto findBoardById(Long id) { } /** - * 게시글 업데이트 메서드 + * 게시글 업데이트 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 게시글 식별자 @@ -116,7 +116,7 @@ public void updateBoard(User loginUser, Long id, BoardRequestDto dto) { } /** - * 게시글 삭제 메서드 + * 게시글 삭제 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 게시글 식별자 diff --git a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java index 66924e3..d4fa8f2 100644 --- a/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/gamemate/domain/comment/controller/CommentController.java @@ -21,7 +21,8 @@ public class CommentController { private final CommentService commentService; /** - * 댓글 생성 API + * 댓글 생성 API 입니다. + * * @param boardId 게시글 식별자 * @param requestDto 댓글 요청 Dto * @return 생성된 댓글 정보를 포함한 ResponseEntity @@ -37,7 +38,8 @@ public ResponseEntity createComment( } /** - * 댓글/대댓글 조회 + * 댓글/대댓글 조회 입니다. + * * @param boardId 댓글 식별자 * @param page 페이지 번호(기본값 : 0) * @return 댓글 리스트 ResponseEntity @@ -52,7 +54,7 @@ public ResponseEntity> getComments( } /** - * 댓글 수정 API + * 댓글 수정 API 입니다. * * @param id 댓글 식별자 * @param requestDto 업데이트할 댓글 Dto @@ -69,7 +71,8 @@ public ResponseEntity updateComment( } /** - * 댓글 삭제 API + * 댓글 삭제 API 입니다. + * * @param id 댓글 식별자 * @param customUserDetails 인증된 사용자 정보 * @return Void diff --git a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java index a70444a..a259cb8 100644 --- a/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java +++ b/src/main/java/com/example/gamemate/domain/comment/service/CommentService.java @@ -39,7 +39,8 @@ public class CommentService { private final NotificationService notificationService; /** - * 댓글 생성 메서드 + * 댓글 생성 메서드입니다. + * * @param loginUser 로그인한 유저 * @param boardId 게시글 식별자 * @param requestDto 댓글 생성할 requestDto @@ -65,7 +66,7 @@ public CommentResponseDto createComment(User loginUser, Long boardId, CommentReq } /** - * 댓글 업데이트 메서드 + * 댓글 업데이트 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 댓글 식별자 @@ -87,7 +88,7 @@ public void updateComment(User loginUser, Long id, CommentRequestDto requestDto) } /** - * 댓글 삭제 메서드 + * 댓글 삭제 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 댓글 식별자 @@ -107,7 +108,7 @@ public void deleteComment(User loginUser, Long id) { } /** - * 댓글 조회 메서드 + * 댓글 조회 메서드입니다. * * @param boardId 게시글 식별자 * @param page 페이지 번호(기본값 : 0) @@ -130,7 +131,7 @@ public List getComments(Long boardId, int page) { } /** - * 댓글 Dto 변환 + * 댓글 Dto 변환입니다. * * @param comment comment * @return 댓글 조회 Dto @@ -152,7 +153,7 @@ private CommentFindResponseDto convertCommentDto(Comment comment) { } /** - * 대댓글 Dto 변환 + * 대댓글 Dto 변환입니다. * * @param reply 대댓글 * @return 대댓글 조회 Dto diff --git a/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java index ddb0a6f..476e3e8 100644 --- a/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java +++ b/src/main/java/com/example/gamemate/domain/reply/controller/ReplyController.java @@ -19,7 +19,7 @@ public class ReplyController { private final ReplyService replyService; /** - * 대댓글 생성 API + * 대댓글 생성 API 입니다. * * @param commentId 댓글 식별자 * @param requestDto 대댓글 생성 Dto @@ -37,7 +37,7 @@ public ResponseEntity createReply( } /** - * 대댓글 수정 API + * 대댓글 수정 API 입니다. * * @param id 대댓글 식별자 * @param requestDto 업데이트할 대댓글 Dto @@ -55,7 +55,8 @@ public ResponseEntity updateReply( } /** - * 대댓글 삭제 API + * 대댓글 삭제 API 입니다. + * * @param id 대댓글 식별자 * @param customUserDetails 인증된 사용자 * @return Void diff --git a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java index a4b0e73..862d05c 100644 --- a/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java +++ b/src/main/java/com/example/gamemate/domain/reply/service/ReplyService.java @@ -26,7 +26,7 @@ public class ReplyService { private final NotificationService notificationService; /** - * 대댓글 생성 메서드 + * 대댓글 생성 메서드입니다. * * @param loginUser 로그인한 유저 * @param commentId 댓글 식별자 @@ -73,7 +73,7 @@ public ReplyResponseDto createReply(User loginUser, Long commentId, ReplyRequest } /** - * 대댓글 업데이트 메서드 + * 대댓글 업데이트 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 대댓글 식별자 @@ -95,7 +95,7 @@ public void updateReply(User loginUser, Long id, ReplyRequestDto requestDto) { } /** - * 대댓글 메서드 + * 대댓글 메서드입니다. * * @param loginUser 로그인한 유저 * @param id 대댓글 식별자 From 6ebe53f4dfd9a916b15a3bff2e55807a936e0fa1 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 31 Jan 2025 21:03:19 +0900 Subject: [PATCH 156/215] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 회원/비회원 나누어서 조회수 카운트하도록 구현 2. SecurityConfig 게시글 조회는 permitAll로 변경 --- build.gradle | 4 +- .../board/controller/BoardController.java | 8 +- .../gamemate/domain/board/entity/Board.java | 5 +- .../board/scheduler/ViewCountScheduler.java | 36 ++++++++ .../domain/board/service/BoardService.java | 32 ++++++- .../global/config/SecurityConfig.java | 2 + .../global/redis/service/RedisService.java | 88 +++++++++++++++++++ 7 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java create mode 100644 src/main/java/com/example/gamemate/global/redis/service/RedisService.java diff --git a/build.gradle b/build.gradle index 2a52670..8f9007d 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' implementation 'at.favre.lib:bcrypt:0.10.2' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-security' @@ -69,6 +68,9 @@ dependencies { // mail implementation 'org.springframework.boot:spring-boot-starter-mail' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index 2c7b72c..503c071 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -6,6 +6,7 @@ import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.service.BoardService; +import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -81,10 +82,13 @@ public ResponseEntity> findAllBoards( */ @GetMapping("/{id}") public ResponseEntity findBoardById( - @PathVariable Long id + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails customUserDetails ){ - BoardFindOneResponseDto dto = boardService.findBoardById(id); + User loginUser = customUserDetails != null ? customUserDetails.getUser() : null; + + BoardFindOneResponseDto dto = boardService.findBoardById(id, loginUser); return new ResponseEntity<>(dto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/board/entity/Board.java b/src/main/java/com/example/gamemate/domain/board/entity/Board.java index 9aa59af..c15f364 100644 --- a/src/main/java/com/example/gamemate/domain/board/entity/Board.java +++ b/src/main/java/com/example/gamemate/domain/board/entity/Board.java @@ -29,7 +29,7 @@ public class Board extends BaseEntity { @Column(nullable = false) private String content; - private final int views = 0; + private int views = 0; @ManyToOne @JoinColumn(name = "user_id") @@ -51,4 +51,7 @@ public void updateBoard(BoardCategory category, String title, String content) { this.content = content; } + public void updateViewCount(int views){ + this.views=views; + } } diff --git a/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java b/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java new file mode 100644 index 0000000..0737517 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java @@ -0,0 +1,36 @@ +package com.example.gamemate.domain.board.scheduler; + +import com.example.gamemate.domain.board.dto.BoardResponseDto; +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.global.redis.service.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.CountedCompleter; + +@Component +@RequiredArgsConstructor +public class ViewCountScheduler { + + private final BoardRepository boardRepository; + private final RedisService redisService; + + // 1시간마다 Redis 조회수를 DB에 반영 + //@Scheduled(fixedRate = 60 * 60 * 1000) + @Scheduled(fixedRate = 180000) + public void syncViewCounts(){ + List boards = boardRepository.findAll(); + + for(Board board : boards){ + int viewCount = redisService.getViewCount(board.getId()); + if(viewCount > 0){ + board.updateViewCount(viewCount); + boardRepository.save(board); + redisService.transferViewCountToDB(board.getId(), viewCount); + } + } + } +} diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 96a2538..a5ddd35 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -8,9 +8,17 @@ import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.enums.ListSize; import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; +import com.example.gamemate.domain.comment.entity.Comment; +import com.example.gamemate.domain.comment.repository.CommentRepository; +import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; +import com.example.gamemate.domain.reply.entity.Reply; +import com.example.gamemate.domain.reply.repository.ReplyRepository; +import com.example.gamemate.domain.reply.service.ReplyService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; +import com.example.gamemate.global.redis.service.RedisService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -28,6 +36,9 @@ public class BoardService { private final BoardRepository boardRepository; + private final CommentRepository commentRepository; + private final ReplyRepository replyRepository; + private final RedisService redisService; /** * 게시글 생성 메서드입니다. @@ -85,7 +96,15 @@ public List findAllBoards(int page, BoardCategory categ * @param id 게시글 식별자 * @return 게시글 조회 ResponseDto */ - public BoardFindOneResponseDto findBoardById(Long id) { + @Transactional + public BoardFindOneResponseDto findBoardById(Long id, User loginUser) { + // 조회수 증가(Redis 저장) + if(loginUser == null) { + redisService.increaseViewCount(id, null); + }else{ + redisService.increaseViewCount(id, loginUser.getId()); + } + // 게시글 조회 Board findBoard = boardRepository.findById(id) .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); @@ -93,6 +112,17 @@ public BoardFindOneResponseDto findBoardById(Long id) { return new BoardFindOneResponseDto(findBoard); } + /** + * 게시글 조회수 리턴하는 메서드입니다. + * + * @param boardId 게시글 식별자 + * @return + */ + public int getPostViewCount(Long boardId){ + return redisService.getViewCount(boardId); + } + + /** * 게시글 업데이트 메서드입니다. * diff --git a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java index e916311..5745f13 100644 --- a/src/main/java/com/example/gamemate/global/config/SecurityConfig.java +++ b/src/main/java/com/example/gamemate/global/config/SecurityConfig.java @@ -52,6 +52,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/signup", "/auth/login", "/auth/refresh", "/auth/email/**").permitAll() .requestMatchers("/oauth2/**", "/login/oauth2/**", "/auth/oauth2/**").permitAll() .requestMatchers("/oauth2-login.html", "/oauth2-login-failure.html", "/oauth2-login-success.html", "/oauth2-set-password.html").permitAll() + .requestMatchers(HttpMethod.GET, "/boards").permitAll() + .requestMatchers(HttpMethod.GET, "/boards/*").permitAll() .requestMatchers(HttpMethod.GET,"/games", "/games/{id}").hasRole("USER") .requestMatchers(HttpMethod.POST,"/games/requests").hasRole("USER") .requestMatchers("/games/recommendations/**").hasRole("USER") diff --git a/src/main/java/com/example/gamemate/global/redis/service/RedisService.java b/src/main/java/com/example/gamemate/global/redis/service/RedisService.java new file mode 100644 index 0000000..d978b27 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/redis/service/RedisService.java @@ -0,0 +1,88 @@ +package com.example.gamemate.global.redis.service; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RedisService { + + //private final StringRedisTemplate redisTemplate; + private final String VIEW_COUNT_KEY = "board:view:"; + private final RedisTemplate redisTemplate; + private final HttpServletRequest request; + + /** + * 조회수 증가하는 메서드입니다. + * + * @param boardId 게시글 식별자 + */ + public void increaseViewCount(Long boardId, Long userId) { + String uniqueKey; + + if (userId != null) { + // 회원 : userId 기반으로 조회 제한 + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + userId; + } else { + // 비회원 + String ipAddress = getClientIp(); + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + ipAddress; + } + + if (Boolean.FALSE.equals(redisTemplate.hasKey(uniqueKey))) { + redisTemplate.opsForValue().set(uniqueKey, "1", Duration.ofHours(1)); + redisTemplate.opsForValue().increment(VIEW_COUNT_KEY + boardId); + } + } + + /** + * 조회수 가져오는 메서드 입니다. + * + * @param boardId 게시글 식별자 + * @return 조회수 + */ + public int getViewCount(Long boardId){ + String key = VIEW_COUNT_KEY + boardId; + String count = redisTemplate.opsForValue().get(key); + return count == null ? 0 : Integer.parseInt(count); + } + + /** + * 클라이언트 IP 가져오는 메서드입니다.(프록시) + * + * @return ip 주소 + */ + private String getClientIp(){ + String ip = request.getHeader("x-forwarded-for"); + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 조회수를 DB에 반영 후 Redis 에서 삭제하는 메서드입니다. + * + * @param boardId 게시글 식별자 + * @param count 조회수 + */ + public void transferViewCountToDB(Long boardId, int count){ + redisTemplate.delete(VIEW_COUNT_KEY + boardId); + } +} From b1c5dedb44357c58f87ff3db2b2346c2086ac27c Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Sun, 2 Feb 2025 18:24:16 +0900 Subject: [PATCH 157/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20Redis=20Stream=20=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=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 1. 알림 저장시 Redis 에 알림 저장 2. 오프라인 사용자가 로그인시 미확인 알림 전송 --- build.gradle | 3 + .../dto/NotificationResponseDto.java | 6 +- .../repository/EmitterRepository.java | 34 +++++-- .../service/NotificationService.java | 86 +++++++++--------- .../service/RedisStreamService.java | 91 +++++++++++++++++++ .../gamemate/global/config/RedisConfig.java | 25 +++++ .../eventListener/GlobalEventListener.java | 10 +- src/main/resources/application.properties | 3 + src/main/resources/application.yml | 11 --- 9 files changed, 203 insertions(+), 66 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java create mode 100644 src/main/java/com/example/gamemate/global/config/RedisConfig.java delete mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index 8ea053b..9aac7d3 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,9 @@ dependencies { // mail implementation 'org.springframework.boot:spring-boot-starter-mail' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java index 66c0236..6e1ef0a 100644 --- a/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/notification/dto/NotificationResponseDto.java @@ -10,15 +10,17 @@ public class NotificationResponseDto { private String content; private NotificationType type; private String relatedUrl; + private Long receiverId; - public NotificationResponseDto(Long id, String content, NotificationType type, String relatedUrl) { + public NotificationResponseDto(Long id, String content, NotificationType type, String relatedUrl, Long receiverId) { this.id = id; this.content = content; this.type = type; this.relatedUrl = relatedUrl; + this.receiverId = receiverId; } public static NotificationResponseDto toDto(Notification notification) { - return new NotificationResponseDto(notification.getId(), notification.getContent(), notification.getType(), notification.getRelatedUrl()); + return new NotificationResponseDto(notification.getId(), notification.getContent(), notification.getType(), notification.getRelatedUrl(), notification.getReceiver().getId()); } } diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java index 664453b..5b5664c 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/EmitterRepository.java @@ -1,5 +1,6 @@ package com.example.gamemate.domain.notification.repository; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -7,20 +8,41 @@ import java.util.concurrent.ConcurrentHashMap; @Repository +@Slf4j public class EmitterRepository { + // ConcurrentHashMap을 사용하여 thread-safe하게 관리 private final Map emitters = new ConcurrentHashMap<>(); - private final Map eventCache = new ConcurrentHashMap<>(); - public SseEmitter findById(Long userId) { - return emitters.get(userId); + public SseEmitter save(Long userId, SseEmitter emitter) { + emitters.put(userId, emitter); + log.info("EmitterRepository - userId: {} 연결 저장", userId); + + // 연결 종료 시 자동 제거 + emitter.onCompletion(() -> { + log.info("EmitterRepository - userId: {} 연결 종료", userId); + this.deleteById(userId); + }); + emitter.onTimeout(() -> { + log.info("EmitterRepository - userId: {} 연결 시간 초과", userId); + this.deleteById(userId); + }); + emitter.onError((e) -> { + log.error("EmitterRepository - userId: {} 연결 에러: {}", userId, e.getMessage()); + this.deleteById(userId); + }); + + return emitter; } - public SseEmitter save(Long userId, SseEmitter sseEmitter) { - emitters.put(userId, sseEmitter); + public SseEmitter findById(Long userId) { return emitters.get(userId); } public void deleteById(Long userId) { emitters.remove(userId); } -} + + public Map findAll() { + return emitters; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 1d4c5f0..a7e7044 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -6,6 +6,7 @@ import com.example.gamemate.domain.notification.repository.EmitterRepository; import com.example.gamemate.domain.notification.repository.NotificationRepository; import com.example.gamemate.domain.user.entity.User; +import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -14,6 +15,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -22,71 +24,71 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final EmitterRepository emitterRepository; + private final RedisStreamService redisStreamService; + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + + @PostConstruct + public void init() { + redisStreamService.createStreamGroup(); + } - // 알림 생성 @Transactional public Notification createNotification(User user, NotificationType type, String relatedUrl) { Notification notification = new Notification(type.getContent(), relatedUrl, type, user); - Notification savedNotification = notificationRepository.save(notification); - return savedNotification; + return notificationRepository.save(notification); } - // 알림 전체 보기 public List findAllNotification(User loginUser) { - - List notificationList = notificationRepository.findAllByReceiverId(loginUser.getId()); - - return notificationList + return notificationRepository.findAllByReceiverId(loginUser.getId()) .stream() .map(NotificationResponseDto::toDto) .toList(); } public SseEmitter subscribe(User loginUser) { - Long DEFAULT_TIMEOUT = 60L * 1000 * 60; - SseEmitter sseEmitter = emitterRepository.save(loginUser.getId(), new SseEmitter(DEFAULT_TIMEOUT)); - - sseEmitter.onCompletion(() -> emitterRepository.deleteById(loginUser.getId())); - sseEmitter.onTimeout(() -> emitterRepository.deleteById(loginUser.getId())); + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); try { - sseEmitter.send( - SseEmitter.event() - .id(loginUser.getId().toString()) - .name("connect") - .data("connected!") - ); + // 연결 직후 더미 데이터를 보내 503 에러 방지 + emitter.send(SseEmitter.event() + .name("connect") + .data("connected!")); + + // 미처리된 알림 조회 및 전송 + List> unreadNotifications = + redisStreamService.getUnreadNotifications(loginUser.getId()); + + for (Map notification : unreadNotifications) { + emitter.send(SseEmitter.event() + .name(notification.get("type")) + .data(notification)); + } + } catch (IOException e) { - emitterRepository.deleteById(loginUser.getId()); - throw new RuntimeException("SSE 연결 오류 발생"); + throw new RuntimeException("연결 실패!"); } - return sseEmitter; + // emitter를 저장소에 저장 (저장 시 이벤트 핸들러도 자동 등록) + return emitterRepository.save(loginUser.getId(), emitter); } public void sendNotification(User user, Notification notification) { - long startTime = System.currentTimeMillis(); - SseEmitter sseEmitter = emitterRepository.findById(user.getId()); + NotificationResponseDto notificationDto = NotificationResponseDto.toDto(notification); - if (sseEmitter == null) { - log.info("User {}는 현재 연결되어 있지 않습니다", user.getId()); - return; - } + // Redis 스트림에 저장 + redisStreamService.addNotificationToStream(notificationDto); - try { - sseEmitter.send( - SseEmitter.event() - .id(user.getId().toString()) - .name(notification.getType().getName()) - .data(NotificationResponseDto.toDto(notification)) - ); - } catch (IOException e) { - emitterRepository.deleteById(user.getId()); - log.error("알림 전송 실패: {}", e.getMessage()); - } finally { - long endTime = System.currentTimeMillis(); - long elapsedTime = endTime - startTime; - log.info("sendNotification 메서드 실행 시간: {}ms", elapsedTime); + // SSE로 전송 + SseEmitter emitter = emitterRepository.findById(user.getId()); + if (emitter != null) { + try { + emitter.send(SseEmitter.event() + .name(notification.getType().name()) + .data(notificationDto)); + } catch (IOException e) { + emitterRepository.deleteById(user.getId()); + log.error("알림 전송 실패: {}", e.getMessage()); + } } } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java new file mode 100644 index 0000000..4d8b2c1 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java @@ -0,0 +1,91 @@ +package com.example.gamemate.domain.notification.service; + +import com.example.gamemate.domain.notification.dto.NotificationResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import java.util.stream.Collectors; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RedisStreamService { + private final RedisTemplate redisTemplate; + private static final String STREAM_KEY = "notification_stream"; + private static final int MAX_STREAM_LENGTH = 1000; + + public void addNotificationToStream(NotificationResponseDto notification) { + try { + Map notificationMap = new HashMap<>(); + notificationMap.put("id", notification.getId().toString()); + notificationMap.put("content", notification.getContent()); + notificationMap.put("type", notification.getType().name()); + notificationMap.put("relatedUrl", notification.getRelatedUrl()); + notificationMap.put("receiverId", notification.getReceiverId().toString()); + + // 알림 추가 + redisTemplate.opsForStream().add( + StreamRecords.newRecord() + .in(STREAM_KEY) + .ofMap(notificationMap) + ); + + // 크기 관리 + manageStreamSize(); + + } catch (Exception e) { + log.error("스트림 저장 실패: {}", e.getMessage()); + } + } + + public void createStreamGroup() { + try { + redisTemplate.opsForStream().createGroup(STREAM_KEY, "notification-group"); + } catch (Exception e) { + log.info("스트림 그룹이 이미 존재합니다: {}", e.getMessage()); + } + } + + private void manageStreamSize() { + try { + long length = redisTemplate.opsForStream().info(STREAM_KEY).streamLength(); + if (length > MAX_STREAM_LENGTH) { + redisTemplate.opsForStream().trim(STREAM_KEY, MAX_STREAM_LENGTH, false); + } + } catch (Exception e) { + log.error("스트림 크기 관리 중 오류 발생: {}", e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + public List> getUnreadNotifications(Long userId) { + try { + List> records = redisTemplate.opsForStream() + .read(StreamOffset.fromStart(STREAM_KEY)); + + return records.stream() + .map(record -> { + Map originalMap = record.getValue(); + Map convertedMap = new HashMap<>(); + originalMap.forEach((key, value) -> + convertedMap.put(key.toString(), value.toString()) + ); + return convertedMap; + }) + .filter(map -> map.get("receiverId").equals(userId.toString())) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("미처리 알림 조회 실패: {}", e.getMessage()); + return Collections.emptyList(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java new file mode 100644 index 0000000..59f3729 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -0,0 +1,25 @@ +package com.example.gamemate.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.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + + // String 타입을 위한 직렬화 설정 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java index 244b980..fbdb72f 100644 --- a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java +++ b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java @@ -79,7 +79,7 @@ public void handleCreateBoardLike(BoardLikeCreatedEvent event) { log.info("게시글 새로운 좋아요 알림 전송 시작"); BoardLike boardLike = event.getBoardLike(); - Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getBoardId()); + Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getId()); notificationService.sendNotification(boardLike.getBoard().getUser(), notification); log.info("게시글 새로운 좋아요 알림 전송 완료"); @@ -104,7 +104,7 @@ public void handleCreateComment(CommentCreatedEvent event) { Comment comment = event.getComment(); if (!Objects.equals(comment.getUser().getId(), comment.getBoard().getUser().getId())) { - Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getCommentId()); + Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getId()); notificationService.sendNotification(comment.getBoard().getUser(), notification); } @@ -118,17 +118,17 @@ public void handleCreateReply(ReplyCreatedEvent event) { Reply reply = event.getReply(); if (!Objects.equals(reply.getUser().getId(), reply.getComment().getBoard().getUser().getId())) { - Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); notificationService.sendNotification(reply.getComment().getBoard().getUser(), boardNotification); } if (!Objects.equals(reply.getUser().getId(), reply.getComment().getUser().getId())) { - Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); notificationService.sendNotification(reply.getComment().getUser(), commentNotification); } if (reply.getParentReply() != null && !Objects.equals(reply.getParentReply().getUser().getId(), reply.getUser().getId())) { - Notification parentReplyNotification = notificationService.createNotification(reply.getParentReply().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getReplyId()); + Notification parentReplyNotification = notificationService.createNotification(reply.getParentReply().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); notificationService.sendNotification(reply.getParentReply().getUser(), parentReplyNotification); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26ee1c8..401a58f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -87,3 +87,6 @@ spring.servlet.multipart.max-request-size=5MB spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always +# Redis +spring.data.redis.host=localhost +spring.data.redis.port=6379 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 4aae4e3..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,11 +0,0 @@ -cloud: - aws: - credentials: - access-key: YOUR_ACCESS_KEY - secret-key: YOUR_SECRET_KEY - s3: - bucket: YOUR_BUCKET_NAME - region: - static: ap-northeast-2 - stack: - auto: false \ No newline at end of file From 078620c17ceeed771bf9123148143a439ceedfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 3 Feb 2025 03:08:41 +0900 Subject: [PATCH 158/215] =?UTF-8?q?refactor:=20=EC=BF=A0=ED=8F=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=EC=9D=84=20=EC=9C=84=ED=95=9C=20Redis=20=EB=B6=84?= =?UTF-8?q?=EC=82=B0=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++ .../domain/coupon/entity/UserCoupon.java | 2 +- .../domain/coupon/service/CouponService.java | 57 ++++++++---------- .../global/common/aop/DistributedLock.java | 16 +++++ .../global/common/aop/DistributedLockAop.java | 60 +++++++++++++++++++ .../gamemate/global/config/RedisConfig.java | 18 ++++++ src/main/resources/application.properties | 6 +- 7 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/example/gamemate/global/common/aop/DistributedLock.java create mode 100644 src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java create mode 100644 src/main/java/com/example/gamemate/global/config/RedisConfig.java diff --git a/build.gradle b/build.gradle index 2a52670..a6043ad 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,8 @@ dependencies { // test testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' //S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' @@ -69,6 +71,10 @@ dependencies { // mail implementation 'org.springframework.boot:spring-boot-starter-mail' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.27.1' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java index 61f4a96..e89ffe0 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/UserCoupon.java @@ -18,7 +18,7 @@ public class UserCoupon extends BaseEntity { private Long id; @Column(nullable = false) - private Boolean isUsed; + private Boolean isUsed = false; @Column(nullable = false) private LocalDateTime issuedAt; diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 261edbc..91e409b 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -9,11 +9,11 @@ import com.example.gamemate.domain.coupon.repository.UserCouponRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.common.aop.DistributedLock; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,40 +50,35 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U return new CouponCreateResponseDto(savedCoupon); } + @DistributedLock(key = "'LOCK:' + #couponId + ':' + #loginUser.id") public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { - try { - Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId) - .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); - - // 발급 가능 체크 - if (!coupon.isIssuable()) { - throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); - } - - // 수량 체크 - if (coupon.isExhausted()) { - throw new ApiException(ErrorCode.COUPON_EXHAUSTED); - } - - // 중복 발급 체크 - if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { - throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); - } - - // 쿠폰 발급 - coupon.incrementIssuedQuantity(); - UserCoupon userCoupon = new UserCoupon(loginUser, coupon); - userCoupon.updateIsUsed(false); - - UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); - - return new CouponIssueResponseDto(savedUserCoupon); - } catch (PessimisticLockingFailureException e) { - log.error("쿠폰 발급 동시 요청으로 잠금 획득 실패: {}", couponId, e); - throw new ApiException(ErrorCode.COUPON_ISSUE_FAILED); + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); + + // 발급 가능 체크 + if (!coupon.isIssuable()) { + throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); + } + + // 중복 발급 체크 + if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { + throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); + } + + // 수량 체크 + if (coupon.isExhausted()) { + throw new ApiException(ErrorCode.COUPON_EXHAUSTED); } + + // 쿠폰 발급 + coupon.incrementIssuedQuantity(); + UserCoupon userCoupon = new UserCoupon(loginUser, coupon); + UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); + + return new CouponIssueResponseDto(savedUserCoupon); } + @Transactional(readOnly = true) public List findMyCoupons(User loginUser) { List userCoupons = userCouponRepository.findByUserId(loginUser.getId()); diff --git a/src/main/java/com/example/gamemate/global/common/aop/DistributedLock.java b/src/main/java/com/example/gamemate/global/common/aop/DistributedLock.java new file mode 100644 index 0000000..bef8942 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/common/aop/DistributedLock.java @@ -0,0 +1,16 @@ +package com.example.gamemate.global.common.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DistributedLock { + String key(); + long waitTime() default 5L; + long leaseTime() default 3L; + TimeUnit timeUnit() default TimeUnit.SECONDS; +} diff --git a/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java b/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java new file mode 100644 index 0000000..bd05923 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java @@ -0,0 +1,60 @@ +package com.example.gamemate.global.common.aop; + +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class DistributedLockAop { + private final RedissonClient redissonClient; + private static final String LOCK_PREFIX = "LOCK:"; + + @Around("@annotation(distributedLock)") + public Object executeWithLock( + ProceedingJoinPoint joinPoint, + DistributedLock distributedLock + ) throws Throwable { + String lockKey = LOCK_PREFIX + joinPoint.getArgs()[0] + ":" + ((User) joinPoint.getArgs()[1]).getId(); + RLock lock = redissonClient.getLock(lockKey); + + log.info("락 획득 시도: {}", lockKey); + lock.lock(distributedLock.leaseTime(), distributedLock.timeUnit()); + log.info("락 획득 완료: {}", lockKey); + + try { + Object result = joinPoint.proceed(); + + // 트랜잭션이 완료된 후 락 해제 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void afterCommit() { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + log.info("락 해제 완료: {}", lockKey); + } + } + }); + + return result; + } catch (Exception e) { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + throw e; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java new file mode 100644 index 0000000..56de371 --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -0,0 +1,18 @@ +package com.example.gamemate.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 RedisConfig { + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://127.0.0.1:6379"); + return Redisson.create(config); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0de4f11..ec7ffa4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -88,4 +88,8 @@ spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always # Lock timeout -spring.jpa.properties.jakarta.persistence.lock.timeout=3000 \ No newline at end of file +spring.jpa.properties.jakarta.persistence.lock.timeout=3000 + +# Redis +spring.data.redis.host=localhost +spring.data.redis.port=6379 \ No newline at end of file From feb522e5658dbf63eb977be13f454ffa99eb2a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 3 Feb 2025 03:14:24 +0900 Subject: [PATCH 159/215] =?UTF-8?q?fix:=20=EC=98=88=EC=95=BD=EC=96=B4?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=B1=ED=8B=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/gamemate/domain/user/entity/User.java | 2 +- src/main/resources/data.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index eeb8248..01b3040 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -14,7 +14,7 @@ import java.util.List; @Entity -@Table(name = "user") +@Table(name = "`user`") @Getter @NoArgsConstructor public class User extends BaseEntity { diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index bf33282..48f4224 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,5 +1,5 @@ -- User 테이블 데이터 -INSERT INTO user ( +INSERT INTO `user` ( email, name, nickname, From b37ca7a5bc6470e77ce4b0a825c9faa9059599ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 3 Feb 2025 03:15:00 +0900 Subject: [PATCH 160/215] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gamemate/domain/user/entity/User.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index 01b3040..92b1d8f 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -111,8 +111,4 @@ public void integrateOAuthProvider(AuthProvider provider, String providerId) { this.providerId = providerId; } - public void setOAuthPassword(String password) { - this.password = password; - } - } From f4abad4b5f88ab3b3d7b4f9e3498b3c44d100dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 3 Feb 2025 03:25:17 +0900 Subject: [PATCH 161/215] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=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 - Spring Security filter chain 예외 처리 방식에 맞춰 수정 --- .../global/exception/GlobalExceptionHandler.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java index 508b2a8..dc3244c 100644 --- a/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/gamemate/global/exception/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -75,14 +76,10 @@ public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotVali return ResponseEntity.badRequest().body(errorResponse); } - @ExceptionHandler({ - ExpiredJwtException.class, - SignatureException.class, - MalformedJwtException.class, - }) - public ResponseEntity handleJwtException(Exception e) { - log.warn("handleJwtException", e); - ErrorCode errorCode = ErrorCode.INVALID_TOKEN; + @ExceptionHandler(InsufficientAuthenticationException.class) + public ResponseEntity handleInsufficientAuthenticationException(InsufficientAuthenticationException e) { + log.warn("Authentication exception", e); + ErrorCode errorCode = ErrorCode.UNAUTHORIZED; return handleExceptionInternal(errorCode, errorCode.getMessage()); } From da3d62a9de6a7c5cd7fb0eb8f1dfef4c40a17a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Mon, 3 Feb 2025 12:00:56 +0900 Subject: [PATCH 162/215] =?UTF-8?q?fix:=20=EC=BF=A0=ED=8F=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=9D=91=EB=8B=B5=20dto=EC=97=90=20=EC=88=98?= =?UTF-8?q?=EB=9F=89=20=ED=95=84=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 --- .../gamemate/domain/coupon/dto/CouponCreateResponseDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java index 60c82b9..d02dc4c 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/coupon/dto/CouponCreateResponseDto.java @@ -13,6 +13,7 @@ public class CouponCreateResponseDto { private final Integer discountAmount; private final LocalDateTime startAt; private final LocalDateTime expiredAt; + private final Integer quantity; public CouponCreateResponseDto(Coupon coupon) { this.id = coupon.getId(); @@ -21,5 +22,6 @@ public CouponCreateResponseDto(Coupon coupon) { this.discountAmount = coupon.getDiscountAmount(); this.startAt = coupon.getStartAt(); this.expiredAt = coupon.getExpiredAt(); + this.quantity = coupon.getTotalQuantity(); } } From 582818c818a915f6c02274b784995d372c8b01d9 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 12:34:03 +0900 Subject: [PATCH 163/215] =?UTF-8?q?fix=20:=20=ED=8C=94=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=ED=95=98=EA=B8=B0=EA=B0=80=20=EC=A0=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=9E=91=EB=8F=99=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 본인의 팔로우가 아닐때 예외처리 하는 부분의 객체끼리 비교하는 것을 객체의 아이디를 비교하도록 수정 --- .../example/gamemate/domain/follow/service/FollowService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 26f3913..78e2a70 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -66,7 +66,7 @@ public void deleteFollow(Long id, User loginUser) { log.info("사용자 email : {}" , loginUser.getEmail()); - if (findFollow.getFollower() != loginUser) { + if (!Objects.equals(findFollow.getFollower().getId(), loginUser.getId())) { throw new ApiException(ErrorCode.INVALID_INPUT); } // 본인의 팔로우가 아닐때 예외처리 From ab6be3626bf219837546d4a4b57ede65647e73bb Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 12:39:18 +0900 Subject: [PATCH 164/215] =?UTF-8?q?docs=20:=20FollowService=20=EC=9E=90?= =?UTF-8?q?=EB=B0=94=EB=8F=85=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/follow/service/FollowService.java | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 78e2a70..0218e5a 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -18,6 +18,10 @@ import java.util.List; import java.util.Objects; + +/** + * 팔로우 기능을 처리하는 서비스 클래스입니다. + */ @Service @RequiredArgsConstructor @Slf4j @@ -26,7 +30,13 @@ public class FollowService { private final FollowRepository followRepository; private final NotificationService notificationService; - // 팔로우하기 + + /** + * 사용자 간의 팔로우를 생성합니다. + * @param dto FollowCreateRequestDto 팔로우할 상대방의 email + * @param loginUser 현재 인증된 사용자 정보 + * @return 팔로우 처리 결과를 담은 FollowResponseDto + */ @Transactional public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser) { @@ -57,7 +67,11 @@ public FollowResponseDto createFollow(FollowCreateRequestDto dto, User loginUser ); } - // 팔로우 취소하기 + /** + * 사용자 간의 팔로우를 취소합니다. + * @param id 팔로우 id + * @param loginUser 현재 인증된 사용자 정보 + */ @Transactional public void deleteFollow(Long id, User loginUser) { @@ -73,7 +87,12 @@ public void deleteFollow(Long id, User loginUser) { followRepository.delete(findFollow); } - // 팔로우 상태 확인 + /** + * 팔로우 상태를 확인합니다. (loginUser 가 followee 를 팔로우 했는지 확인) + * @param loginUser 현재 인증된 사용자 정보 + * @param email 팔로우 상태를 확인할 사용자 email + * @return 팔로우 상태의 정보를 담은 FollowBooleanResponseDto + */ public FollowBooleanResponseDto findFollow(User loginUser, String email) { User followee = userRepository.findByEmail(email) @@ -98,7 +117,11 @@ public FollowBooleanResponseDto findFollow(User loginUser, String email) { ); } - // 팔로워 목록보기 + /** + * 특정 유저의 팔로워 목록를 조회합니다. + * @param email 팔로워 목록을 확인할 상대방 email + * @return 팔로워 목록을 담은 List + */ public List findFollowers(String email) { User followee = userRepository.findByEmail(email) @@ -121,7 +144,11 @@ public List findFollowers(String email) { .toList(); } - // 팔로잉 목록보기 + /** + * 특정 유저의 팔로잉 목록를 조회합니다. + * @param email 팔로잉 목록을 조회할 상대방 email + * @return 팔로잉 목록을 담은 List + */ public List findFollowing(String email) { User follower = userRepository.findByEmail(email) From 2403d9412865e4851e721ef25c6987abd11b6563 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 12:57:42 +0900 Subject: [PATCH 165/215] =?UTF-8?q?docs=20:=20MatchService=20=EC=9E=90?= =?UTF-8?q?=EB=B0=94=EB=8F=85=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/match/service/MatchService.java | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 9fcc04e..02ea533 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -27,6 +27,9 @@ import static com.example.gamemate.domain.match.enums.Priority.*; +/** + * 매칭 기능을 처리하는 서비스 클래스입니다. + */ @Service @RequiredArgsConstructor public class MatchService { @@ -36,7 +39,12 @@ public class MatchService { private final MatchUserInfoRepository matchUserInfoRepository; private final NotificationService notificationService; - // 매칭 요청 생성 + /** + * 사용자 간의 매칭 요청을 생성합니다. + * @param dto 매칭을 원하는 상대방 ID, 상대방에게 보낼 메세지를 포함합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 매칭 요청 처리 결과를 담은 MatchResponseDto + */ @Transactional public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { @@ -66,7 +74,12 @@ public MatchResponseDto createMatch(MatchCreateRequestDto dto, User loginUser) { return MatchResponseDto.toDto(match); } - // 매칭 수락/거절 + /** + * 받은 매칭 요청의 수락/거절을 처리합니다. + * @param id 수락/거절할 매칭 요청 ID + * @param dto status (ACCEPTED 수락 / REJECTED 거절) + * @param loginUser 현재 인증된 사용자 정보 + */ @Transactional public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { @@ -92,7 +105,11 @@ public void updateMatch(Long id, MatchUpdateRequestDto dto, User loginUser) { findMatch.updateStatus(dto.getStatus()); } - // 받은 매칭 전체 조회 + /** + * 사용자가 받은 매칭 요청을 조회합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 사용자의 받은 매칭 요청 목록을 담은 List + */ public List findAllReceivedMatch(User loginUser) { List matchList = matchRepository.findAllByReceiverId(loginUser.getId()); @@ -102,7 +119,11 @@ public List findAllReceivedMatch(User loginUser) { .toList(); } - // 보낸 매칭 전체 조회 + /** + * 사용자가 보낸 매칭 요청을 조회합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 사용자가 보낸 매칭 요청 목록을 담은 List + */ public List findAllSentMatch(User loginUser) { List matchList = matchRepository.findAllBySenderId(loginUser.getId()); @@ -112,7 +133,11 @@ public List findAllSentMatch(User loginUser) { .toList(); } - // 매치 삭제 (취소) + /** + * 사용자가 보낸 매칭 요청을 취소합니다. + * @param id 취소할 매칭 요청 ID + * @param loginUser 현재 인증된 사용자 정보 + */ @Transactional public void deleteMatch(Long id, User loginUser) { @@ -126,7 +151,12 @@ public void deleteMatch(Long id, User loginUser) { matchRepository.delete(findMatch); } - // 내 정보 입력, 정보 입력시 매칭 추천에서 검색됨 + /** + * 매칭을 위한 정보를 입력합니다. + * @param dto 매칭을 위해 자신의 정보를 입력합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 사용자의 정보가 처리된 MatchInfoResponseDto + */ @Transactional public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User loginUser) { @@ -147,7 +177,11 @@ public MatchInfoResponseDto createMyInfo(MatchInfoCreateRequestDto dto, User log return MatchInfoResponseDto.toDto(matchUserInfo); } - // 내 정보 조회 + /** + * 매칭을 위해 입력한 내 정보를 확인합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 내 정보를 담은 MatchInfoResponseDto + */ public MatchInfoResponseDto findMyInfo(User loginUser) { MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) .orElseThrow(() -> new ApiException(ErrorCode.MATCH_USER_INFO_NOT_FOUND)); @@ -155,7 +189,12 @@ public MatchInfoResponseDto findMyInfo(User loginUser) { return MatchInfoResponseDto.toDto(matchUserInfo); } - // 상대방 정보 조회 + /** + * 매칭 상대방의 입력한 정보를 확인합니다. + * @param id 확인할 매칭 요청 ID + * @param loginUser 현재 인증된 사용자 정보 + * @return 매칭 요청 ID의 상대방이 입력한 정보를 담은 MatchInfoResponseDto + */ public MatchInfoResponseDto findOpponentInfo(Long id, User loginUser) { Match findMatch = matchRepository.findById(id) .orElseThrow(() -> new ApiException(ErrorCode.MATCH_NOT_FOUND)); @@ -170,7 +209,11 @@ public MatchInfoResponseDto findOpponentInfo(Long id, User loginUser) { : getMatchInfoResponseDto(findMatch.getReceiver()); } - // 내 정보 수정 + /** + * 입력한 내 정보를 수정합니다. + * @param dto 수정할 정보를 입력합니다. + * @param loginUser 현재 인증된 사용자 정보 + */ @Transactional public void updateMyInfo(MatchInfoUpdateRequestDto dto, User loginUser) { MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) @@ -188,7 +231,10 @@ public void updateMyInfo(MatchInfoUpdateRequestDto dto, User loginUser) { ); } - // 내 정보 삭제, 내정보 삭제시 매칭 추천에서 더이상 검색되지 않음 + /** + * 내 정보 삭제, 내 정보 삭제시 더이상 매칭에서 검색되지 않습니다. + * @param loginUser 현재 인증된 사용자 정보 + */ @Transactional public void deleteMyInfo(User loginUser) { MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(loginUser) @@ -197,7 +243,12 @@ public void deleteMyInfo(User loginUser) { matchUserInfoRepository.delete(matchUserInfo); } - // 매칭 로직 + /** + * 사용자간의 연결을 위한 매칭 로직입니다. + * @param dto MatchSearchConditionDto 검색할 상대방의 조건을 입력합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 입력한 조건과 매칭로직을 통해 점수를 매겨 상위 5명의 정보를 보여줍니다. + */ public List findRecommendation(MatchSearchConditionDto dto, User loginUser) { // 1. 성별과 플레이 시간대를 기준으로 필터링된 사용자 정보 조회 List filteredUsers = matchUserInfoRepository.findByGenderAndPlayTimeRanges( @@ -227,7 +278,12 @@ public List findRecommendation(MatchSearchConditionDto dto } - // 점수 계산 로직 + /** + * 매칭의 점수 계산 로직입니다. + * @param condition 사용자가 입력한 원하는 상대의 조건입니다. + * @param userInfo 매칭에 추천될 사람들의 정보입니다. + * @return 점수계산 로직을 통해 나온 점수 + */ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo userInfo) { int score = 0; int normalScorePerMatch = 5; // 매칭되는 항목당 점수 @@ -320,7 +376,12 @@ private int calculateMatchScore(MatchSearchConditionDto condition, MatchUserInfo } - // 랭크가 비슷한지 판단하는 로직 추가 + /** + * 매칭 조건 중 랭크 조건에 비슷한 랭크인지 판단하는 메서드입니다. + * @param conditionRank 사용자가 입력한 원하는 랭크입니다. + * @param userRank 매칭로직에서 검사될 상대방들의 랭크입니다. + * @return 유사하다면 true, 아니면 false + */ private boolean isRankSimilar(GameRank conditionRank, GameRank userRank) { if (conditionRank == GameRank.DONT_MIND) { return true; // "상관없음"은 모든 랭크와 유사하다고 판단 @@ -332,7 +393,11 @@ private boolean isRankSimilar(GameRank conditionRank, GameRank userRank) { } - // 동점자는 랜덤으로 섞어서 출력 + /** + * 매칭 로직을 통해 나온 동점자들을 랜덤하게 섞어서 출력합니다. + * @param sortedUsers 매칭 점수 로직을 통해 나온 사용자들입니다. + * @return 동점자들을 랜덤하게 섞어서 출력된 정보입니다. + */ private List handleTies(List sortedUsers) { if (sortedUsers.isEmpty()) { return sortedUsers; @@ -362,7 +427,11 @@ private List handleTies(List sortedUsers) { return resultList; } - // 매칭 상대방 정보 찾아서 dto 로 변환 + /** + * 매칭의 상대방정보를 dto 로 변환합니다. + * @param user dto 로 변환할 상대방 사용자입니다. + * @return MatchInfoResponseDto + */ private MatchInfoResponseDto getMatchInfoResponseDto(User user) { MatchUserInfo matchUserInfo = matchUserInfoRepository.findByUser(user) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); From fa73fd8809368165331d5ad8b38e0fa73008ac97 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 13:18:29 +0900 Subject: [PATCH 166/215] =?UTF-8?q?refactor=20:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=EC=8B=9C=20=EC=B4=88=EA=B8=B0=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EB=82=B4=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 성별과 플레이시간대로만 필터링하던것에서 최근 로그인일자가 7일이내, 유저상태가 ACTIVE 인 유저만 검색되도록 변경 --- .../repository/MatchUserInfoRepository.java | 20 ++++++++++++++----- .../domain/match/service/MatchService.java | 9 +++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java index 9953d42..13aceff 100644 --- a/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java +++ b/src/main/java/com/example/gamemate/domain/match/repository/MatchUserInfoRepository.java @@ -4,11 +4,13 @@ import com.example.gamemate.domain.match.enums.Gender; import com.example.gamemate.domain.match.enums.PlayTimeRange; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.enums.UserStatus; 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.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.Set; @@ -18,9 +20,17 @@ public interface MatchUserInfoRepository extends JpaRepository findByUser(User user); Boolean existsByUser(User user); - @Query("SELECT m FROM MatchUserInfo m WHERE m.gender = :gender AND EXISTS (SELECT pt FROM m.playTimeRanges pt WHERE pt IN :playTimeRanges) AND m.user.id <> :userId") - List findByGenderAndPlayTimeRanges(@Param("gender") Gender gender, - @Param("playTimeRanges") Set playTimeRanges, - @Param("userId") Long userId); - + @Query("SELECT m FROM MatchUserInfo m " + + "WHERE m.gender = :gender " + + "AND EXISTS (SELECT pt FROM m.playTimeRanges pt WHERE pt IN :playTimeRanges) " + + "AND m.user.id <> :userId " + + "AND m.user.modifiedAt >= :sevenDaysAgo " + + "AND m.user.userStatus = :userStatus") + List findByGenderAndPlayTimeRanges( + @Param("gender") Gender gender, + @Param("playTimeRanges") Set playTimeRanges, + @Param("userId") Long userId, + @Param("sevenDaysAgo") LocalDateTime sevenDaysAgo, + @Param("userStatus") UserStatus userStatus + ); } diff --git a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java index 02ea533..365c89b 100644 --- a/src/main/java/com/example/gamemate/domain/match/service/MatchService.java +++ b/src/main/java/com/example/gamemate/domain/match/service/MatchService.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -250,11 +251,15 @@ public void deleteMyInfo(User loginUser) { * @return 입력한 조건과 매칭로직을 통해 점수를 매겨 상위 5명의 정보를 보여줍니다. */ public List findRecommendation(MatchSearchConditionDto dto, User loginUser) { - // 1. 성별과 플레이 시간대를 기준으로 필터링된 사용자 정보 조회 + // 1. 성별과 플레이 시간대 및 최근 로그인날짜가 7일이내, 유저상태가 ACTIVE 인 필터링된 사용자 정보 조회 + LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); + List filteredUsers = matchUserInfoRepository.findByGenderAndPlayTimeRanges( dto.getGender(), dto.getPlayTimeRanges(), - loginUser.getId() + loginUser.getId(), + sevenDaysAgo, + UserStatus.ACTIVE ); // 2. 매칭 점수 계산 및 저장 From 31c5a3a6d29b1c190c021ef087e6753190c3c578 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 14:58:50 +0900 Subject: [PATCH 167/215] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=9D=BD=EC=9D=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 31 ++++++++++++++++--- .../service/NotificationService.java | 15 +++++++++ .../gamemate/global/constant/ErrorCode.java | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index dfa7224..100181c 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -8,20 +8,26 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; +/** + * 알림을 처리하는 컨트롤러 클래스입니다. + */ @RestController @RequestMapping("/notifications") @RequiredArgsConstructor public class NotificationController { private final NotificationService notificationService; + /** + * 로그인한 사용자를 SSE 에 연결합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 로그인한 사용자의 SseEmitter 를 담은 ResponseEntity + */ @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity connect( @AuthenticationPrincipal CustomUserDetails customUserDetails @@ -45,4 +51,21 @@ public ResponseEntity> findAllNotification( List notificationResponseDtoList = notificationService.findAllNotification(customUserDetails.getUser()); return new ResponseEntity<>(notificationResponseDtoList, HttpStatus.OK); } + + /** + * 단일 알림의 읽음 상태를 처리합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @param id 읽음 상태를 처리할 알림 id + * @return 204 NO_CONTENT + */ + @PatchMapping("/{id}") + public ResponseEntity readNotification( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @PathVariable Long id + ) { + + notificationService.readNotification(customUserDetails.getUser(), id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index a7e7044..85db40b 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -6,6 +6,8 @@ import com.example.gamemate.domain.notification.repository.EmitterRepository; import com.example.gamemate.domain.notification.repository.NotificationRepository; import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -16,6 +18,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -91,4 +94,16 @@ public void sendNotification(User user, Notification notification) { } } } + + @Transactional + public void readNotification(User loginUser, Long id) { + Notification notification = notificationRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.NOTIFICATION_NOT_FOUND)); + + if (!Objects.equals(notification.getReceiver().getId(), loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } // 알림의 받는 사람과 로그인 한 유저가 다르면 예외 처리 + + notification.updateIsRead(true); + } } diff --git a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java index a9b1a48..ae1a557 100644 --- a/src/main/java/com/example/gamemate/global/constant/ErrorCode.java +++ b/src/main/java/com/example/gamemate/global/constant/ErrorCode.java @@ -50,6 +50,7 @@ public enum ErrorCode { RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND,"RECOMMENDATION_NOT_FOUND","추천 게임을 찾을 수 없습니다."), MATCH_USER_INFO_NOT_FOUND(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_FOUND", "매칭을 위해 입력된 회원 정보를 찾을 수 없습니다."), MATCH_USER_INFO_NOT_WRITTEN(HttpStatus.NOT_FOUND, "MATCH_USER_INFO_NOT_WRITTEN", "매칭을 위해 회원 정보 입력은 필수입니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTIFICATION_NOT_FOUND", "알림을 찾을수 없습니다."), /* 500 서버 오류 */ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"INTERNAL_SERVER_ERROR","서버 오류 입니다."), From 1e5c6d1aac01aada5a4120a844cb6975f3fc0adb Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 15:09:54 +0900 Subject: [PATCH 168/215] =?UTF-8?q?feat=20:=20=EC=9D=BD=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EB=AA=A8=EB=93=A0=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 15 +++++++++++++++ .../repository/NotificationRepository.java | 3 +-- .../notification/service/NotificationService.java | 9 +++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java index 100181c..721367d 100644 --- a/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/example/gamemate/domain/notification/controller/NotificationController.java @@ -68,4 +68,19 @@ public ResponseEntity readNotification( notificationService.readNotification(customUserDetails.getUser(), id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + /** + * 사용자의 모든 읽지않은 알림을 읽음 처리합니다. + * + * @param customUserDetails 현재 인증된 사용자 정보 + * @return 204 NO_CONTENT + */ + @PatchMapping + public ResponseEntity readAllNotification( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + + notificationService.readAllNotification(customUserDetails.getUser()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } } diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index 84de06d..397a5b6 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -10,6 +10,5 @@ public interface NotificationRepository extends JpaRepository { List findAllByReceiverId(Long receiverId); - List findAllByIsRead(boolean isRead); - + List findAllByReceiverIdAndIsRead(Long receiverId, boolean isRead); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 85db40b..97054c1 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -106,4 +106,13 @@ public void readNotification(User loginUser, Long id) { notification.updateIsRead(true); } + + @Transactional + public void readAllNotification(User loginUser) { + List unreadNotificationList = notificationRepository.findAllByReceiverIdAndIsRead(loginUser.getId(), false); + + for (Notification notification : unreadNotificationList) { + notification.updateIsRead(true); + } + } } From 30ab12412efe5e3be17d9c497e9c4118fbba5a23 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 15:15:49 +0900 Subject: [PATCH 169/215] =?UTF-8?q?docs=20:=20NotificationService=20?= =?UTF-8?q?=EC=9E=90=EB=B0=94=EB=8F=85=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationService.java | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 97054c1..3ab7a17 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -20,6 +20,9 @@ import java.util.Map; import java.util.Objects; +/** + * 알림을 처리하는 서비스 클래스입니다. + */ @Service @RequiredArgsConstructor @Slf4j @@ -30,17 +33,32 @@ public class NotificationService { private final RedisStreamService redisStreamService; private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + /** + * 레디스 스트림의 스트림그룹을 생성합니다. + */ @PostConstruct public void init() { redisStreamService.createStreamGroup(); } + /** + * 알림을 생성합니다. + * @param user 알림을 받는 사용자 + * @param type 알림 타입 + * @param relatedUrl 알림과 관련된 URL + * @return Notification 생성된 알림 + */ @Transactional public Notification createNotification(User user, NotificationType type, String relatedUrl) { Notification notification = new Notification(type.getContent(), relatedUrl, type, user); return notificationRepository.save(notification); } + /** + * 사용자의 모든 알림을 조회합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 로그인 한 사용자의 모든 알림이 담긴 List + */ public List findAllNotification(User loginUser) { return notificationRepository.findAllByReceiverId(loginUser.getId()) .stream() @@ -48,6 +66,41 @@ public List findAllNotification(User loginUser) { .toList(); } + /** + * 단일 알림을 읽음 처리합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @param id 읽음 처리할 알림 id + */ + @Transactional + public void readNotification(User loginUser, Long id) { + Notification notification = notificationRepository.findById(id) + .orElseThrow(() -> new ApiException(ErrorCode.NOTIFICATION_NOT_FOUND)); + + if (!Objects.equals(notification.getReceiver().getId(), loginUser.getId())) { + throw new ApiException(ErrorCode.FORBIDDEN); + } // 알림의 받는 사람과 로그인 한 유저가 다르면 예외 처리 + + notification.updateIsRead(true); + } + + /** + * 로그인한 사용자의 읽지 않은 모든 알림을 읽음 처리합니다. + * @param loginUser 현재 인증된 사용자 정보 + */ + @Transactional + public void readAllNotification(User loginUser) { + List unreadNotificationList = notificationRepository.findAllByReceiverIdAndIsRead(loginUser.getId(), false); + + for (Notification notification : unreadNotificationList) { + notification.updateIsRead(true); + } + } + + /** + * SSE 연결을 구독합니다. + * @param loginUser 현재 인증된 사용자 정보 + * @return 사용자 연결정보가 담긴 SseEmitter + */ public SseEmitter subscribe(User loginUser) { SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); @@ -75,6 +128,11 @@ public SseEmitter subscribe(User loginUser) { return emitterRepository.save(loginUser.getId(), emitter); } + /** + * 사용자에게 알림을 전송합니다. + * @param user 알림을 받을 사용자 + * @param notification 보내질 알림 + */ public void sendNotification(User user, Notification notification) { NotificationResponseDto notificationDto = NotificationResponseDto.toDto(notification); @@ -94,25 +152,4 @@ public void sendNotification(User user, Notification notification) { } } } - - @Transactional - public void readNotification(User loginUser, Long id) { - Notification notification = notificationRepository.findById(id) - .orElseThrow(() -> new ApiException(ErrorCode.NOTIFICATION_NOT_FOUND)); - - if (!Objects.equals(notification.getReceiver().getId(), loginUser.getId())) { - throw new ApiException(ErrorCode.FORBIDDEN); - } // 알림의 받는 사람과 로그인 한 유저가 다르면 예외 처리 - - notification.updateIsRead(true); - } - - @Transactional - public void readAllNotification(User loginUser) { - List unreadNotificationList = notificationRepository.findAllByReceiverIdAndIsRead(loginUser.getId(), false); - - for (Notification notification : unreadNotificationList) { - notification.updateIsRead(true); - } - } } From f30e39059f706035651656666acde4c908cbf64a Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Mon, 3 Feb 2025 19:59:50 +0900 Subject: [PATCH 170/215] =?UTF-8?q?refactor=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. sendNotification 에서 바로 sse 로 알림을 전송하고 redis stream 에는 데이터를 저장하기만 하던 방식에서 레디스에서 알림메세지를 소비(전송)하도록 리팩토링 2. Sse subscribe 시 읽지 않은 가장 최근의 알림 1개를 조회해 보여주도록 변경 --- .../repository/NotificationRepository.java | 2 + .../service/NotificationService.java | 79 +++++--- .../service/RedisStreamService.java | 185 ++++++++++++++---- .../eventListener/GlobalEventListener.java | 36 +--- 4 files changed, 202 insertions(+), 100 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index 397a5b6..cb4428d 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -5,10 +5,12 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface NotificationRepository extends JpaRepository { List findAllByReceiverId(Long receiverId); List findAllByReceiverIdAndIsRead(Long receiverId, boolean isRead); + Optional findTopByReceiverIdAndIsReadOrderByCreatedAtDesc(Long receiverId, boolean isRead); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 3ab7a17..e413883 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -12,13 +12,14 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.StreamInfo; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; /** * 알림을 처리하는 서비스 클래스입니다. @@ -31,6 +32,9 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final EmitterRepository emitterRepository; private final RedisStreamService redisStreamService; + private final RedisTemplate redisTemplate; + private static final String STREAM_KEY = "notification_stream"; + private static final String GROUP_NAME = "notification-group"; private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; /** @@ -38,7 +42,27 @@ public class NotificationService { */ @PostConstruct public void init() { - redisStreamService.createStreamGroup(); + try { + // 스트림이 존재하지 않으면 생성 + if (!Boolean.TRUE.equals(redisTemplate.hasKey(STREAM_KEY))) { + redisTemplate.opsForStream() + .add(StreamRecords.newRecord() + .in(STREAM_KEY) + .ofMap(Collections.singletonMap("init", "init"))); + } + + // 그룹 정보 조회 + StreamInfo.XInfoGroups groups = redisTemplate.opsForStream().groups(STREAM_KEY); + boolean groupExists = groups.stream() + .anyMatch(group -> GROUP_NAME.equals(group.groupName())); + + // 그룹이 없으면 생성 + if (!groupExists) { + redisTemplate.opsForStream().createGroup(STREAM_KEY, GROUP_NAME); + } + } catch (Exception e) { + log.error("스트림 초기화 중 오류 발생: {}", e.getMessage()); + } } /** @@ -97,7 +121,7 @@ public void readAllNotification(User loginUser) { } /** - * SSE 연결을 구독합니다. + * SSE 연결을 구독합니다. 또한 가장 최근 읽지 않은 알림 1개를 전송합니다. * @param loginUser 현재 인증된 사용자 정보 * @return 사용자 연결정보가 담긴 SseEmitter */ @@ -110,46 +134,37 @@ public SseEmitter subscribe(User loginUser) { .name("connect") .data("connected!")); - // 미처리된 알림 조회 및 전송 - List> unreadNotifications = - redisStreamService.getUnreadNotifications(loginUser.getId()); - - for (Map notification : unreadNotifications) { - emitter.send(SseEmitter.event() - .name(notification.get("type")) - .data(notification)); - } + // DB에서 가장 최근 읽지 않은 알림 1개만 조회 + notificationRepository.findTopByReceiverIdAndIsReadOrderByCreatedAtDesc(loginUser.getId(), false) + .ifPresent(notification -> { + try { + NotificationResponseDto notificationDto = NotificationResponseDto.toDto(notification); + emitter.send(SseEmitter.event() + .name(notification.getType().name()) + .data(notificationDto)); + log.debug("최근 알림 전송 - ID: {}", notification.getId()); + } catch (IOException e) { + log.error("알림 전송 실패 - ID: {} - 에러: {}", notification.getId(), e.getMessage()); + } + }); } catch (IOException e) { - throw new RuntimeException("연결 실패!"); + log.error("SSE 연결 실패 - 유저: {} - 에러: {}", loginUser.getId(), e.getMessage()); + throw new RuntimeException("SSE 연결 실패", e); } - // emitter를 저장소에 저장 (저장 시 이벤트 핸들러도 자동 등록) return emitterRepository.save(loginUser.getId(), emitter); } /** * 사용자에게 알림을 전송합니다. - * @param user 알림을 받을 사용자 * @param notification 보내질 알림 */ - public void sendNotification(User user, Notification notification) { + @Transactional + public void sendNotification(Notification notification) { NotificationResponseDto notificationDto = NotificationResponseDto.toDto(notification); - // Redis 스트림에 저장 + // Redis Stream 에 알림 추가 redisStreamService.addNotificationToStream(notificationDto); - - // SSE로 전송 - SseEmitter emitter = emitterRepository.findById(user.getId()); - if (emitter != null) { - try { - emitter.send(SseEmitter.event() - .name(notification.getType().name()) - .data(notificationDto)); - } catch (IOException e) { - emitterRepository.deleteById(user.getId()); - log.error("알림 전송 실패: {}", e.getMessage()); - } - } } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java index 4d8b2c1..f272e9d 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java @@ -1,28 +1,70 @@ package com.example.gamemate.domain.notification.service; import com.example.gamemate.domain.notification.dto.NotificationResponseDto; +import com.example.gamemate.domain.notification.repository.EmitterRepository; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.stream.MapRecord; -import org.springframework.data.redis.connection.stream.StreamOffset; -import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.connection.stream.*; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.util.stream.Collectors; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.io.IOException; +import java.time.Duration; +import java.util.*; @Service @RequiredArgsConstructor @Slf4j public class RedisStreamService { private final RedisTemplate redisTemplate; + private final EmitterRepository emitterRepository; + private static final String STREAM_KEY = "notification_stream"; + private static final String GROUP_NAME = "notification-group"; + private static final String CONSUMER_PREFIX = "consumer"; + private static final int BATCH_SIZE = 100; + private static final Duration POLL_TIMEOUT = Duration.ofMillis(100); private static final int MAX_STREAM_LENGTH = 1000; + @PostConstruct + public void init() { + createStreamGroup(); + initializeStreamTrimming(); + } + + public void createStreamGroup() { + try { + // 스트림이 없으면 생성 + if (!Boolean.TRUE.equals(redisTemplate.hasKey(STREAM_KEY))) { + redisTemplate.opsForStream().createGroup(STREAM_KEY, ReadOffset.from("0-0"), GROUP_NAME); + log.info("스트림과 그룹 생성 완료: {}", GROUP_NAME); + } + // 스트림은 있지만 그룹이 없는 경우 + else { + try { + redisTemplate.opsForStream().createGroup(STREAM_KEY, GROUP_NAME); + log.info("기존 스트림에 그룹 생성 완료: {}", GROUP_NAME); + } catch (Exception e) { + log.info("그룹이 이미 존재합니다: {}", e.getMessage()); + } + } + } catch (Exception e) { + log.error("스트림 그룹 생성 중 오류 발생: {}", e.getMessage()); + } + } + + private void initializeStreamTrimming() { + // 스트림 크기 제한 설정 + try { + redisTemplate.opsForStream().trim(STREAM_KEY, MAX_STREAM_LENGTH); + } catch (Exception e) { + log.error("스트림 크기 초기화 중 오류 발생: {}", e.getMessage()); + } + } + public void addNotificationToStream(NotificationResponseDto notification) { try { Map notificationMap = new HashMap<>(); @@ -31,61 +73,120 @@ public void addNotificationToStream(NotificationResponseDto notification) { notificationMap.put("type", notification.getType().name()); notificationMap.put("relatedUrl", notification.getRelatedUrl()); notificationMap.put("receiverId", notification.getReceiverId().toString()); + notificationMap.put("timestamp", String.valueOf(System.currentTimeMillis())); - // 알림 추가 - redisTemplate.opsForStream().add( - StreamRecords.newRecord() + RecordId recordId = redisTemplate.opsForStream() + .add(StreamRecords.newRecord() .in(STREAM_KEY) - .ofMap(notificationMap) - ); + .ofMap(notificationMap)); - // 크기 관리 - manageStreamSize(); + log.info("알림이 스트림에 추가됨: {}", recordId); + // 스트림 크기 관리 + manageStream(); } catch (Exception e) { - log.error("스트림 저장 실패: {}", e.getMessage()); + log.error("알림 스트림 저장 실패: {}", e.getMessage()); + throw new RuntimeException("알림 저장 실패", e); } } - public void createStreamGroup() { + private void manageStream() { try { - redisTemplate.opsForStream().createGroup(STREAM_KEY, "notification-group"); + // 스트림 길이 제한 + long length = redisTemplate.opsForStream().size(STREAM_KEY); + if (length > MAX_STREAM_LENGTH) { + redisTemplate.opsForStream().trim(STREAM_KEY, MAX_STREAM_LENGTH); + log.info("스트림 크기 조정 완료. 현재 크기: {}", length); + } } catch (Exception e) { - log.info("스트림 그룹이 이미 존재합니다: {}", e.getMessage()); + log.error("스트림 관리 중 오류 발생: {}", e.getMessage()); } } - private void manageStreamSize() { + @Scheduled(fixedRate = 1000) + public void processUnconsumedNotifications() { + String consumerName = CONSUMER_PREFIX + "-" + UUID.randomUUID().toString(); + try { - long length = redisTemplate.opsForStream().info(STREAM_KEY).streamLength(); - if (length > MAX_STREAM_LENGTH) { - redisTemplate.opsForStream().trim(STREAM_KEY, MAX_STREAM_LENGTH, false); + // 처리되지 않은 메시지 읽기 + List> records = + redisTemplate.opsForStream().read( + Consumer.from(GROUP_NAME, consumerName), + StreamReadOptions.empty().count(BATCH_SIZE).block(POLL_TIMEOUT), + StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed())); + + for (MapRecord record : records) { + try { + processNotification(record); + // 성공적으로 처리된 메시지 승인 + redisTemplate.opsForStream() + .acknowledge(GROUP_NAME, record); + } catch (Exception e) { + log.error("알림 처리 실패: {}", e.getMessage()); + // 실패한 메시지 재처리 큐에 추가 + handleFailedNotification(record); + } } } catch (Exception e) { - log.error("스트림 크기 관리 중 오류 발생: {}", e.getMessage()); + log.error("알림 처리 중 오류 발생: {}", e.getMessage()); + } + } + + private void processNotification(MapRecord record) throws IOException { + Map value = record.getValue(); + Long receiverId = Long.parseLong(value.get("receiverId").toString()); + + // 연결된 SSE Emitter 찾기 + SseEmitter emitter = emitterRepository.findById(receiverId); + if (emitter != null) { + try { + // SSE로 알림 전송 + emitter.send(SseEmitter.event() + .name(value.get("type").toString()) + .data(value)); + } catch (Exception e) { + log.error("SSE 알림 전송 실패: {}", e.getMessage()); + emitterRepository.deleteById(receiverId); + throw e; + } } } - @SuppressWarnings("unchecked") - public List> getUnreadNotifications(Long userId) { + private void handleFailedNotification(MapRecord record) { + // 실패한 메시지를 재처리 큐에 추가하는 로직 try { - List> records = redisTemplate.opsForStream() - .read(StreamOffset.fromStart(STREAM_KEY)); - - return records.stream() - .map(record -> { - Map originalMap = record.getValue(); - Map convertedMap = new HashMap<>(); - originalMap.forEach((key, value) -> - convertedMap.put(key.toString(), value.toString()) - ); - return convertedMap; - }) - .filter(map -> map.get("receiverId").equals(userId.toString())) - .collect(Collectors.toList()); + String failedStreamKey = STREAM_KEY + "-failed"; + redisTemplate.opsForStream() + .add(StreamRecords.newRecord() + .in(failedStreamKey) + .ofMap(new HashMap<>(record.getValue()))); + } catch (Exception e) { + log.error("실패한 알림 처리 중 오류: {}", e.getMessage()); + } + } + + @Scheduled(fixedRate = 5000) + public void retryFailedNotifications() { + String failedStreamKey = STREAM_KEY + "-failed"; + + try { + List> failedRecords = + redisTemplate.opsForStream() + .read(StreamOffset.fromStart(failedStreamKey)); + + for (MapRecord record : failedRecords) { + try { + // 실패한 메시지 재처리 + processNotification(record); + // 성공적으로 처리된 메시지 제거 + redisTemplate.opsForStream() + .delete(failedStreamKey, record.getId()); + } catch (Exception e) { + log.error("실패한 알림 재처리 실패: {}", e.getMessage()); + } + } } catch (Exception e) { - log.error("미처리 알림 조회 실패: {}", e.getMessage()); - return Collections.emptyList(); + log.error("실패한 알림 재처리 중 오류 발생: {}", e.getMessage()); } } } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java index fbdb72f..9d8171e 100644 --- a/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java +++ b/src/main/java/com/example/gamemate/global/eventListener/GlobalEventListener.java @@ -32,9 +32,7 @@ public void handleCreateFollow(FollowCreatedEvent event) { Follow follow = event.getFollow(); Notification notification = notificationService.createNotification(follow.getFollowee(), NotificationType.NEW_FOLLOWER, "/users/" + follow.getFollower().getId()); - notificationService.sendNotification(follow.getFollowee(), notification); - - log.info("새로운 팔로우 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -44,9 +42,7 @@ public void handleCreateMatch(MatchCreatedEvent event) { Match match = event.getMatch(); Notification notification = notificationService.createNotification(match.getReceiver(), NotificationType.NEW_MATCH, "/matches/" + match.getId()); - notificationService.sendNotification(match.getReceiver(), notification); - - log.info("새로운 매칭 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -56,9 +52,7 @@ public void handleAcceptMatch(MatchAcceptedEvent event) { Match match = event.getMatch(); Notification notification = notificationService.createNotification(match.getSender(), NotificationType.MATCH_ACCEPTED, "/matches/" + match.getId()); - notificationService.sendNotification(match.getSender(), notification); - - log.info("매칭 수락 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -68,9 +62,7 @@ public void handleRejectMatch(MatchRejectedEvent event) { Match match = event.getMatch(); Notification notification = notificationService.createNotification(match.getSender(), NotificationType.MATCH_REJECTED, "/matches/" + match.getId()); - notificationService.sendNotification(match.getSender(), notification); - - log.info("매칭 거절 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -80,9 +72,7 @@ public void handleCreateBoardLike(BoardLikeCreatedEvent event) { BoardLike boardLike = event.getBoardLike(); Notification notification = notificationService.createNotification(boardLike.getBoard().getUser(), NotificationType.NEW_LIKE, "/boards/" + boardLike.getBoard().getId()); - notificationService.sendNotification(boardLike.getBoard().getUser(), notification); - - log.info("게시글 새로운 좋아요 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -92,9 +82,7 @@ public void handleCreateReviewLike(ReviewLikeCreatedEvent event) { ReviewLike reviewLike = event.getReviewLike(); Notification notification = notificationService.createNotification(reviewLike.getReview().getUser(), NotificationType.NEW_LIKE, "/reviews/" + reviewLike.getReview().getId()); - notificationService.sendNotification(reviewLike.getReview().getUser(), notification); - - log.info("리뷰 새로운 좋아요 알림 전송 완료"); + notificationService.sendNotification(notification); } @Async @@ -105,10 +93,8 @@ public void handleCreateComment(CommentCreatedEvent event) { if (!Objects.equals(comment.getUser().getId(), comment.getBoard().getUser().getId())) { Notification notification = notificationService.createNotification(comment.getBoard().getUser(), NotificationType.NEW_COMMENT, "/comments/" + comment.getId()); - notificationService.sendNotification(comment.getBoard().getUser(), notification); + notificationService.sendNotification(notification); } - - log.info("새로운 댓글 알림 전송 완료"); } @Async @@ -119,19 +105,17 @@ public void handleCreateReply(ReplyCreatedEvent event) { if (!Objects.equals(reply.getUser().getId(), reply.getComment().getBoard().getUser().getId())) { Notification boardNotification = notificationService.createNotification(reply.getComment().getBoard().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); - notificationService.sendNotification(reply.getComment().getBoard().getUser(), boardNotification); + notificationService.sendNotification(boardNotification); } if (!Objects.equals(reply.getUser().getId(), reply.getComment().getUser().getId())) { Notification commentNotification = notificationService.createNotification(reply.getComment().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); - notificationService.sendNotification(reply.getComment().getUser(), commentNotification); + notificationService.sendNotification(commentNotification); } if (reply.getParentReply() != null && !Objects.equals(reply.getParentReply().getUser().getId(), reply.getUser().getId())) { Notification parentReplyNotification = notificationService.createNotification(reply.getParentReply().getUser(), NotificationType.NEW_COMMENT, "/replies/" + reply.getId()); - notificationService.sendNotification(reply.getParentReply().getUser(), parentReplyNotification); + notificationService.sendNotification(parentReplyNotification); } - - log.info("새로운 대댓글 알림 전송 완료"); } } From 072842a4edbccdc8200c5fc50774579311db26ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 4 Feb 2025 13:14:50 +0900 Subject: [PATCH 171/215] =?UTF-8?q?fix:=20=EB=B6=84=EC=82=B0=20=EB=9D=BD?= =?UTF-8?q?=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C=EA=B8=89=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=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 - 락 키 쿠폰 단위로 관리하도록 변경 - tryLock으로 변경하여 락 획득 실패 시 예외 처리 --- .../domain/coupon/service/CouponService.java | 2 +- .../global/common/aop/DistributedLockAop.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 91e409b..e19a951 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -50,7 +50,7 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U return new CouponCreateResponseDto(savedCoupon); } - @DistributedLock(key = "'LOCK:' + #couponId + ':' + #loginUser.id") + @DistributedLock(key = "'LOCK:coupon:' + #couponId") public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { Coupon coupon = couponRepository.findById(couponId) .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); diff --git a/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java b/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java index bd05923..c7bfd4c 100644 --- a/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java +++ b/src/main/java/com/example/gamemate/global/common/aop/DistributedLockAop.java @@ -28,11 +28,17 @@ public Object executeWithLock( ProceedingJoinPoint joinPoint, DistributedLock distributedLock ) throws Throwable { - String lockKey = LOCK_PREFIX + joinPoint.getArgs()[0] + ":" + ((User) joinPoint.getArgs()[1]).getId(); + String lockKey = LOCK_PREFIX + "coupon:" + joinPoint.getArgs()[0]; RLock lock = redissonClient.getLock(lockKey); log.info("락 획득 시도: {}", lockKey); - lock.lock(distributedLock.leaseTime(), distributedLock.timeUnit()); + boolean isLocked = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit()); + + if (!isLocked) { + log.warn("락 획득 실패: {}", lockKey); + throw new ApiException(ErrorCode.COUPON_ISSUE_FAILED); + } + log.info("락 획득 완료: {}", lockKey); try { @@ -56,5 +62,5 @@ public void afterCommit() { } throw e; } - } + } } \ No newline at end of file From ac5b0856098c39baca1adfda8c876593ab1dec31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 4 Feb 2025 14:04:58 +0900 Subject: [PATCH 172/215] =?UTF-8?q?fix:=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=B4=20Redis=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠폰 수량을 레디스에서 관리 --- build.gradle | 3 ++ .../gamemate/domain/coupon/entity/Coupon.java | 17 +--------- .../domain/coupon/service/CouponService.java | 32 +++++++++++++++---- src/main/resources/application.properties | 3 ++ 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 2a52670..b433146 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,9 @@ dependencies { // mail implementation 'org.springframework.boot:spring-boot-starter-mail' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java index 4bd8f3a..7f2c59e 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java @@ -27,12 +27,6 @@ public class Coupon extends BaseEntity { @Column(nullable = false) private Integer discountAmount; - @Column(nullable = false) - private Integer quantity; - - @Column(nullable = false) - private Integer issuedQuantity = 0; - @Column(nullable = false) private LocalDateTime startAt; @@ -42,11 +36,10 @@ public class Coupon extends BaseEntity { @OneToMany(mappedBy = "coupon") private List userCoupons = new ArrayList<>(); - public Coupon(String code, String name, Integer discountAmount, Integer quantity, LocalDateTime startAt, LocalDateTime expiredAt) { + public Coupon(String code, String name, Integer discountAmount, LocalDateTime startAt, LocalDateTime expiredAt) { this.code = code; this.name = name; this.discountAmount = discountAmount; - this.quantity = quantity; this.startAt = startAt; this.expiredAt = expiredAt; } @@ -55,13 +48,5 @@ public boolean isIssuable() { LocalDateTime now = LocalDateTime.now(); return now.isAfter(startAt) && now.isBefore(expiredAt); } - - public boolean isExhausted() { - return issuedQuantity >= quantity; - } - - public void incrementIssuedQuantity() { - this.issuedQuantity++; - } } diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 467f0ea..c218c83 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -12,6 +12,7 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +26,10 @@ public class CouponService { private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; + private final StringRedisTemplate redisTemplate; + + private static final String COUPON_QUANTITY_KEY = "coupon:%d:quantity"; + private static final String COUPON_ISSUED_COUNT_KEY = "coupon:%d:issued"; public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, User loginUser) { // 관리자 권한 체크 @@ -41,9 +46,15 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U validateCouponDates(requestDto.getStartAt(), requestDto.getExpiredAt()); // 쿠폰 생성 - Coupon coupon = new Coupon(requestDto.getCode(), requestDto.getName(), requestDto.getDiscountAmount(), requestDto.getQuantity(), requestDto.getStartAt(), requestDto.getExpiredAt()); + Coupon coupon = new Coupon(requestDto.getCode(), requestDto.getName(), requestDto.getDiscountAmount(), requestDto.getStartAt(), requestDto.getExpiredAt()); Coupon savedCoupon = couponRepository.save(coupon); + // Redis에 쿠폰 수량 정보 저장 + String quantityKey = String.format(COUPON_QUANTITY_KEY, savedCoupon.getId()); + String issuedKey = String.format(COUPON_ISSUED_COUNT_KEY, savedCoupon.getId()); + redisTemplate.opsForValue().set(quantityKey, String.valueOf(requestDto.getQuantity())); + redisTemplate.opsForValue().set(issuedKey, "0"); + return new CouponCreateResponseDto(savedCoupon); } @@ -56,18 +67,25 @@ public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); } - // 수량 체크 - if (coupon.isExhausted()) { - throw new ApiException(ErrorCode.COUPON_EXHAUSTED); - } - // 중복 발급 체크 if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); } + // Redis에서 수량 확인 및 증가 + String quantityKey = String.format(COUPON_QUANTITY_KEY, couponId); + String issuedKey = String.format(COUPON_ISSUED_COUNT_KEY, couponId); + + Long issuedCount = redisTemplate.opsForValue().increment(issuedKey); + String quantityStr = redisTemplate.opsForValue().get(quantityKey); + int quantity = Integer.parseInt(quantityStr); + + if (issuedCount > quantity) { + redisTemplate.opsForValue().decrement(issuedKey); + throw new ApiException(ErrorCode.COUPON_EXHAUSTED); + } + // 쿠폰 발급 - coupon.incrementIssuedQuantity(); UserCoupon userCoupon = new UserCoupon(loginUser, coupon); userCoupon.updateIsUsed(false); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26ee1c8..13fbb20 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -87,3 +87,6 @@ spring.servlet.multipart.max-request-size=5MB spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always +# Redis +spring.data.redis.host=localhost +spring.data.redis.port=6379 \ No newline at end of file From a21bd292c7a1bb94dbfb37a6d6d46a553b294570 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 15:13:46 +0900 Subject: [PATCH 173/215] =?UTF-8?q?feat:=20=EC=A1=B0=ED=9A=8C=EC=88=98=20?= =?UTF-8?q?=EB=86=92=EC=9D=80=205=EA=B0=9C=EC=9D=98=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 조회수가 높은 5개 게시글 조회되도록 구현 2. 조회수는 자정에 DB와 동기화 되도록 설정 --- .../board/controller/BoardController.java | 24 ++ .../board/repository/BoardRepository.java | 12 + .../board/scheduler/ViewCountScheduler.java | 36 --- .../domain/board/service/BoardService.java | 208 +++++++++++++++--- .../global/redis/service/RedisService.java | 88 -------- 5 files changed, 218 insertions(+), 150 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java delete mode 100644 src/main/java/com/example/gamemate/global/redis/service/RedisService.java diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index 503c071..e807407 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -45,6 +45,30 @@ public ResponseEntity createBoard( return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } + + /** + * 게시글 조회하고 검색하는 API 입니다. + * + * @param category 카테고리 종류 + * @return 게시글 목록을 포함한 ResponseEntity + */ + @GetMapping("/rankings") + public ResponseEntity> findTopBoards( + @RequestParam(required = false) String category + ) { + + BoardCategory boardCategory = null; + if (category != null) { + boardCategory = BoardCategory.fromName(category); + } + + List dtos = boardService.findTopBoards(boardCategory); + if(dtos.isEmpty()){ + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(dtos, HttpStatus.OK); + } + /** * 게시글 조회하고 검색하는 API 입니다. * diff --git a/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java b/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java index e3b3f25..a66b356 100644 --- a/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/example/gamemate/domain/board/repository/BoardRepository.java @@ -14,4 +14,16 @@ @Repository public interface BoardRepository extends JpaRepository, BoardQuerydslRepository{ Page findByCategory(String category, Pageable pageable); + + List findTop5ByOrderByViewsDesc(); + + @Query("select b from Board b where b.id IN :boardIds order by b.views desc, b.createdAt desc") + List findByIdInOrderByCreatedAtDesc(@Param("boardIds") List boardIds); + + List findTop5ByOrderByCreatedAtDesc(); + + @Query("select b from Board b where b.id IN :boardIds order by b.views desc, b.createdAt desc") + List findByIdInOrderByViewsDescCreatedAtDesc(@Param("boardIds") List boardIds); + + List findByIdIn(List boardIds); } diff --git a/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java b/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java deleted file mode 100644 index 0737517..0000000 --- a/src/main/java/com/example/gamemate/domain/board/scheduler/ViewCountScheduler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.gamemate.domain.board.scheduler; - -import com.example.gamemate.domain.board.dto.BoardResponseDto; -import com.example.gamemate.domain.board.entity.Board; -import com.example.gamemate.domain.board.repository.BoardRepository; -import com.example.gamemate.global.redis.service.RedisService; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.concurrent.CountedCompleter; - -@Component -@RequiredArgsConstructor -public class ViewCountScheduler { - - private final BoardRepository boardRepository; - private final RedisService redisService; - - // 1시간마다 Redis 조회수를 DB에 반영 - //@Scheduled(fixedRate = 60 * 60 * 1000) - @Scheduled(fixedRate = 180000) - public void syncViewCounts(){ - List boards = boardRepository.findAll(); - - for(Board board : boards){ - int viewCount = redisService.getViewCount(board.getId()); - if(viewCount > 0){ - board.updateViewCount(viewCount); - boardRepository.save(board); - redisService.transferViewCountToDB(board.getId(), viewCount); - } - } - } -} diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index a5ddd35..901eff2 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -8,37 +8,38 @@ import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.enums.ListSize; import com.example.gamemate.domain.board.repository.BoardRepository; -import com.example.gamemate.domain.comment.dto.CommentFindResponseDto; -import com.example.gamemate.domain.comment.entity.Comment; -import com.example.gamemate.domain.comment.repository.CommentRepository; -import com.example.gamemate.domain.reply.dto.ReplyFindResponseDto; -import com.example.gamemate.domain.reply.entity.Reply; -import com.example.gamemate.domain.reply.repository.ReplyRepository; -import com.example.gamemate.domain.reply.service.ReplyService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; -import com.example.gamemate.global.redis.service.RedisService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; - +@Slf4j @Service @RequiredArgsConstructor public class BoardService { private final BoardRepository boardRepository; - private final CommentRepository commentRepository; - private final ReplyRepository replyRepository; - private final RedisService redisService; + private final String VIEW_COUNT_KEY = "board:view:"; + private final String VIEW_RANKING_KEY = "board:ranking:"; + private final RedisTemplate redisTemplate; + private final HttpServletRequest request; /** * 게시글 생성 메서드입니다. @@ -63,6 +64,31 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { ); } + /** + * 조회수 높은 게시글 조회 하는 메서드입니다. + * + * @param boardCategory 카테고리 종류 + * @return 게시글 조회 List + */ + public List findTopBoards(BoardCategory boardCategory) { + List top5Boards = getTop5Boards(); + + List result = new ArrayList<>(); + + for(Board board : top5Boards) { + int redisViewCount = getViewCount(board.getId()); + result.add(new BoardFindAllResponseDto( + board.getId(), + board.getCategory(), + board.getTitle(), + board.getCreatedAt(), + redisViewCount + )); + } + + return result; + } + /** * 게시판 리스트 조회 메서드입니다. * @@ -78,14 +104,13 @@ public List findAllBoards(int page, BoardCategory categ Page boardPage = boardRepository.searchBoardQuerydsl(category, title, content, pageable); - return boardPage.stream() .map(board -> new BoardFindAllResponseDto( board.getId(), board.getCategory(), board.getTitle(), board.getCreatedAt(), - board.getViews() + getViewCount(board.getId()) )) .collect(Collectors.toList()); } @@ -100,9 +125,9 @@ public List findAllBoards(int page, BoardCategory categ public BoardFindOneResponseDto findBoardById(Long id, User loginUser) { // 조회수 증가(Redis 저장) if(loginUser == null) { - redisService.increaseViewCount(id, null); + increaseViewCount(id, null); }else{ - redisService.increaseViewCount(id, loginUser.getId()); + increaseViewCount(id, loginUser.getId()); } // 게시글 조회 @@ -112,16 +137,6 @@ public BoardFindOneResponseDto findBoardById(Long id, User loginUser) { return new BoardFindOneResponseDto(findBoard); } - /** - * 게시글 조회수 리턴하는 메서드입니다. - * - * @param boardId 게시글 식별자 - * @return - */ - public int getPostViewCount(Long boardId){ - return redisService.getViewCount(boardId); - } - /** * 게시글 업데이트 메서드입니다. @@ -164,4 +179,145 @@ public void deleteBoard(User loginUser, Long id) { boardRepository.delete(findBoard); } + + /** + * 조회수 증가시키는 메서드입니다. + * + * @param boardId 게시글 식별자 + */ + @Transactional + public void increaseViewCount(Long boardId, Long userId) { + String uniqueKey; + + if (userId != null) { + // 회원 : userId 기반으로 조회 제한 + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + userId; + } else { + // 비회원 + String ipAddress = getClientIp(); + //String hashedIp = hashIpAddress(ipAddress); + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + ipAddress; + } + + if (Boolean.FALSE.equals(redisTemplate.hasKey(uniqueKey))) { + redisTemplate.opsForValue().set(uniqueKey, "1", Duration.ofHours(1)); + redisTemplate.opsForValue().increment(VIEW_COUNT_KEY + boardId); + redisTemplate.opsForZSet().incrementScore(VIEW_RANKING_KEY, String.valueOf(boardId),1); + } + } + + /** + * 조회수 가져오는 메서드 입니다. + * + * @param boardId 게시글 식별자 + * @return 조회수 + */ + public int getViewCount(Long boardId){ + + String key = VIEW_COUNT_KEY + boardId; + String count = redisTemplate.opsForValue().get(key); + + if(count != null){ + return Integer.parseInt(count); + } + + // Redis에 값이 없으면 DB에서 조회 후 Redis 에 반영 + Board board = boardRepository.findById(boardId) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); + + int dbViewCount = board.getViews(); + + // Redis 에 저장(초기화) + redisTemplate.opsForValue().set(key, String.valueOf(dbViewCount)); + + return dbViewCount; + } + + /** + * 클라이언트 IP 가져오는 메서드입니다.(프록시) + * + * @return ip 주소 + */ + private String getClientIp(){ + + String ip = request.getHeader("x-forwarded-for"); + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + + /** + * 매일 00시 정각 시간마다 동기화 + */ + @Scheduled(cron = "0 0 0 * * *") + @Transactional + public void syncRedisToDb(){ + log.info("Redis 조회수 데이터 DB로 동기화"); + + // 조회수 기반으로 DB에 업데이트 + Set keys = redisTemplate.keys(VIEW_COUNT_KEY + "*") + .stream() + .filter(key -> key.split(":").length == 3) // board:view:{boardId} 형식만 남김 + .collect(Collectors.toSet()); + if(!keys.isEmpty()){ + List updatedBoards = new ArrayList<>(); + for(String key : keys){ + Long boardId = Long.parseLong(key.split(":")[2]); + int viewCount = getViewCount(boardId); + if(viewCount > 0){ + Board findBoard = boardRepository.findById(boardId) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); + int boardViewCount = findBoard.getViews(); + findBoard.updateViewCount( viewCount); + updatedBoards.add(findBoard); + + // redis 값을 유지(db 값 반영 후 덮어쓰기) + redisTemplate.opsForValue().set(VIEW_COUNT_KEY + boardId, String.valueOf(viewCount)); + } + } + // 업데이트 + boardRepository.saveAll(updatedBoards); + log.info("업데이트 완료"); + } + + } + + /** + * 조회수 높은 5개의 게시글 조회 + * + * @return 조회수 높은 게시글 리스트 + */ + public List getTop5Boards(){ + Set topBoardIds = redisTemplate.opsForZSet().reverseRange(VIEW_RANKING_KEY, 0, 4); + + if(topBoardIds == null || topBoardIds.isEmpty()){ + return boardRepository.findTop5ByOrderByCreatedAtDesc(); + } + + List boardIds = topBoardIds.stream().map(Long::parseLong).toList(); + + // DB 에서 해당 게시글 조회(조회수 순으로 정렬) + List boards = boardRepository.findByIdIn(boardIds); + + // 조회수는 redis 값으로 최신화 + boards.forEach(board -> { + int redisViewCount = getViewCount(board.getId()); + board.updateViewCount(redisViewCount); + }); + + boards.sort(Comparator.comparing(Board::getViews).reversed() + .thenComparing(Board::getCreatedAt, Comparator.reverseOrder())); + + return boards; + } } diff --git a/src/main/java/com/example/gamemate/global/redis/service/RedisService.java b/src/main/java/com/example/gamemate/global/redis/service/RedisService.java deleted file mode 100644 index d978b27..0000000 --- a/src/main/java/com/example/gamemate/global/redis/service/RedisService.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.gamemate.global.redis.service; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RedisService { - - //private final StringRedisTemplate redisTemplate; - private final String VIEW_COUNT_KEY = "board:view:"; - private final RedisTemplate redisTemplate; - private final HttpServletRequest request; - - /** - * 조회수 증가하는 메서드입니다. - * - * @param boardId 게시글 식별자 - */ - public void increaseViewCount(Long boardId, Long userId) { - String uniqueKey; - - if (userId != null) { - // 회원 : userId 기반으로 조회 제한 - uniqueKey = VIEW_COUNT_KEY + boardId + ":" + userId; - } else { - // 비회원 - String ipAddress = getClientIp(); - uniqueKey = VIEW_COUNT_KEY + boardId + ":" + ipAddress; - } - - if (Boolean.FALSE.equals(redisTemplate.hasKey(uniqueKey))) { - redisTemplate.opsForValue().set(uniqueKey, "1", Duration.ofHours(1)); - redisTemplate.opsForValue().increment(VIEW_COUNT_KEY + boardId); - } - } - - /** - * 조회수 가져오는 메서드 입니다. - * - * @param boardId 게시글 식별자 - * @return 조회수 - */ - public int getViewCount(Long boardId){ - String key = VIEW_COUNT_KEY + boardId; - String count = redisTemplate.opsForValue().get(key); - return count == null ? 0 : Integer.parseInt(count); - } - - /** - * 클라이언트 IP 가져오는 메서드입니다.(프록시) - * - * @return ip 주소 - */ - private String getClientIp(){ - String ip = request.getHeader("x-forwarded-for"); - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - return ip; - } - - /** - * 조회수를 DB에 반영 후 Redis 에서 삭제하는 메서드입니다. - * - * @param boardId 게시글 식별자 - * @param count 조회수 - */ - public void transferViewCountToDB(Long boardId, int count){ - redisTemplate.delete(VIEW_COUNT_KEY + boardId); - } -} From 28d919c62aec41df630f87abe6443609258703e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Tue, 4 Feb 2025 15:32:14 +0900 Subject: [PATCH 174/215] =?UTF-8?q?fix:=20=EC=BF=A0=ED=8F=B0=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=A0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EC=84=B1=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버 재시작 시 Redis-DB 쿠폰 재고 동기화 - 쿠폰 발급 트랜잭션 실패 시 Redis 재고 복원 --- .../gamemate/domain/coupon/entity/Coupon.java | 6 ++- .../repository/UserCouponRepository.java | 3 +- .../domain/coupon/service/CouponService.java | 49 ++++++++++--------- .../global/config/CouponDataSynchronizer.java | 37 ++++++++++++++ 4 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java index 7f2c59e..b364572 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java @@ -27,6 +27,9 @@ public class Coupon extends BaseEntity { @Column(nullable = false) private Integer discountAmount; + @Column(nullable = false) + private Integer totalQuantity; + @Column(nullable = false) private LocalDateTime startAt; @@ -36,10 +39,11 @@ public class Coupon extends BaseEntity { @OneToMany(mappedBy = "coupon") private List userCoupons = new ArrayList<>(); - public Coupon(String code, String name, Integer discountAmount, LocalDateTime startAt, LocalDateTime expiredAt) { + public Coupon(String code, String name, Integer discountAmount, Integer totalQuantity, LocalDateTime startAt, LocalDateTime expiredAt) { this.code = code; this.name = name; this.discountAmount = discountAmount; + this.totalQuantity = totalQuantity; this.startAt = startAt; this.expiredAt = expiredAt; } diff --git a/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java b/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java index 98d6dba..eac2441 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java +++ b/src/main/java/com/example/gamemate/domain/coupon/repository/UserCouponRepository.java @@ -4,10 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; -import java.util.Optional; public interface UserCouponRepository extends JpaRepository { boolean existsByUserIdAndCouponId(Long userId, Long couponId); List findByUserId(Long userId); -// Optional findByUserIdAndCouponId(Long userId, Long couponId); + long countByCouponId(Long couponId); } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index c218c83..8cb9c80 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -28,8 +28,11 @@ public class CouponService { private final UserCouponRepository userCouponRepository; private final StringRedisTemplate redisTemplate; - private static final String COUPON_QUANTITY_KEY = "coupon:%d:quantity"; - private static final String COUPON_ISSUED_COUNT_KEY = "coupon:%d:issued"; + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); + } public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, User loginUser) { // 관리자 권한 체크 @@ -46,14 +49,12 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U validateCouponDates(requestDto.getStartAt(), requestDto.getExpiredAt()); // 쿠폰 생성 - Coupon coupon = new Coupon(requestDto.getCode(), requestDto.getName(), requestDto.getDiscountAmount(), requestDto.getStartAt(), requestDto.getExpiredAt()); + Coupon coupon = new Coupon(requestDto.getCode(), requestDto.getName(), requestDto.getDiscountAmount(), requestDto.getQuantity(), requestDto.getStartAt(), requestDto.getExpiredAt()); Coupon savedCoupon = couponRepository.save(coupon); - // Redis에 쿠폰 수량 정보 저장 - String quantityKey = String.format(COUPON_QUANTITY_KEY, savedCoupon.getId()); - String issuedKey = String.format(COUPON_ISSUED_COUNT_KEY, savedCoupon.getId()); - redisTemplate.opsForValue().set(quantityKey, String.valueOf(requestDto.getQuantity())); - redisTemplate.opsForValue().set(issuedKey, "0"); + // Redis에 쿠폰 재고 수량 저장 + redisTemplate.opsForValue().set(getCouponStockKey(savedCoupon.getId()), + String.valueOf(requestDto.getQuantity())); return new CouponCreateResponseDto(savedCoupon); } @@ -72,26 +73,30 @@ public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); } - // Redis에서 수량 확인 및 증가 - String quantityKey = String.format(COUPON_QUANTITY_KEY, couponId); - String issuedKey = String.format(COUPON_ISSUED_COUNT_KEY, couponId); - - Long issuedCount = redisTemplate.opsForValue().increment(issuedKey); - String quantityStr = redisTemplate.opsForValue().get(quantityKey); - int quantity = Integer.parseInt(quantityStr); + // 재고 감소 + Long stock = redisTemplate.opsForValue().decrement(getCouponStockKey(couponId)); - if (issuedCount > quantity) { - redisTemplate.opsForValue().decrement(issuedKey); + // 재고 체크 + if (stock == null || stock < 0) { + // 재고 복원 + redisTemplate.opsForValue().increment(getCouponStockKey(couponId)); throw new ApiException(ErrorCode.COUPON_EXHAUSTED); } - // 쿠폰 발급 - UserCoupon userCoupon = new UserCoupon(loginUser, coupon); - userCoupon.updateIsUsed(false); + try { + // 쿠폰 발급 + UserCoupon userCoupon = new UserCoupon(loginUser, coupon); + userCoupon.updateIsUsed(false); - UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); + UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); - return new CouponIssueResponseDto(savedUserCoupon); + return new CouponIssueResponseDto(savedUserCoupon); + + } catch (Exception e) { + // DB 저장 실패 시 Redis 재고 복원 + redisTemplate.opsForValue().increment(getCouponStockKey(couponId)); + throw e; + } } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java new file mode 100644 index 0000000..2e670ec --- /dev/null +++ b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java @@ -0,0 +1,37 @@ +package com.example.gamemate.global.config; + +import com.example.gamemate.domain.coupon.entity.Coupon; +import com.example.gamemate.domain.coupon.repository.CouponRepository; +import com.example.gamemate.domain.coupon.repository.UserCouponRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CouponDataSynchronizer { + private final CouponRepository couponRepository; + private final UserCouponRepository userCouponRepository; + private final StringRedisTemplate redisTemplate; + + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); + } + + @EventListener(ApplicationReadyEvent.class) + public void syncCouponStock() { + List coupons = couponRepository.findAll(); + for (Coupon coupon : coupons) { + long issuedCount = userCouponRepository.countByCouponId(coupon.getId()); + long remainingStock = coupon.getTotalQuantity() - issuedCount; + redisTemplate.opsForValue().set(getCouponStockKey(coupon.getId()), + String.valueOf(remainingStock)); + } + } +} \ No newline at end of file From c4e62a0155db90030a3595887956843fb92fbd8c Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Tue, 4 Feb 2025 17:01:46 +0900 Subject: [PATCH 175/215] =?UTF-8?q?refactor=20:=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9B=8C=20/=20=ED=8C=94=EB=A1=9C=EC=9E=89=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 기존 코드 : fetchType 이 Lazy 로 getFollower 에 접근할때 N+1 문제 발생 2. 개선 코드 : JPQL 로 쿼리 하나로 모두 조회 --- .../follow/repository/FollowRepository.java | 17 ++++++ .../domain/follow/service/FollowService.java | 26 +-------- .../global/test/TestDataController.java | 58 +++++++++++++++++++ 3 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/example/gamemate/global/test/TestDataController.java diff --git a/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java b/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java index b91f7b0..c8d7441 100644 --- a/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java +++ b/src/main/java/com/example/gamemate/domain/follow/repository/FollowRepository.java @@ -1,8 +1,11 @@ package com.example.gamemate.domain.follow.repository; +import com.example.gamemate.domain.follow.dto.FollowFindResponseDto; import com.example.gamemate.domain.follow.entity.Follow; import com.example.gamemate.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 org.springframework.stereotype.Repository; import java.util.List; @@ -12,4 +15,18 @@ public interface FollowRepository extends JpaRepository { Boolean existsByFollowerAndFollowee(User follower, User followee); List findByFollowee(User followee); List findByFollower(User follower); + + @Query("SELECT NEW com.example.gamemate.domain.follow.dto.FollowFindResponseDto(f.follower.id, f.follower.nickname) " + + "FROM Follow f " + + "JOIN f.follower " + + "WHERE f.followee.email = :email " + + "AND f.follower.userStatus != 'WITHDRAW'") + List findFollowersByFolloweeEmail(@Param("email") String email); + + @Query("SELECT NEW com.example.gamemate.domain.follow.dto.FollowFindResponseDto(f.followee.id, f.followee.nickname) " + + "FROM Follow f " + + "JOIN f.followee " + + "WHERE f.follower.email = :email " + + "AND f.followee.userStatus != 'WITHDRAW'") + List findFollowingByFollowerEmail(@Param("email") String email); } diff --git a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java index 0218e5a..c53b5fb 100644 --- a/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/gamemate/domain/follow/service/FollowService.java @@ -123,7 +123,6 @@ public FollowBooleanResponseDto findFollow(User loginUser, String email) { * @return 팔로워 목록을 담은 List */ public List findFollowers(String email) { - User followee = userRepository.findByEmail(email) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -131,17 +130,7 @@ public List findFollowers(String email) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } // 확인할 상대방이 탈퇴한 회원일때 예외처리 - List followListByFollowee = followRepository.findByFollowee(followee); - - List followersByFollowee = followListByFollowee.stream() - .map(Follow::getFollower) - .filter(follower -> follower.getUserStatus() != UserStatus.WITHDRAW) - .toList(); - - return followersByFollowee - .stream() - .map(FollowFindResponseDto::toDto) - .toList(); + return followRepository.findFollowersByFolloweeEmail(email); } /** @@ -150,7 +139,6 @@ public List findFollowers(String email) { * @return 팔로잉 목록을 담은 List */ public List findFollowing(String email) { - User follower = userRepository.findByEmail(email) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -158,16 +146,6 @@ public List findFollowing(String email) { throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); } // 확인할 상대방이 탈퇴한 회원일때 예외처리 - List followListByFollower = followRepository.findByFollower(follower); - - List followingByFollower = followListByFollower.stream() - .map(Follow::getFollowee) - .filter(followee -> followee.getUserStatus() != UserStatus.WITHDRAW) - .toList(); - - return followingByFollower - .stream() - .map(FollowFindResponseDto::toDto) - .toList(); + return followRepository.findFollowingByFollowerEmail(email); } } diff --git a/src/main/java/com/example/gamemate/global/test/TestDataController.java b/src/main/java/com/example/gamemate/global/test/TestDataController.java new file mode 100644 index 0000000..178982c --- /dev/null +++ b/src/main/java/com/example/gamemate/global/test/TestDataController.java @@ -0,0 +1,58 @@ +package com.example.gamemate.global.test; + +import com.example.gamemate.domain.follow.entity.Follow; +import com.example.gamemate.domain.follow.repository.FollowRepository; +import com.example.gamemate.domain.user.entity.User; +import com.example.gamemate.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/test") +public class TestDataController { + private final UserRepository userRepository; + private final FollowRepository followRepository; + + @PostMapping("/init") + public ResponseEntity initializeTestData() { + // 타겟 유저 생성 + User targetUser = new User("target@test.com", "TargetUser", "TargetUser", "1234"); + userRepository.save(targetUser); + + // 1000명의 팔로워 생성 및 팔로우 관계 설정 + List followers = new ArrayList<>(); + List follows = new ArrayList<>(); + + for (int i = 0; i < 1000; i++) { + User follower = new User( + "test" + i + "@test.com", + "User" + i, + "User" + i, + "1234" + ); + followers.add(follower); + } + + // 벌크 저장으로 성능 향상 + List savedFollowers = userRepository.saveAll(followers); + + // 팔로우 관계 생성 + for (User follower : savedFollowers) { + follows.add(new Follow(follower, targetUser)); + } + followRepository.saveAll(follows); + + return ResponseEntity.ok(String.format( + "테스트 데이터 생성 완료 (타겟 유저: %s, 팔로워: %d명)", + targetUser.getEmail(), + follows.size() + )); + } +} \ No newline at end of file From 6c2020a481948d39fadc6e641eb0e220f0f1858e Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 17:20:41 +0900 Subject: [PATCH 176/215] =?UTF-8?q?build:=20deploy.sh=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?,=20github-actions.yml=20=ED=8C=8C=EC=9D=BC=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 1. deploy.sh 추가 2. github-actions.yml 파일 수정 --- .../workflows/docker-multi-stage-build.yml | 117 +++++++----------- deploy.sh | 21 ++++ 2 files changed, 64 insertions(+), 74 deletions(-) create mode 100644 deploy.sh diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 69e4b1b..d7876d2 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -1,89 +1,58 @@ name: docker multi-stage build on: - push: - branches: - - '**' + pull_request: + branches: [ "develop" ] jobs: - docker-build-and-push: + deploy: runs-on: ubuntu-latest steps: - - name: Login to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v2 + + - name: Build with Gradle + run: ./gradlew build -x test + + # AWS 인증 설정 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build and push - uses: docker/build-push-action@v6 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + # ECR 로그인 + - name: Login to ECR + uses: aws-actions/amazon-ecr-login@v1 + id: login-ecr + + # Docker 이미지 빌드 및 ECR에 Push + - name: Build and Push Docker image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: aws-demo + IMAGE_TAG: latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + # deploy.sh & docker-compose.yml을 EC2로 업로드 + - name: Upload deploy script and docker-compose.yml to EC2 + uses: appleboy/scp-action@master with: - file: ./Dockerfile - push: true - tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest + host: ${{ secrets.EC2_HOST }} + username: ec2-user + key: ${{ secrets.EC2_KEY }} + source: "./deploy.sh, ./docker-compose.yml" + target: "/home/ec2-user/" - deploy-to-ec2: - needs: docker-build-and-push - runs-on: ubuntu-latest - steps: + # EC2에서 deploy.sh 실행 (최신 Docker 이미지 가져와서 실행) - name: Deploy to EC2 - uses: appleboy/ssh-action@v1.2.0 + uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} + username: ec2-user key: ${{ secrets.EC2_KEY }} script: | - # 기존 컨테이너 정리 - echo "Stopping and removing existing containers..." - sudo docker-compose down || true - - # docker-compose.yml 파일 생성 - echo "Creating docker-compose.yml..." - cat < docker-compose.yml - services: - app: - image: "${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKER_IMAGE_TAG_NAME }}:latest" - platform: linux/amd64 - container_name: app - ports: - - "8080:8080" - environment: - MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" - MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" - MYSQL_URL: "${{ secrets.MYSQL_URL }}" - JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" - JWT_SECRET: "${{ secrets.JWT_SECRET }}" - YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" - YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" - YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" - OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" - OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" - OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" - OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" - EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" - EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" - GEMINI_URL: "${{ secrets.GEMINI_URL }}" - GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" - depends_on: - - db - - db: - image: mysql:8.0 - container_name: db - environment: - MYSQL_ROOT_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" - volumes: - - db-data:/var/lib/mysql - ports: - - "3306:3306" - - volumes: - db-data: - EOF - - # docker-compose 실행 - echo "Starting services with docker-compose..." - sudo docker-compose up -d + chmod +x /home/ec2-user/deploy.sh + /home/ec2-user/deploy.sh \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..3bb76ed --- /dev/null +++ b/deploy.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# ECR 설정 +AWS_ACCOUNT_ID="863518453426" +REGION="ap-northeast-2" +ECR_REPOSITORY="game_mate" +IMAGE_TAG="latest" +ECR_URI="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY" + +# AWS ECR 로그인 +aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_URI + +# 최신 이미지 Pull +docker pull $ECR_URI:$IMAGE_TAG + +# 기존 컨테이너 중지 및 삭제 +docker stop spring-app || true +docker rm spring-app || true + +# docker-compose 실행 (환경변수 적용) +docker-compose -f /home/ec2-user/docker-compose.yml up -d From 6c2cad6743964ddc34aefdd65d3d6e13b780c20c Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 17:26:06 +0900 Subject: [PATCH 177/215] =?UTF-8?q?build:=20github-actions=20yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index d7876d2..d7ece46 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -10,6 +10,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Grant execute permission to gradlew + run: chmod +x ./gradlew + - name: Build with Gradle run: ./gradlew build -x test From 14a818b3cedb4c7218c8c7466318e5bdbd732415 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 17:29:22 +0900 Subject: [PATCH 178/215] =?UTF-8?q?build:=20github-actions=20yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index d7ece46..5d07ec2 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -33,7 +33,7 @@ jobs: - name: Build and Push Docker image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: aws-demo + ECR_REPOSITORY: game_mate IMAGE_TAG: latest run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . From 21a1c8451b7f0f4413cc590e329941816e934f1b Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 18:42:20 +0900 Subject: [PATCH 179/215] =?UTF-8?q?build:=20username=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 10 +++++----- deploy.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 5d07ec2..75c83c5 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -44,18 +44,18 @@ jobs: uses: appleboy/scp-action@master with: host: ${{ secrets.EC2_HOST }} - username: ec2-user + username: ubuntu key: ${{ secrets.EC2_KEY }} source: "./deploy.sh, ./docker-compose.yml" - target: "/home/ec2-user/" + target: "/home/ubuntu/" # EC2에서 deploy.sh 실행 (최신 Docker 이미지 가져와서 실행) - name: Deploy to EC2 uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST }} - username: ec2-user + username: ubuntu key: ${{ secrets.EC2_KEY }} script: | - chmod +x /home/ec2-user/deploy.sh - /home/ec2-user/deploy.sh \ No newline at end of file + chmod +x /home/ubuntu/deploy.sh + /home/ubuntu/deploy.sh \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 3bb76ed..f5189e3 100644 --- a/deploy.sh +++ b/deploy.sh @@ -18,4 +18,4 @@ docker stop spring-app || true docker rm spring-app || true # docker-compose 실행 (환경변수 적용) -docker-compose -f /home/ec2-user/docker-compose.yml up -d +docker-compose -f /home/ubuntu/docker-compose.yml up -d From b4fec3667304b78ef6f029178551f2f20a4336f5 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 18:48:10 +0900 Subject: [PATCH 180/215] =?UTF-8?q?build:=20container=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index f5189e3..3c6cc73 100644 --- a/deploy.sh +++ b/deploy.sh @@ -14,8 +14,8 @@ aws ecr get-login-password --region $REGION | docker login --username AWS --pass docker pull $ECR_URI:$IMAGE_TAG # 기존 컨테이너 중지 및 삭제 -docker stop spring-app || true -docker rm spring-app || true +docker stop app || true +docker rm app || true # docker-compose 실행 (환경변수 적용) docker-compose -f /home/ubuntu/docker-compose.yml up -d From 88c25ba421ed2d6cf0c0c696b3d3de916e53fb3d Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 19:00:18 +0900 Subject: [PATCH 181/215] =?UTF-8?q?build:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 75c83c5..27adfc7 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -35,6 +35,22 @@ jobs: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: game_mate IMAGE_TAG: latest + MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" + MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" + MYSQL_URL: "${{ secrets.MYSQL_URL }}" + JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" + JWT_SECRET: "${{ secrets.JWT_SECRET }}" + YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" + YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" + YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" + OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" + OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" + OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" + OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" + EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" + EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" + GEMINI_URL: "${{ secrets.GEMINI_URL }}" + GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG From 6c784fbfc726174408beb93d715371b8ed8644ec Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 19:21:48 +0900 Subject: [PATCH 182/215] =?UTF-8?q?build:=20docker-compose.yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4dbb5db..b8141f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,8 @@ services: OAUTH2_KAKAO_CLIENT_SECRET: "${OAUTH2_KAKAO_CLIENT_SECRET}" EMAIL_USERNAME: "${EMAIL_USERNAME}" EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" - GEMINI_URL: "${ secrets.GEMINI_URL }" - GEMINI_KEY: "${ secrets.GEMINI_KEY }" + GEMINI_URL: "${GEMINI_URL}" + GEMINI_KEY: "${GEMINI_KEY}" depends_on: - db From e02c6969f71ae3ade474b9baad15baac21c11895 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 19:30:41 +0900 Subject: [PATCH 183/215] =?UTF-8?q?uild:=20docker-compose.yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b8141f4..742d8fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,22 +6,22 @@ services: ports: - "8080:8080" environment: - MYSQL_USERNAME: "${MYSQL_USERNAME}" - MYSQL_PASSWORD: "${MYSQL_PASSWORD}" - MYSQL_URL: "${MYSQL_URL}" - JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" - JWT_SECRET: "${JWT_SECRET}" - YOUR_ACCESS_KEY: "${YOUR_ACCESS_KEY}" - YOUR_SECRET_KEY: "${YOUR_SECRET_KEY}" - YOUR_BUCKET_NAME: "${YOUR_BUCKET_NAME}" - OAUTH2_GOOGLE_CLIENT_ID: "${OAUTH2_GOOGLE_CLIENT_ID}" - OAUTH2_GOOGLE_CLIENT_SECRET: "${OAUTH2_GOOGLE_CLIENT_SECRET}" - OAUTH2_KAKAO_CLIENT_ID : "${OAUTH2_KAKAO_CLIENT_ID}" - OAUTH2_KAKAO_CLIENT_SECRET: "${OAUTH2_KAKAO_CLIENT_SECRET}" - EMAIL_USERNAME: "${EMAIL_USERNAME}" - EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" - GEMINI_URL: "${GEMINI_URL}" - GEMINI_KEY: "${GEMINI_KEY}" + MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" + MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" + MYSQL_URL: "${{ secrets.MYSQL_URL }}" + JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" + JWT_SECRET: "${{ secrets.JWT_SECRET }}" + YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" + YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" + YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" + OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" + OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" + OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" + OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" + EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" + EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" + GEMINI_URL: "${{ secrets.GEMINI_URL }}" + GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" depends_on: - db From d3dda5c09a77bdb8e8c9aef92b41a4f619de5603 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 19:45:56 +0900 Subject: [PATCH 184/215] =?UTF-8?q?build:=20docker-compose.yml,=20deploy.s?= =?UTF-8?q?h=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 7 +++---- docker-compose.yml | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/deploy.sh b/deploy.sh index 3c6cc73..8a9152f 100644 --- a/deploy.sh +++ b/deploy.sh @@ -11,11 +11,10 @@ ECR_URI="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY" aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_URI # 최신 이미지 Pull -docker pull $ECR_URI:$IMAGE_TAG +sudo docker pull $ECR_URI:$IMAGE_TAG # 기존 컨테이너 중지 및 삭제 -docker stop app || true -docker rm app || true +sudo docker-compose down || true # docker-compose 실행 (환경변수 적용) -docker-compose -f /home/ubuntu/docker-compose.yml up -d +sudo docker-compose -f /home/ubuntu/docker-compose.yml up -d diff --git a/docker-compose.yml b/docker-compose.yml index 742d8fb..b8141f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,22 +6,22 @@ services: ports: - "8080:8080" environment: - MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" - MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" - MYSQL_URL: "${{ secrets.MYSQL_URL }}" - JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" - JWT_SECRET: "${{ secrets.JWT_SECRET }}" - YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" - YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" - YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" - OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" - OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" - OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" - OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" - EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" - EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" - GEMINI_URL: "${{ secrets.GEMINI_URL }}" - GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" + MYSQL_USERNAME: "${MYSQL_USERNAME}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD}" + MYSQL_URL: "${MYSQL_URL}" + JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" + JWT_SECRET: "${JWT_SECRET}" + YOUR_ACCESS_KEY: "${YOUR_ACCESS_KEY}" + YOUR_SECRET_KEY: "${YOUR_SECRET_KEY}" + YOUR_BUCKET_NAME: "${YOUR_BUCKET_NAME}" + OAUTH2_GOOGLE_CLIENT_ID: "${OAUTH2_GOOGLE_CLIENT_ID}" + OAUTH2_GOOGLE_CLIENT_SECRET: "${OAUTH2_GOOGLE_CLIENT_SECRET}" + OAUTH2_KAKAO_CLIENT_ID : "${OAUTH2_KAKAO_CLIENT_ID}" + OAUTH2_KAKAO_CLIENT_SECRET: "${OAUTH2_KAKAO_CLIENT_SECRET}" + EMAIL_USERNAME: "${EMAIL_USERNAME}" + EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" + GEMINI_URL: "${GEMINI_URL}" + GEMINI_KEY: "${GEMINI_KEY}" depends_on: - db From 79ea384fe64adb9708518089fd6004ef79cb798d Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 20:19:54 +0900 Subject: [PATCH 185/215] =?UTF-8?q?build:=20docker-compose.yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index b8141f4..e7bd1e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,8 @@ services: EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" GEMINI_URL: "${GEMINI_URL}" GEMINI_KEY: "${GEMINI_KEY}" + env_file: + - .env depends_on: - db @@ -34,6 +36,9 @@ services: - db-data:/var/lib/mysql ports: - "3306:3306" + env_file: + - .env + volumes: db-data: From 00760e1f5e0438180dd19bda4be25b7ef2bdea21 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 20:25:31 +0900 Subject: [PATCH 186/215] =?UTF-8?q?build:=20docker-compose.yml,=20github-a?= =?UTF-8?q?ctions=20yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 16 ---------------- docker-compose.yml | 4 ---- 2 files changed, 20 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 27adfc7..75c83c5 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -35,22 +35,6 @@ jobs: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: game_mate IMAGE_TAG: latest - MYSQL_USERNAME: "${{ secrets.MYSQL_USERNAME }}" - MYSQL_PASSWORD: "${{ secrets.MYSQL_PASSWORD }}" - MYSQL_URL: "${{ secrets.MYSQL_URL }}" - JPA_HIBERNATE_DDL: "${{ secrets.JPA_HIBERNATE_DDL }}" - JWT_SECRET: "${{ secrets.JWT_SECRET }}" - YOUR_ACCESS_KEY: "${{ secrets.YOUR_ACCESS_KEY }}" - YOUR_SECRET_KEY: "${{ secrets.YOUR_SECRET_KEY }}" - YOUR_BUCKET_NAME: "${{ secrets.YOUR_BUCKET_NAME }}" - OAUTH2_GOOGLE_CLIENT_ID: "${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" - OAUTH2_GOOGLE_CLIENT_SECRET: "${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" - OAUTH2_KAKAO_CLIENT_ID: "${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" - OAUTH2_KAKAO_CLIENT_SECRET: "${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" - EMAIL_USERNAME: "${{ secrets.EMAIL_USERNAME }}" - EMAIL_APP_PASSWORD: "${{ secrets.EMAIL_APP_PASSWORD }}" - GEMINI_URL: "${{ secrets.GEMINI_URL }}" - GEMINI_KEY: "${{ secrets.GEMINI_KEY }}" run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG diff --git a/docker-compose.yml b/docker-compose.yml index e7bd1e8..b42ee4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,8 +22,6 @@ services: EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" GEMINI_URL: "${GEMINI_URL}" GEMINI_KEY: "${GEMINI_KEY}" - env_file: - - .env depends_on: - db @@ -36,8 +34,6 @@ services: - db-data:/var/lib/mysql ports: - "3306:3306" - env_file: - - .env volumes: From a37d6a23f9667729a0495aae27a1250dac306612 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 20:40:23 +0900 Subject: [PATCH 187/215] =?UTF-8?q?build:=20github-actions=20yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/docker-multi-stage-build.yml | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 75c83c5..956c20b 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -49,6 +49,37 @@ jobs: source: "./deploy.sh, ./docker-compose.yml" target: "/home/ubuntu/" + # .env 파일 생성 후 EC2로 전송 + - name: Create and Upload .env file to EC2 + run: | + echo "DOCKERHUB_USERNAME=${{ vars.DOCKERHUB_USERNAME }}" > .env + echo "DOCKER_IMAGE_TAG_NAME=${{ vars.DOCKER_IMAGE_TAG_NAME }}" >> .env + echo "MYSQL_USERNAME=${{ secrets.MYSQL_USERNAME }}" >> .env + echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> .env + echo "MYSQL_URL=${{ secrets.MYSQL_URL }}" >> .env + echo "JPA_HIBERNATE_DDL=${{ secrets.JPA_HIBERNATE_DDL }}" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "YOUR_ACCESS_KEY=${{ secrets.YOUR_ACCESS_KEY }}" >> .env + echo "YOUR_SECRET_KEY=${{ secrets.YOUR_SECRET_KEY }}" >> .env + echo "YOUR_BUCKET_NAME=${{ secrets.YOUR_BUCKET_NAME }}" >> .env + echo "OAUTH2_GOOGLE_CLIENT_ID=${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" >> .env + echo "OAUTH2_GOOGLE_CLIENT_SECRET=${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" >> .env + echo "OAUTH2_KAKAO_CLIENT_ID=${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" >> .env + echo "OAUTH2_KAKAO_CLIENT_SECRET=${{ secrets.OAUTH2_KAKAO_CLIENT_SECRET }}" >> .env + echo "EMAIL_USERNAME=${{ secrets.EMAIL_USERNAME }}" >> .env + echo "EMAIL_APP_PASSWORD=${{ secrets.EMAIL_APP_PASSWORD }}" >> .env + echo "GEMINI_URL=${{ secrets.GEMINI_URL }}" >> .env + echo "GEMINI_KEY=${{ secrets.GEMINI_KEY }}" >> .env + + - name: Upload .env file to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_KEY }} + source: "./.env" + target: "/home/ubuntu/" + # EC2에서 deploy.sh 실행 (최신 Docker 이미지 가져와서 실행) - name: Deploy to EC2 uses: appleboy/ssh-action@master @@ -56,6 +87,7 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_KEY }} + script: | chmod +x /home/ubuntu/deploy.sh - /home/ubuntu/deploy.sh \ No newline at end of file + /home/ubuntu/deploy.sh From 2a9f09f5d777bfdc1d6f3c02b53b145d4bd24039 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 21:07:42 +0900 Subject: [PATCH 188/215] =?UTF-8?q?build:=20github-actions=20yml=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 956c20b..b0bd8a2 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -1,8 +1,9 @@ name: docker multi-stage build on: - pull_request: - branches: [ "develop" ] + push: + branches: + - '**' jobs: deploy: From 4fa1704cb9000cb928ad25e9e886abf37925a6cf Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 21:14:54 +0900 Subject: [PATCH 189/215] =?UTF-8?q?build:=20docker-compose.yml,=20github-a?= =?UTF-8?q?ctions=20yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 5 ++--- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index b0bd8a2..956c20b 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -1,9 +1,8 @@ name: docker multi-stage build on: - push: - branches: - - '**' + pull_request: + branches: [ "develop" ] jobs: deploy: diff --git a/docker-compose.yml b/docker-compose.yml index b42ee4b..13078c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: "${DOCKERHUB_USERNAME}/${DOCKER_IMAGE_TAG_NAME}:latest" + image: "863518453426.dkr.ecr.ap-northeast-2.amazonaws.com/game_mate:latest" platform: linux/amd64 container_name: app ports: From 529813f6c5e889122540852036e768037da869c9 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 21:33:58 +0900 Subject: [PATCH 190/215] =?UTF-8?q?build:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 7 ++++--- docker-compose.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 956c20b..adb2321 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -59,9 +59,10 @@ jobs: echo "MYSQL_URL=${{ secrets.MYSQL_URL }}" >> .env echo "JPA_HIBERNATE_DDL=${{ secrets.JPA_HIBERNATE_DDL }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env - echo "YOUR_ACCESS_KEY=${{ secrets.YOUR_ACCESS_KEY }}" >> .env - echo "YOUR_SECRET_KEY=${{ secrets.YOUR_SECRET_KEY }}" >> .env - echo "YOUR_BUCKET_NAME=${{ secrets.YOUR_BUCKET_NAME }}" >> .env + echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env + echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> .env + echo "AWS_BUCKET=${{ secrets.AWS_BUCKET }}" >> .env + echo "AWS_STACK_AUTO=${{ secrets.AWS_STACK_AUTO }}" >> .env echo "OAUTH2_GOOGLE_CLIENT_ID=${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" >> .env echo "OAUTH2_GOOGLE_CLIENT_SECRET=${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" >> .env echo "OAUTH2_KAKAO_CLIENT_ID=${{ secrets.OAUTH2_KAKAO_CLIENT_ID }}" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index 13078c0..f3bfe09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,9 +11,10 @@ services: MYSQL_URL: "${MYSQL_URL}" JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" JWT_SECRET: "${JWT_SECRET}" - YOUR_ACCESS_KEY: "${YOUR_ACCESS_KEY}" - YOUR_SECRET_KEY: "${YOUR_SECRET_KEY}" - YOUR_BUCKET_NAME: "${YOUR_BUCKET_NAME}" + AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}" + AWS_SECRET_KEY: "${AWS_SECRET_KEY}" + AWS_BUCKET: "${AWS_BUCKET}" + AWS_STACK_AUTO: "${AWS_STACK_AUTO}" OAUTH2_GOOGLE_CLIENT_ID: "${OAUTH2_GOOGLE_CLIENT_ID}" OAUTH2_GOOGLE_CLIENT_SECRET: "${OAUTH2_GOOGLE_CLIENT_SECRET}" OAUTH2_KAKAO_CLIENT_ID : "${OAUTH2_KAKAO_CLIENT_ID}" From b2541628c1f16d21ea34557f57474fbe49319af2 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 22:01:10 +0900 Subject: [PATCH 191/215] =?UTF-8?q?build:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index adb2321..52c416f 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -62,6 +62,7 @@ jobs: echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> .env echo "AWS_BUCKET=${{ secrets.AWS_BUCKET }}" >> .env + echo "AWS_REGION=${{ secrets.AWS_REGION }}" >> .env echo "AWS_STACK_AUTO=${{ secrets.AWS_STACK_AUTO }}" >> .env echo "OAUTH2_GOOGLE_CLIENT_ID=${{ secrets.OAUTH2_GOOGLE_CLIENT_ID }}" >> .env echo "OAUTH2_GOOGLE_CLIENT_SECRET=${{ secrets.OAUTH2_GOOGLE_CLIENT_SECRET }}" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index f3bfe09..40fb49b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}" AWS_SECRET_KEY: "${AWS_SECRET_KEY}" AWS_BUCKET: "${AWS_BUCKET}" + AWS_REGION: "${AWS_REGION}" AWS_STACK_AUTO: "${AWS_STACK_AUTO}" OAUTH2_GOOGLE_CLIENT_ID: "${OAUTH2_GOOGLE_CLIENT_ID}" OAUTH2_GOOGLE_CLIENT_SECRET: "${OAUTH2_GOOGLE_CLIENT_SECRET}" From d206ddbe142a8765205c02fa43f06e0b62dc3aef Mon Sep 17 00:00:00 2001 From: sumyeom Date: Tue, 4 Feb 2025 22:55:21 +0900 Subject: [PATCH 192/215] =?UTF-8?q?build:=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index 52c416f..a980adf 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -2,7 +2,11 @@ name: docker multi-stage build on: pull_request: - branches: [ "develop" ] + branches: + - develop + push: + branches: + - develop jobs: deploy: From 23e94ff385a75d5728def50d322935e082a044de Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 5 Feb 2025 01:17:52 +0900 Subject: [PATCH 193/215] =?UTF-8?q?fix:=20deploy.sh=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 8a9152f..433c39f 100644 --- a/deploy.sh +++ b/deploy.sh @@ -16,5 +16,5 @@ sudo docker pull $ECR_URI:$IMAGE_TAG # 기존 컨테이너 중지 및 삭제 sudo docker-compose down || true -# docker-compose 실행 (환경변수 적용) +# docker-compose 실행 sudo docker-compose -f /home/ubuntu/docker-compose.yml up -d From bb1b2dda0d42a8070fc3a1cad98c0bb742790b91 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 5 Feb 2025 02:13:12 +0900 Subject: [PATCH 194/215] =?UTF-8?q?fix:=20deploy.sh,=20github-action.yml?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 6 ++++-- deploy.sh | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index a980adf..f7a4716 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -12,7 +12,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # 모든 히스토리를 가져와서 최신 코드 보장 - name: Grant execute permission to gradlew run: chmod +x ./gradlew @@ -40,7 +42,7 @@ jobs: ECR_REPOSITORY: game_mate IMAGE_TAG: latest run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG # deploy.sh & docker-compose.yml을 EC2로 업로드 diff --git a/deploy.sh b/deploy.sh index 433c39f..f7baf77 100644 --- a/deploy.sh +++ b/deploy.sh @@ -8,13 +8,23 @@ IMAGE_TAG="latest" ECR_URI="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY" # AWS ECR 로그인 -aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_URI +aws ecr get-login-password --region $REGION | sudo docker login --username AWS --password-stdin $ECR_URI + +# 기존 컨테이너 중지 및 삭제 +if [ "$(sudo docker ps -q)" ]; then + sudo docker-compose down +fi + +# 기존 이미지 삭제 (최신 이미지 사용을 보장) +if [ "$(sudo docker images -q $ECR_URI:$IMAGE_TAG)" ]; then + sudo docker rmi -f $ECR_URI:$IMAGE_TAG +fi # 최신 이미지 Pull sudo docker pull $ECR_URI:$IMAGE_TAG -# 기존 컨테이너 중지 및 삭제 -sudo docker-compose down || true +# docker-compose 실행 경로 설정 +cd /home/ubuntu # docker-compose 실행 -sudo docker-compose -f /home/ubuntu/docker-compose.yml up -d +sudo docker-compose -f docker-compose.yml up -d --force-recreate From 74647b494db618423a37051582a1e1d52841147b Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 5 Feb 2025 11:37:33 +0900 Subject: [PATCH 195/215] =?UTF-8?q?refactor=20:=20=EC=9D=BD=EC=A7=80?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EC=95=8C=EB=A6=BC=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 알림갯수만큼 쿼리가 나가는 것에서 벌크업데이트로 1개의 쿼리로 처리하도록 수정 --- .../repository/NotificationRepository.java | 12 +++++++++++- .../notification/service/NotificationService.java | 6 +----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java index cb4428d..ef47e90 100644 --- a/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/gamemate/domain/notification/repository/NotificationRepository.java @@ -2,6 +2,9 @@ import com.example.gamemate.domain.notification.entity.Notification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -11,6 +14,13 @@ public interface NotificationRepository extends JpaRepository { List findAllByReceiverId(Long receiverId); - List findAllByReceiverIdAndIsRead(Long receiverId, boolean isRead); + + @Query("UPDATE Notification n " + + "SET n.isRead = true " + + "WHERE n.receiver.id = :receiverId " + + "AND n.isRead = false") + @Modifying + void updateUnreadNotificationToRead(@Param("receiverId") Long receiverId); + Optional findTopByReceiverIdAndIsReadOrderByCreatedAtDesc(Long receiverId, boolean isRead); } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index e413883..725967b 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -113,11 +113,7 @@ public void readNotification(User loginUser, Long id) { */ @Transactional public void readAllNotification(User loginUser) { - List unreadNotificationList = notificationRepository.findAllByReceiverIdAndIsRead(loginUser.getId(), false); - - for (Notification notification : unreadNotificationList) { - notification.updateIsRead(true); - } + notificationRepository.updateUnreadNotificationToRead(loginUser.getId()); } /** From 100bb374de4777e7a9c51b448e2a77979a8aa4be Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 5 Feb 2025 11:50:09 +0900 Subject: [PATCH 196/215] =?UTF-8?q?fix:=20redis=5Fhost=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 2 +- docker-compose.yml | 1 + src/main/resources/application.properties | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index f7a4716..fc5d21f 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -78,7 +78,7 @@ jobs: echo "EMAIL_APP_PASSWORD=${{ secrets.EMAIL_APP_PASSWORD }}" >> .env echo "GEMINI_URL=${{ secrets.GEMINI_URL }}" >> .env echo "GEMINI_KEY=${{ secrets.GEMINI_KEY }}" >> .env - + echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env - name: Upload .env file to EC2 uses: appleboy/scp-action@master with: diff --git a/docker-compose.yml b/docker-compose.yml index 40fb49b..c963f78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: EMAIL_APP_PASSWORD: "${EMAIL_APP_PASSWORD}" GEMINI_URL: "${GEMINI_URL}" GEMINI_KEY: "${GEMINI_KEY}" + REDIS_HOST: "${REDIS_HOST}" depends_on: - db diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e9035a0..c6ab0af 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -91,5 +91,5 @@ spring.sql.init.mode=always spring.jpa.properties.jakarta.persistence.lock.timeout=3000 # Redis -spring.data.redis.host=localhost +spring.data.redis.host=${REDIS_HOST} spring.data.redis.port=6379 From 4d7bb1461d75397f91e9f3ca479a205c01e9e78b Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 5 Feb 2025 12:20:33 +0900 Subject: [PATCH 197/215] =?UTF-8?q?feat=20:=20=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영환경(prod) / 개발환경(dev) 분리 --- src/main/resources/application-dev.properties | 7 +++++++ src/main/resources/application-prod.properties | 6 ++++++ src/main/resources/application.properties | 11 +++-------- src/main/resources/{data.sql => data-dev.sql} | 0 4 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/application-dev.properties create mode 100644 src/main/resources/application-prod.properties rename src/main/resources/{data.sql => data-dev.sql} (100%) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..e101fa9 --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,7 @@ +spring.datasource.url=${MYSQL_DEV_URL} +spring.datasource.username=${MYSQL_USERNAME} +spring.datasource.password=${MYSQL_PASSWORD} +spring.jpa.hibernate.ddl-auto=create +spring.jpa.defer-datasource-initialization=true +spring.sql.init.mode=always +spring.sql.init.data-locations=classpath:data-dev.sql \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..5e88577 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,6 @@ +spring.datasource.url=${MYSQL_PROD_URL} +spring.datasource.username=${MYSQL_USERNAME} +spring.datasource.password=${MYSQL_PASSWORD} +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=false +spring.sql.init.mode=never \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 401a58f..616c042 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,16 +2,15 @@ spring.application.name=gamemate spring.config.import=optional:file:.env[.properties] +# Environment (prod, dev) +spring.profiles.active=dev + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.datasource.url=${MYSQL_URL} -spring.datasource.username=${MYSQL_USERNAME} -spring.datasource.password=${MYSQL_PASSWORD} spring.jpa.show-sql=true spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect # create, update, none, creat-drop spring.jpa.database=mysql -spring.jpa.hibernate.ddl-auto=${JPA_HIBERNATE_DDL} spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy spring.jpa.generate-ddl=false spring.jpa.properties.hibernate.format_sql=true @@ -83,10 +82,6 @@ cloud.aws.stack.auto=${AWS_STACK_AUTO} spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=5MB -# Dummy data -spring.jpa.defer-datasource-initialization=true -spring.sql.init.mode=always - # Redis spring.data.redis.host=localhost spring.data.redis.port=6379 diff --git a/src/main/resources/data.sql b/src/main/resources/data-dev.sql similarity index 100% rename from src/main/resources/data.sql rename to src/main/resources/data-dev.sql From 4440ade6459102fcac55d1f8090449e4e0312db7 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 5 Feb 2025 12:53:41 +0900 Subject: [PATCH 198/215] =?UTF-8?q?fix:=20BoardService=EC=99=80=20BoardVie?= =?UTF-8?q?wService=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 4 +- .../domain/board/service/BoardService.java | 183 +--------------- .../board/service/BoardViewService.java | 201 ++++++++++++++++++ 3 files changed, 208 insertions(+), 180 deletions(-) create mode 100644 src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java diff --git a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java index e807407..f88bdcb 100644 --- a/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java +++ b/src/main/java/com/example/gamemate/domain/board/controller/BoardController.java @@ -6,6 +6,7 @@ import com.example.gamemate.domain.board.dto.BoardFindOneResponseDto; import com.example.gamemate.domain.board.enums.BoardCategory; import com.example.gamemate.domain.board.service.BoardService; +import com.example.gamemate.domain.board.service.BoardViewService; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.validation.Valid; @@ -27,6 +28,7 @@ public class BoardController { private final BoardService boardService; + private final BoardViewService boardViewService; /** * 게시글 생성 API 입니다. @@ -62,7 +64,7 @@ public ResponseEntity> findTopBoards( boardCategory = BoardCategory.fromName(category); } - List dtos = boardService.findTopBoards(boardCategory); + List dtos = boardViewService.findTopBoards(boardCategory); if(dtos.isEmpty()){ return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 901eff2..12ae91b 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -11,35 +11,24 @@ import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; -@Slf4j @Service @RequiredArgsConstructor public class BoardService { private final BoardRepository boardRepository; - private final String VIEW_COUNT_KEY = "board:view:"; - private final String VIEW_RANKING_KEY = "board:ranking:"; - private final RedisTemplate redisTemplate; - private final HttpServletRequest request; + private final BoardViewService boardViewService; /** * 게시글 생성 메서드입니다. @@ -64,31 +53,6 @@ public BoardResponseDto createBoard(User loginUser, BoardRequestDto dto) { ); } - /** - * 조회수 높은 게시글 조회 하는 메서드입니다. - * - * @param boardCategory 카테고리 종류 - * @return 게시글 조회 List - */ - public List findTopBoards(BoardCategory boardCategory) { - List top5Boards = getTop5Boards(); - - List result = new ArrayList<>(); - - for(Board board : top5Boards) { - int redisViewCount = getViewCount(board.getId()); - result.add(new BoardFindAllResponseDto( - board.getId(), - board.getCategory(), - board.getTitle(), - board.getCreatedAt(), - redisViewCount - )); - } - - return result; - } - /** * 게시판 리스트 조회 메서드입니다. * @@ -110,7 +74,7 @@ public List findAllBoards(int page, BoardCategory categ board.getCategory(), board.getTitle(), board.getCreatedAt(), - getViewCount(board.getId()) + boardViewService.getViewCount(board.getId()) )) .collect(Collectors.toList()); } @@ -125,9 +89,9 @@ public List findAllBoards(int page, BoardCategory categ public BoardFindOneResponseDto findBoardById(Long id, User loginUser) { // 조회수 증가(Redis 저장) if(loginUser == null) { - increaseViewCount(id, null); + boardViewService.increaseViewCount(id, null); }else{ - increaseViewCount(id, loginUser.getId()); + boardViewService.increaseViewCount(id, loginUser.getId()); } // 게시글 조회 @@ -180,144 +144,5 @@ public void deleteBoard(User loginUser, Long id) { boardRepository.delete(findBoard); } - /** - * 조회수 증가시키는 메서드입니다. - * - * @param boardId 게시글 식별자 - */ - @Transactional - public void increaseViewCount(Long boardId, Long userId) { - String uniqueKey; - - if (userId != null) { - // 회원 : userId 기반으로 조회 제한 - uniqueKey = VIEW_COUNT_KEY + boardId + ":" + userId; - } else { - // 비회원 - String ipAddress = getClientIp(); - //String hashedIp = hashIpAddress(ipAddress); - uniqueKey = VIEW_COUNT_KEY + boardId + ":" + ipAddress; - } - - if (Boolean.FALSE.equals(redisTemplate.hasKey(uniqueKey))) { - redisTemplate.opsForValue().set(uniqueKey, "1", Duration.ofHours(1)); - redisTemplate.opsForValue().increment(VIEW_COUNT_KEY + boardId); - redisTemplate.opsForZSet().incrementScore(VIEW_RANKING_KEY, String.valueOf(boardId),1); - } - } - - /** - * 조회수 가져오는 메서드 입니다. - * - * @param boardId 게시글 식별자 - * @return 조회수 - */ - public int getViewCount(Long boardId){ - - String key = VIEW_COUNT_KEY + boardId; - String count = redisTemplate.opsForValue().get(key); - - if(count != null){ - return Integer.parseInt(count); - } - - // Redis에 값이 없으면 DB에서 조회 후 Redis 에 반영 - Board board = boardRepository.findById(boardId) - .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); - - int dbViewCount = board.getViews(); - // Redis 에 저장(초기화) - redisTemplate.opsForValue().set(key, String.valueOf(dbViewCount)); - - return dbViewCount; - } - - /** - * 클라이언트 IP 가져오는 메서드입니다.(프록시) - * - * @return ip 주소 - */ - private String getClientIp(){ - - String ip = request.getHeader("x-forwarded-for"); - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - - if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - return ip; - } - - - /** - * 매일 00시 정각 시간마다 동기화 - */ - @Scheduled(cron = "0 0 0 * * *") - @Transactional - public void syncRedisToDb(){ - log.info("Redis 조회수 데이터 DB로 동기화"); - - // 조회수 기반으로 DB에 업데이트 - Set keys = redisTemplate.keys(VIEW_COUNT_KEY + "*") - .stream() - .filter(key -> key.split(":").length == 3) // board:view:{boardId} 형식만 남김 - .collect(Collectors.toSet()); - if(!keys.isEmpty()){ - List updatedBoards = new ArrayList<>(); - for(String key : keys){ - Long boardId = Long.parseLong(key.split(":")[2]); - int viewCount = getViewCount(boardId); - if(viewCount > 0){ - Board findBoard = boardRepository.findById(boardId) - .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); - int boardViewCount = findBoard.getViews(); - findBoard.updateViewCount( viewCount); - updatedBoards.add(findBoard); - - // redis 값을 유지(db 값 반영 후 덮어쓰기) - redisTemplate.opsForValue().set(VIEW_COUNT_KEY + boardId, String.valueOf(viewCount)); - } - } - // 업데이트 - boardRepository.saveAll(updatedBoards); - log.info("업데이트 완료"); - } - - } - - /** - * 조회수 높은 5개의 게시글 조회 - * - * @return 조회수 높은 게시글 리스트 - */ - public List getTop5Boards(){ - Set topBoardIds = redisTemplate.opsForZSet().reverseRange(VIEW_RANKING_KEY, 0, 4); - - if(topBoardIds == null || topBoardIds.isEmpty()){ - return boardRepository.findTop5ByOrderByCreatedAtDesc(); - } - - List boardIds = topBoardIds.stream().map(Long::parseLong).toList(); - - // DB 에서 해당 게시글 조회(조회수 순으로 정렬) - List boards = boardRepository.findByIdIn(boardIds); - - // 조회수는 redis 값으로 최신화 - boards.forEach(board -> { - int redisViewCount = getViewCount(board.getId()); - board.updateViewCount(redisViewCount); - }); - - boards.sort(Comparator.comparing(Board::getViews).reversed() - .thenComparing(Board::getCreatedAt, Comparator.reverseOrder())); - - return boards; - } } diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java new file mode 100644 index 0000000..10ded65 --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardViewService.java @@ -0,0 +1,201 @@ +package com.example.gamemate.domain.board.service; + +import com.example.gamemate.domain.board.dto.BoardFindAllResponseDto; +import com.example.gamemate.domain.board.entity.Board; +import com.example.gamemate.domain.board.enums.BoardCategory; +import com.example.gamemate.domain.board.repository.BoardRepository; +import com.example.gamemate.global.constant.ErrorCode; +import com.example.gamemate.global.exception.ApiException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BoardViewService { + + private final BoardRepository boardRepository; + private final String VIEW_COUNT_KEY = "board:view:"; + private final String VIEW_RANKING_KEY = "board:ranking:"; + private final RedisTemplate redisTemplate; + private final HttpServletRequest request; + + + /** + * 조회수 높은 게시글 조회 하는 메서드입니다. + * + * @param boardCategory 카테고리 종류 + * @return 게시글 조회 List + */ + public List findTopBoards(BoardCategory boardCategory) { + List top5Boards = getTop5Boards(); + + List result = new ArrayList<>(); + + for(Board board : top5Boards) { + int redisViewCount = getViewCount(board.getId()); + result.add(new BoardFindAllResponseDto( + board.getId(), + board.getCategory(), + board.getTitle(), + board.getCreatedAt(), + redisViewCount + )); + } + + return result; + } + + /** + * 조회수 증가시키는 메서드입니다. + * + * @param boardId 게시글 식별자 + */ + @Transactional + public void increaseViewCount(Long boardId, Long userId) { + String uniqueKey; + + if (userId != null) { + // 회원 : userId 기반으로 조회 제한 + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + userId; + } else { + // 비회원 + String ipAddress = getClientIp(); + //String hashedIp = hashIpAddress(ipAddress); + uniqueKey = VIEW_COUNT_KEY + boardId + ":" + ipAddress; + } + + if (Boolean.FALSE.equals(redisTemplate.hasKey(uniqueKey))) { + redisTemplate.opsForValue().set(uniqueKey, "1", Duration.ofHours(1)); + redisTemplate.opsForValue().increment(VIEW_COUNT_KEY + boardId); + redisTemplate.opsForZSet().incrementScore(VIEW_RANKING_KEY, String.valueOf(boardId),1); + } + } + + /** + * 조회수 가져오는 메서드 입니다. + * + * @param boardId 게시글 식별자 + * @return 조회수 + */ + public int getViewCount(Long boardId){ + + String key = VIEW_COUNT_KEY + boardId; + String count = redisTemplate.opsForValue().get(key); + + if(count != null){ + return Integer.parseInt(count); + } + + // Redis에 값이 없으면 DB에서 조회 후 Redis 에 반영 + Board board = boardRepository.findById(boardId) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); + + int dbViewCount = board.getViews(); + + // Redis 에 저장(초기화) + redisTemplate.opsForValue().set(key, String.valueOf(dbViewCount)); + + return dbViewCount; + } + + /** + * 클라이언트 IP 가져오는 메서드입니다.(프록시) + * + * @return ip 주소 + */ + private String getClientIp(){ + + String ip = request.getHeader("x-forwarded-for"); + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + + if( ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + + /** + * 매일 00시 정각 시간마다 동기화 + */ + @Scheduled(cron = "0 0 0 * * *") + @Transactional + public void syncRedisToDb(){ + log.info("Redis 조회수 데이터 DB로 동기화"); + + // 조회수 기반으로 DB에 업데이트 + Set keys = redisTemplate.keys(VIEW_COUNT_KEY + "*") + .stream() + .filter(key -> key.split(":").length == 3) // board:view:{boardId} 형식만 남김 + .collect(Collectors.toSet()); + if(!keys.isEmpty()){ + List updatedBoards = new ArrayList<>(); + for(String key : keys){ + Long boardId = Long.parseLong(key.split(":")[2]); + int viewCount = getViewCount(boardId); + if(viewCount > 0){ + Board findBoard = boardRepository.findById(boardId) + .orElseThrow(()->new ApiException(ErrorCode.BOARD_NOT_FOUND)); + int boardViewCount = findBoard.getViews(); + findBoard.updateViewCount( viewCount); + updatedBoards.add(findBoard); + + // redis 값을 유지(db 값 반영 후 덮어쓰기) + redisTemplate.opsForValue().set(VIEW_COUNT_KEY + boardId, String.valueOf(viewCount)); + } + } + // 업데이트 + boardRepository.saveAll(updatedBoards); + log.info("업데이트 완료"); + } + + } + + /** + * 조회수 높은 5개의 게시글 조회 + * + * @return 조회수 높은 게시글 리스트 + */ + public List getTop5Boards(){ + Set topBoardIds = redisTemplate.opsForZSet().reverseRange(VIEW_RANKING_KEY, 0, 4); + + if(topBoardIds == null || topBoardIds.isEmpty()){ + return boardRepository.findTop5ByOrderByCreatedAtDesc(); + } + + List boardIds = topBoardIds.stream().map(Long::parseLong).toList(); + + // DB 에서 해당 게시글 조회(조회수 순으로 정렬) + List boards = boardRepository.findByIdIn(boardIds); + + // 조회수는 redis 값으로 최신화 + boards.forEach(board -> { + int redisViewCount = getViewCount(board.getId()); + board.updateViewCount(redisViewCount); + }); + + boards.sort(Comparator.comparing(Board::getViews).reversed() + .thenComparing(Board::getCreatedAt, Comparator.reverseOrder())); + + return boards; + } +} From 2bbfaa9dda81822f01c33e9856a8c29936379ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 13:10:17 +0900 Subject: [PATCH 199/215] =?UTF-8?q?refactor:=20Redis=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=85=BC=EB=A6=AC?= =?UTF-8?q?=EC=A0=81=20=EB=B6=84=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../domain/board/service/BoardService.java | 14 ++- .../domain/coupon/service/CouponService.java | 28 ++++-- .../service/NotificationService.java | 21 ++++- .../service/RedisStreamService.java | 15 ++- .../gamemate/global/config/RedisConfig.java | 91 ++++++++++++++++++- 6 files changed, 148 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 342a21a..1bb04d1 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,7 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.27.1' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java index 901eff2..5f9178f 100644 --- a/src/main/java/com/example/gamemate/domain/board/service/BoardService.java +++ b/src/main/java/com/example/gamemate/domain/board/service/BoardService.java @@ -14,11 +14,13 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; 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.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,15 +34,23 @@ @Slf4j @Service -@RequiredArgsConstructor public class BoardService { private final BoardRepository boardRepository; private final String VIEW_COUNT_KEY = "board:view:"; private final String VIEW_RANKING_KEY = "board:ranking:"; - private final RedisTemplate redisTemplate; + private final StringRedisTemplate redisTemplate; private final HttpServletRequest request; + public BoardService( + BoardRepository boardRepository, + @Qualifier("viewCountRedisTemplate")StringRedisTemplate redisTemplate, + HttpServletRequest request) { + this.boardRepository = boardRepository; + this.redisTemplate = redisTemplate; + this.request = request; + } + /** * 게시글 생성 메서드입니다. * diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 8cb9c80..6d90825 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -12,6 +12,7 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,16 +23,21 @@ @Service @Transactional -@RequiredArgsConstructor public class CouponService { + + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; private final StringRedisTemplate redisTemplate; - private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; - - private String getCouponStockKey(Long couponId) { - return String.format(COUPON_STOCK_KEY, couponId); + public CouponService( + CouponRepository couponRepository, + UserCouponRepository userCouponRepository, + @Qualifier("couponRedisTemplate") StringRedisTemplate redisTemplate) { + this.couponRepository = couponRepository; + this.userCouponRepository = userCouponRepository; + this.redisTemplate = redisTemplate; } public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, User loginUser) { @@ -59,6 +65,12 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U return new CouponCreateResponseDto(savedCoupon); } + private void validateCouponDates(LocalDateTime startAt, LocalDateTime expiredAt) { + if (startAt.isAfter(expiredAt)) { + throw new ApiException(ErrorCode.INVALID_COUPON_DATE); + } + } + public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { Coupon coupon = couponRepository.findById(couponId) .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); @@ -131,9 +143,7 @@ public void useCoupon(Long userCouponId, User loginUser) { userCoupon.updateUsedAt(); } - private void validateCouponDates(LocalDateTime startAt, LocalDateTime expiredAt) { - if (startAt.isAfter(expiredAt)) { - throw new ApiException(ErrorCode.INVALID_COUPON_DATE); - } + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); } } diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index e413883..518391a 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -12,6 +12,8 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.StreamInfo; import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.core.RedisTemplate; @@ -25,17 +27,28 @@ * 알림을 처리하는 서비스 클래스입니다. */ @Service -@RequiredArgsConstructor @Slf4j public class NotificationService { + private static final String STREAM_KEY = "notification_stream"; + private static final String GROUP_NAME = "notification-group"; + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + private final NotificationRepository notificationRepository; private final EmitterRepository emitterRepository; private final RedisStreamService redisStreamService; private final RedisTemplate redisTemplate; - private static final String STREAM_KEY = "notification_stream"; - private static final String GROUP_NAME = "notification-group"; - private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + + public NotificationService( + NotificationRepository notificationRepository, + EmitterRepository emitterRepository, + RedisStreamService redisStreamService, + @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate) { + this.notificationRepository = notificationRepository; + this.emitterRepository = emitterRepository; + this.redisStreamService = redisStreamService; + this.redisTemplate = redisTemplate; + } /** * 레디스 스트림의 스트림그룹을 생성합니다. diff --git a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java index f272e9d..13bf2de 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java @@ -5,6 +5,7 @@ import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.*; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; @@ -16,19 +17,27 @@ import java.util.*; @Service -@RequiredArgsConstructor @Slf4j public class RedisStreamService { - private final RedisTemplate redisTemplate; - private final EmitterRepository emitterRepository; private static final String STREAM_KEY = "notification_stream"; private static final String GROUP_NAME = "notification-group"; + private static final String CONSUMER_PREFIX = "consumer"; private static final int BATCH_SIZE = 100; private static final Duration POLL_TIMEOUT = Duration.ofMillis(100); private static final int MAX_STREAM_LENGTH = 1000; + private final RedisTemplate redisTemplate; + private final EmitterRepository emitterRepository; + + public RedisStreamService( + @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate, + EmitterRepository emitterRepository) { + this.redisTemplate = redisTemplate; + this.emitterRepository = emitterRepository; + } + @PostConstruct public void init() { createStreamGroup(); diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 59f3729..f07d0c1 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -1,25 +1,108 @@ package com.example.gamemate.global.config; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + // LettuceConnectionFactory를 DB별로 하나씩 생성하여 Bean으로 관리 + // DB 0: 알림 + @Bean + public LettuceConnectionFactory notificationConnectionFactory() { + return createLettuceConnectionFactory(0); + } + + // DB 1: 조회수 @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + public LettuceConnectionFactory viewCountConnectionFactory() { + return createLettuceConnectionFactory(1); + } + + // DB 2: 리프레시 토큰 + @Bean + public LettuceConnectionFactory refreshTokenConnectionFactory() { + return createLettuceConnectionFactory(2); + } + + // DB 3: 토큰 블랙리스트 + @Bean + public LettuceConnectionFactory tokenBlacklistConnectionFactory() { + return createLettuceConnectionFactory(3); + } + + // DB 4: 쿠폰 + @Bean + public LettuceConnectionFactory couponConnectionFactory() { + return createLettuceConnectionFactory(4); + } + + // 공통: LettuceConnectionFactory 생성 + private LettuceConnectionFactory createLettuceConnectionFactory(int database) { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(redisHost); + configuration.setPort(redisPort); + configuration.setDatabase(database); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration); + connectionFactory.afterPropertiesSet(); + return connectionFactory; + } + + // 알림 RedisTemplate (DB 0) + @Bean + public RedisTemplate notificationRedisTemplate( + @Qualifier("notificationConnectionFactory") LettuceConnectionFactory connectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); - - // String 타입을 위한 직렬화 설정 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); - return redisTemplate; } + + // 조회수 RedisTemplate (DB 1) + @Bean + public StringRedisTemplate viewCountRedisTemplate( + @Qualifier("viewCountConnectionFactory") LettuceConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } + + // 리프레시 토큰 RedisTemplate (DB 2) + @Bean + public StringRedisTemplate refreshTokenRedisTemplate( + @Qualifier("refreshTokenConnectionFactory") LettuceConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } + + // 토큰 블랙리스트 RedisTemplate (DB 3) + @Bean + public StringRedisTemplate tokenBlacklistRedisTemplate( + @Qualifier("tokenBlacklistConnectionFactory") LettuceConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } + + // 쿠폰 RedisTemplate (DB 4) + @Bean + public StringRedisTemplate couponRedisTemplate( + @Qualifier("couponConnectionFactory") LettuceConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } } \ No newline at end of file From 74dc863a92a306b05fdd8a93f3f0435265bc133f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 15:14:33 +0900 Subject: [PATCH 200/215] =?UTF-8?q?fix:=20RedisConfig=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../service/NotificationService.java | 4 +--- .../service/RedisStreamService.java | 3 +-- .../global/config/CouponDataSynchronizer.java | 21 ++++++++++++++----- .../gamemate/global/config/RedisConfig.java | 14 ++++++------- .../application-noreactive.properties | 1 + src/main/resources/application.properties | 6 +++++- 7 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/application-noreactive.properties diff --git a/build.gradle b/build.gradle index 1bb04d1..342a21a 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,6 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.redisson:redisson-spring-boot-starter:3.27.1' } tasks.named('test') { diff --git a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java index 518391a..3c162fc 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/NotificationService.java @@ -10,9 +10,7 @@ import com.example.gamemate.global.exception.ApiException; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.StreamInfo; import org.springframework.data.redis.connection.stream.StreamRecords; @@ -43,7 +41,7 @@ public NotificationService( NotificationRepository notificationRepository, EmitterRepository emitterRepository, RedisStreamService redisStreamService, - @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate) { + @Qualifier("redisTemplate") RedisTemplate redisTemplate) { this.notificationRepository = notificationRepository; this.emitterRepository = emitterRepository; this.redisStreamService = redisStreamService; diff --git a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java index 13bf2de..d153941 100644 --- a/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java +++ b/src/main/java/com/example/gamemate/domain/notification/service/RedisStreamService.java @@ -3,7 +3,6 @@ import com.example.gamemate.domain.notification.dto.NotificationResponseDto; import com.example.gamemate.domain.notification.repository.EmitterRepository; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.connection.stream.*; @@ -32,7 +31,7 @@ public class RedisStreamService { private final EmitterRepository emitterRepository; public RedisStreamService( - @Qualifier("notificationRedisTemplate") RedisTemplate redisTemplate, + @Qualifier("redisTemplate") RedisTemplate redisTemplate, EmitterRepository emitterRepository) { this.redisTemplate = redisTemplate; this.emitterRepository = emitterRepository; diff --git a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java index 2e670ec..290dda0 100644 --- a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java +++ b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java @@ -4,6 +4,7 @@ import com.example.gamemate.domain.coupon.repository.CouponRepository; import com.example.gamemate.domain.coupon.repository.UserCouponRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.data.redis.core.StringRedisTemplate; @@ -12,16 +13,21 @@ import java.util.List; @Component -@RequiredArgsConstructor public class CouponDataSynchronizer { + + private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; + private final CouponRepository couponRepository; private final UserCouponRepository userCouponRepository; private final StringRedisTemplate redisTemplate; - private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; - - private String getCouponStockKey(Long couponId) { - return String.format(COUPON_STOCK_KEY, couponId); + public CouponDataSynchronizer( + CouponRepository couponRepository, + UserCouponRepository userCouponRepository, + @Qualifier("couponRedisTemplate") StringRedisTemplate redisTemplate) { + this.couponRepository = couponRepository; + this.userCouponRepository = userCouponRepository; + this.redisTemplate = redisTemplate; } @EventListener(ApplicationReadyEvent.class) @@ -34,4 +40,9 @@ public void syncCouponStock() { String.valueOf(remainingStock)); } } + + + private String getCouponStockKey(Long couponId) { + return String.format(COUPON_STOCK_KEY, couponId); + } } \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index f07d0c1..016c0f2 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -1,17 +1,15 @@ package com.example.gamemate.global.config; -import org.redisson.Redisson; -import org.redisson.api.RedissonClient; -import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @@ -26,6 +24,7 @@ public class RedisConfig { // LettuceConnectionFactory를 DB별로 하나씩 생성하여 Bean으로 관리 // DB 0: 알림 @Bean + @Primary public LettuceConnectionFactory notificationConnectionFactory() { return createLettuceConnectionFactory(0); } @@ -67,14 +66,15 @@ private LettuceConnectionFactory createLettuceConnectionFactory(int database) { // 알림 RedisTemplate (DB 0) @Bean - public RedisTemplate notificationRedisTemplate( + @Primary + public RedisTemplate redisTemplate( @Qualifier("notificationConnectionFactory") LettuceConnectionFactory connectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); return redisTemplate; } diff --git a/src/main/resources/application-noreactive.properties b/src/main/resources/application-noreactive.properties new file mode 100644 index 0000000..29df9aa --- /dev/null +++ b/src/main/resources/application-noreactive.properties @@ -0,0 +1 @@ +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e9035a0..e85e6f6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=gamemate -spring.config.import=optional:file:.env[.properties] +spring.config.import=optional:file:.env[.properties],optional:classpath:/application-noreactive.properties spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=${MYSQL_URL} @@ -67,6 +67,10 @@ logging.level.org.springframework.web.servlet=DEBUG logging.level.org.springframework.security.oauth2=TRACE logging.level.org.springframework.web.client=TRACE logging.level.com.example.gamemate=DEBUG +logging.level.org.springframework.data.redis=DEBUG +logging.level.com.example.gamemate.domain.notification.service.RedisStreamService=DEBUG +logging.level.org.springframework.data.redis.core=DEBUG +logging.level.io.lettuce.core=DEBUG # Gemini gemini.api.url=${GEMINI_URL} From c956d829500b8200d98ed4348674199b0ecffdc1 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Wed, 5 Feb 2025 15:55:10 +0900 Subject: [PATCH 201/215] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 2 ++ docker-compose.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index fc5d21f..bec24df 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -63,6 +63,8 @@ jobs: echo "MYSQL_USERNAME=${{ secrets.MYSQL_USERNAME }}" >> .env echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> .env echo "MYSQL_URL=${{ secrets.MYSQL_URL }}" >> .env + echo "MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }}" >> .env + echo "MYSQL_DEV_URL=${{ secrets.MYSQL_DEV_URL }}" >> .env echo "JPA_HIBERNATE_DDL=${{ secrets.JPA_HIBERNATE_DDL }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index c963f78..b2b4b6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,8 @@ services: MYSQL_USERNAME: "${MYSQL_USERNAME}" MYSQL_PASSWORD: "${MYSQL_PASSWORD}" MYSQL_URL: "${MYSQL_URL}" + MYSQL_PROD_URL: "${MYSQL_PROD_URL}" + MYSQL_DEV_URL: "${MYSQL_DEV_URL}" JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" JWT_SECRET: "${JWT_SECRET}" AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}" From 9f532ffcc709c813053849c7a1b344307edcf32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 17:32:08 +0900 Subject: [PATCH 202/215] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20Redis=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 - 리프레시 토큰 Redis에서 관리 - 토큰 블랙리스트 Redis에서 관리 - 토큰 서비스 분리 --- .../auth/controller/AuthController.java | 5 +- ...nseDto.java => LoginTokenResponseDto.java} | 2 +- .../domain/auth/service/AuthService.java | 32 +++---- .../auth/service/RefreshTokenService.java | 69 ++++++++++++++ .../domain/auth/service/TokenService.java | 89 ++++++------------- .../user/controller/UserController.java | 2 +- .../gamemate/domain/user/entity/User.java | 11 --- .../domain/user/service/UserService.java | 14 ++- .../config/auth/OAuth2SuccessHandler.java | 32 ++----- .../global/provider/JwtTokenProvider.java | 4 +- src/main/resources/application.properties | 4 + 11 files changed, 128 insertions(+), 136 deletions(-) rename src/main/java/com/example/gamemate/domain/auth/dto/{LocalLoginResponseDto.java => LoginTokenResponseDto.java} (83%) create mode 100644 src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java diff --git a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java index 0d71fd7..ed65594 100644 --- a/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/gamemate/domain/auth/controller/AuthController.java @@ -3,7 +3,6 @@ import com.example.gamemate.domain.auth.dto.*; import com.example.gamemate.domain.auth.service.AuthService; import com.example.gamemate.domain.auth.service.EmailService; -import com.example.gamemate.domain.auth.service.OAuth2Service; import com.example.gamemate.global.config.auth.CustomUserDetails; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -48,11 +47,11 @@ public ResponseEntity verifyEmail( @PostMapping("/login") - public ResponseEntity localLogin( + public ResponseEntity localLogin( @Valid @RequestBody LocalLoginRequestDto requestDto, HttpServletResponse response ) { - LocalLoginResponseDto responseDto = authService.localLogin(requestDto, response); + LoginTokenResponseDto responseDto = authService.localLogin(requestDto, response); return new ResponseEntity<>(responseDto, HttpStatus.OK); } diff --git a/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java b/src/main/java/com/example/gamemate/domain/auth/dto/LoginTokenResponseDto.java similarity index 83% rename from src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java rename to src/main/java/com/example/gamemate/domain/auth/dto/LoginTokenResponseDto.java index 606c9b4..21d178f 100644 --- a/src/main/java/com/example/gamemate/domain/auth/dto/LocalLoginResponseDto.java +++ b/src/main/java/com/example/gamemate/domain/auth/dto/LoginTokenResponseDto.java @@ -5,7 +5,7 @@ @Getter @RequiredArgsConstructor -public class LocalLoginResponseDto { +public class LoginTokenResponseDto { private final String accessToken; diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index 85ea800..e36b07f 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -11,11 +11,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import java.util.Optional; @@ -27,6 +25,7 @@ public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final TokenService tokenService; + private final RefreshTokenService refreshTokenService; private final JwtTokenProvider jwtTokenProvider; private final EmailService emailService; @@ -57,7 +56,7 @@ public SignupResponseDto signup(SignupRequestDto requestDto) { return new SignupResponseDto(savedUser); } - public LocalLoginResponseDto localLogin(LocalLoginRequestDto requestDto, HttpServletResponse response) { + public LoginTokenResponseDto localLogin(LocalLoginRequestDto requestDto, HttpServletResponse response) { User findUser = userRepository.findByEmail(requestDto.getEmail()) .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); @@ -96,37 +95,30 @@ public TokenRefreshResponseDto refreshAccessToken(String refreshToken) { } String email = jwtTokenProvider.getEmailFromToken(refreshToken); - User user = userRepository.findByEmail(email) - .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + String storedToken = refreshTokenService.getRefreshToken(email); - if(!refreshToken.equals(user.getRefreshToken())) { + if(!refreshToken.equals(storedToken)) { throw new ApiException(ErrorCode.INVALID_TOKEN); } + User user = userRepository.findByEmail(email) + .orElseThrow(()-> new ApiException(ErrorCode.USER_NOT_FOUND)); + String newAccessToken = jwtTokenProvider.createAccessToken(email, user.getRole()); return new TokenRefreshResponseDto(newAccessToken); } public void logout(User user, HttpServletRequest request, HttpServletResponse response) { - String accessToken = extractToken(request); + // 엑세스 토큰 블랙리스트 추가 + String accessToken = tokenService.extractToken(request); if(accessToken != null) { tokenService.blacklistToken(accessToken); } - String refreshToken = tokenService.extractRefreshTokenFromCookie(request); + // 리프레시 토큰 처리 + String refreshToken = refreshTokenService.extractRefreshTokenFromCookie(request); if(refreshToken != null) { - user.removeRefreshToken(); - userRepository.save(user); - tokenService.removeRefreshTokenCookie(response); + refreshTokenService.removeRefreshToken(user.getEmail(), response); } } - - private String extractToken(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } - } diff --git a/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java new file mode 100644 index 0000000..28395be --- /dev/null +++ b/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java @@ -0,0 +1,69 @@ +package com.example.gamemate.domain.auth.service; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; + +@Service +@RequiredArgsConstructor +@Transactional +public class RefreshTokenService { + + private final StringRedisTemplate refreshTokenRedisTemplate; + private final Duration REFRESH_TOKEN_TTL = Duration.ofDays(7); + private int refreshTokenMaxAge = 60 * 60 * 24 * 7; // 7일 + + public void saveRefreshToken(String email, String refreshToken, HttpServletResponse response) { + String key = getKey(email); + refreshTokenRedisTemplate.opsForValue().set(key, refreshToken, REFRESH_TOKEN_TTL); + addRefreshTokenToCookie(response, refreshToken); + } + + public String getRefreshToken(String email) { + String key = getKey(email); + return refreshTokenRedisTemplate.opsForValue().get(key); + } + + public void removeRefreshToken(String email, HttpServletResponse response) { + String key = getKey(email); + refreshTokenRedisTemplate.delete(key); + removeRefreshTokenCookie(response); + } + + private String getKey(String email) { + return "refresh_token:" + email; + } + + private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie("refresh_token", refreshToken); + cookie.setHttpOnly(true); // 자바 스크립트에서 접근 불가 + cookie.setSecure(true); // HTTPS에서만 동작 + cookie.setPath("/"); // 모든 경로에서 유효 + cookie.setMaxAge(refreshTokenMaxAge); + response.addCookie(cookie); // 쿠키를 응답에 추가 + } + + public void removeRefreshTokenCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("refresh_token", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + + public String extractRefreshTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("refresh_token".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } +} diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java index 114b503..ce004a2 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -1,99 +1,60 @@ package com.example.gamemate.domain.auth.service; -import com.example.gamemate.domain.auth.dto.LocalLoginResponseDto; +import com.example.gamemate.domain.auth.dto.LoginTokenResponseDto; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.repository.UserRepository; import com.example.gamemate.global.provider.JwtTokenProvider; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.time.Duration; @Service @RequiredArgsConstructor @Transactional public class TokenService { - private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; + private final StringRedisTemplate tokenBlacklistRedisTemplate; - // 블랙리스트 저장 - private final Set blacklist = new ConcurrentHashMap().newKeySet(); - private final Map tokenExpirations = new ConcurrentHashMap<>(); - - public LocalLoginResponseDto generateLoginTokens(User user, HttpServletResponse response) { + public LoginTokenResponseDto generateLoginTokens(User user, HttpServletResponse response) { String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); - user.updateRefreshToken(refreshToken); - userRepository.save(user); - - addRefreshTokenToCookie(response, refreshToken); - return new LocalLoginResponseDto(accessToken); - } - - public String extractRefreshTokenFromCookie(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if ("refresh_token".equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - return null; - } - - private void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) { - Cookie cookie = new Cookie("refresh_token", refreshToken); - cookie.setHttpOnly(true); // 자바 스크립트에서 접근 불가 - cookie.setSecure(true); // HTTPS에서만 동작 - cookie.setPath("/"); // 모든 경로에서 유효 - cookie.setMaxAge(3 * 24 * 60 * 60); // 3일 - response.addCookie(cookie); // 쿠키를 응답에 추가 - } - - public void removeRefreshTokenCookie(HttpServletResponse response) { - Cookie cookie = new Cookie("refresh_token", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - response.addCookie(cookie); + refreshTokenService.saveRefreshToken(user.getEmail(), refreshToken, response); + return new LoginTokenResponseDto(accessToken); } public void blacklistToken(String token) { long expirationTime = jwtTokenProvider.getExpirationFromToken(token); - blacklist.add(token); - tokenExpirations.put(token, expirationTime); - removeExpiredTokens(); + Duration ttl = Duration.ofMillis(expirationTime - System.currentTimeMillis()); + if (!ttl.isNegative()) { + tokenBlacklistRedisTemplate.opsForValue().set(getBlacklistKey(token), "1", ttl); + } } - public boolean isBlacklisted(String token) { - removeExpiredTokens(); - return blacklist.contains(token); + public boolean validateToken(String token) { + return !isBlacklisted(token) && jwtTokenProvider.validateToken(token); } - public boolean validateToken(String token) { - if (isBlacklisted(token)) { - return false; + public String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); } - return jwtTokenProvider.validateToken(token); + return null; } - private void removeExpiredTokens() { - long currentTime = System.currentTimeMillis(); - tokenExpirations.entrySet().removeIf(entry -> { - if (entry.getValue() < currentTime) { - blacklist.remove(entry.getKey()); - return true; - } - return false; - }); + private boolean isBlacklisted(String token) { + return Boolean.TRUE.equals(tokenBlacklistRedisTemplate.hasKey(getBlacklistKey(token))); } + private String getBlacklistKey(String token) { + return "blacklist:" + token; + } } diff --git a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java index 2729e5a..54052b7 100644 --- a/src/main/java/com/example/gamemate/domain/user/controller/UserController.java +++ b/src/main/java/com/example/gamemate/domain/user/controller/UserController.java @@ -90,7 +90,7 @@ public ResponseEntity withdraw( HttpServletRequest request, HttpServletResponse response ) { - userService.withdrawUser(customUserDetails.getUser()); + userService.withdrawUser(customUserDetails.getUser(), request, response); authService.logout(customUserDetails.getUser(), request, response); return new ResponseEntity<>(HttpStatus.NO_CONTENT); diff --git a/src/main/java/com/example/gamemate/domain/user/entity/User.java b/src/main/java/com/example/gamemate/domain/user/entity/User.java index eeb8248..ea78adf 100644 --- a/src/main/java/com/example/gamemate/domain/user/entity/User.java +++ b/src/main/java/com/example/gamemate/domain/user/entity/User.java @@ -43,8 +43,6 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private UserStatus userStatus; - private String refreshToken; - @Enumerated(EnumType.STRING) private AuthProvider provider; @@ -70,7 +68,6 @@ public User(String email, String name, String nickname, String password) { this.role = Role.USER; this.isPremium = false; this.userStatus = UserStatus.ACTIVE; - this.refreshToken = null; } // OAuth용 생성자 @@ -98,14 +95,6 @@ public void updateUserStatus(UserStatus status) { this.userStatus = status; } - public void updateRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - } - - public void removeRefreshToken() { - this.refreshToken = null; - } - public void integrateOAuthProvider(AuthProvider provider, String providerId) { this.provider = provider; this.providerId = providerId; diff --git a/src/main/java/com/example/gamemate/domain/user/service/UserService.java b/src/main/java/com/example/gamemate/domain/user/service/UserService.java index 48b2422..4c69d4c 100644 --- a/src/main/java/com/example/gamemate/domain/user/service/UserService.java +++ b/src/main/java/com/example/gamemate/domain/user/service/UserService.java @@ -8,6 +8,8 @@ import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import com.example.gamemate.global.provider.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -90,20 +92,14 @@ public void updatePassword(Long id, String oldPassword, String newPassword, User * 사용자의 탈퇴 요청을 처리합니다. * @param loginUser 현재 로그인한 사용자 */ - public void withdrawUser(User loginUser) { + public void withdrawUser(User loginUser, HttpServletRequest request, HttpServletResponse response) { loginUser.updateUserStatus(UserStatus.WITHDRAW); - loginUser.removeRefreshToken(); - userRepository.save(loginUser); - } + authService.logout(loginUser, request, response); -// private void validateToken(String token) { -// if(!jwtTokenProvider.validateToken(token)) { -// throw new ApiException(ErrorCode.INVALID_TOKEN); -// } -// } + } /** * 사용자가 일치하는지 확인합니다. diff --git a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java index 935c841..6982669 100644 --- a/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java +++ b/src/main/java/com/example/gamemate/global/config/auth/OAuth2SuccessHandler.java @@ -1,9 +1,8 @@ package com.example.gamemate.global.config.auth; +import com.example.gamemate.domain.auth.dto.LoginTokenResponseDto; +import com.example.gamemate.domain.auth.service.TokenService; import com.example.gamemate.domain.user.entity.User; -import com.example.gamemate.domain.user.repository.UserRepository; -import com.example.gamemate.global.provider.JwtTokenProvider; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -21,9 +20,7 @@ @RequiredArgsConstructor public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final JwtTokenProvider jwtTokenProvider; - private final UserRepository userRepository; - private int refreshTokenMaxAge = 60 * 60 * 24 * 3; //3일 + private final TokenService tokenService; @Value("${oauth2.success.redirect-uri}") private String successRedirectUri; @@ -42,23 +39,17 @@ public void onAuthenticationSuccess( User user = userDetails.getUser(); try { - String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); - String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); - - user.updateRefreshToken(refreshToken); - userRepository.save(user); - - // 쿠키에 Refresh 토큰 저장 - addRefreshTokenCookie(response, refreshToken); + // 토큰 생성 및 저장 + LoginTokenResponseDto tokenDto = tokenService.generateLoginTokens(user, response); String targetUrl; if("OAUTH2_USER".equals(user.getPassword())) { targetUrl = UriComponentsBuilder.fromUriString(passwordSetupRedirectUri) - .queryParam("token", accessToken) + .queryParam("token", tokenDto.getAccessToken()) .build(false).toUriString(); } else { targetUrl = UriComponentsBuilder.fromUriString(successRedirectUri) - .queryParam("token", accessToken) + .queryParam("token", tokenDto.getAccessToken()) .build(false).toUriString(); } @@ -68,13 +59,4 @@ public void onAuthenticationSuccess( throw new IOException("OAuth2 인증 처리 중 오류가 발생했습니다.", e); } } - - private void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) { - Cookie cookie = new Cookie("refresh_token", refreshToken); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setPath("/"); - cookie.setMaxAge(refreshTokenMaxAge); - response.addCookie(cookie); - } } diff --git a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java index 9bfdf2c..267cf9e 100644 --- a/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java +++ b/src/main/java/com/example/gamemate/global/provider/JwtTokenProvider.java @@ -24,8 +24,8 @@ public class JwtTokenProvider { @Value("${jwt.access-token.expiration:3600000}") private int accessTokenExpirationMs; //60분 - @Value("${jwt.refresh-token.expiration:259200000}") - private int refreshTokenExpirationMs; //3일 + @Value("${jwt.refresh-token.expiration:604800000}") + private int refreshTokenExpirationMs; //7일 public String createAccessToken(String email, Role role) { Claims claims = Jwts.claims().setSubject(email); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e9035a0..b8b282f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -93,3 +93,7 @@ spring.jpa.properties.jakarta.persistence.lock.timeout=3000 # Redis spring.data.redis.host=localhost spring.data.redis.port=6379 + +# Jwt Token +jwt.access-token.expiration=3600000 +jwt.refresh-token.expiration=604800000 \ No newline at end of file From 617f424abda2b9fd72a43c3c3eac075182a2e8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 18:32:42 +0900 Subject: [PATCH 203/215] =?UTF-8?q?fix:=20RedisConfig=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/global/config/RedisConfig.java | 119 ++++++++++-------- src/main/resources/application.properties | 12 +- 2 files changed, 69 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index ea46df7..1c914a4 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -1,10 +1,9 @@ package com.example.gamemate.global.config; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; @@ -15,94 +14,104 @@ @Configuration public class RedisConfig { - @Value("${spring.data.redis.host}") - private String redisHost; - - @Value("${spring.data.redis.port}") - private int redisPort; + // 기본 RedisConnectionFactory + @Bean + @Primary + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(0); + return new LettuceConnectionFactory(config); + } - // LettuceConnectionFactory를 DB별로 하나씩 생성하여 Bean으로 관리 - // DB 0: 알림 + // 기본 RedisTemplate @Bean @Primary - public LettuceConnectionFactory notificationConnectionFactory() { - return createLettuceConnectionFactory(0); + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + return template; } - // DB 1: 조회수 + // DB 1: 알림 @Bean - public LettuceConnectionFactory viewCountConnectionFactory() { - return createLettuceConnectionFactory(1); + public RedisConnectionFactory notificationConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(1); + return new LettuceConnectionFactory(config); } - // DB 2: 리프레시 토큰 + // DB 2: 조회수 @Bean - public LettuceConnectionFactory refreshTokenConnectionFactory() { - return createLettuceConnectionFactory(2); + public RedisConnectionFactory viewCountConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(2); + return new LettuceConnectionFactory(config); } - // DB 3: 토큰 블랙리스트 + // DB 3: 리프레시 토큰 @Bean - public LettuceConnectionFactory tokenBlacklistConnectionFactory() { - return createLettuceConnectionFactory(3); + public RedisConnectionFactory refreshTokenConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(3); + return new LettuceConnectionFactory(config); } - // DB 4: 쿠폰 + // DB 4: 토큰 블랙리스트 @Bean - public LettuceConnectionFactory couponConnectionFactory() { - return createLettuceConnectionFactory(4); + public RedisConnectionFactory blacklistConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(4); + return new LettuceConnectionFactory(config); } - // 공통: LettuceConnectionFactory 생성 - private LettuceConnectionFactory createLettuceConnectionFactory(int database) { - RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); - configuration.setHostName(redisHost); - configuration.setPort(redisPort); - configuration.setDatabase(database); - LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration); - connectionFactory.afterPropertiesSet(); - return connectionFactory; + // DB 5: 쿠폰 + @Bean + public RedisConnectionFactory couponConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setDatabase(5); + return new LettuceConnectionFactory(config); } - // 알림 RedisTemplate (DB 0) + // 알림 RedisTemplate (DB 1) @Bean - @Primary - public RedisTemplate notificationRedisTemplate( - @Qualifier("notificationConnectionFactory") LettuceConnectionFactory connectionFactory) { + public RedisTemplate notificationRedisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setConnectionFactory(notificationConnectionFactory()); + + // String 타입을 위한 직렬화 설정 redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + return redisTemplate; } - // 조회수 RedisTemplate (DB 1) + // 조회수 RedisTemplate (DB 2) @Bean - public StringRedisTemplate viewCountRedisTemplate( - @Qualifier("viewCountConnectionFactory") LettuceConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); + public StringRedisTemplate viewCountRedisTemplate() { + return new StringRedisTemplate(viewCountConnectionFactory()); } - // 리프레시 토큰 RedisTemplate (DB 2) + // 리프레시 토큰 RedisTemplate (DB 3) @Bean - public StringRedisTemplate refreshTokenRedisTemplate( - @Qualifier("refreshTokenConnectionFactory") LettuceConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); + public StringRedisTemplate refreshTokenRedisTemplate() { + return new StringRedisTemplate(refreshTokenConnectionFactory()); } - // 토큰 블랙리스트 RedisTemplate (DB 3) + // 토큰 블랙리스트 RedisTemplate (DB 4) @Bean - public StringRedisTemplate tokenBlacklistRedisTemplate( - @Qualifier("tokenBlacklistConnectionFactory") LettuceConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); + public StringRedisTemplate blacklistRedisTemplate() { + return new StringRedisTemplate(blacklistConnectionFactory()); } - // 쿠폰 RedisTemplate (DB 4) + // 쿠폰 RedisTemplate (DB 5) @Bean - public StringRedisTemplate couponRedisTemplate( - @Qualifier("couponConnectionFactory") LettuceConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); + public StringRedisTemplate couponRedisTemplate() { + return new StringRedisTemplate(couponConnectionFactory()); } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 06c97e8..5556976 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -62,15 +62,13 @@ spring.mail.properties.mail.smtp.starttls.enable=true # DEBUG logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.security.oauth2=TRACE logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web.servlet=DEBUG -logging.level.org.springframework.security.oauth2=TRACE -logging.level.org.springframework.web.client=TRACE +logging.level.org.springframework.web.client=INFO logging.level.com.example.gamemate=DEBUG -logging.level.org.springframework.data.redis=DEBUG -logging.level.com.example.gamemate.domain.notification.service.RedisStreamService=DEBUG -logging.level.org.springframework.data.redis.core=DEBUG -logging.level.io.lettuce.core=DEBUG +logging.level.org.springframework.data.redis=INFO +logging.level.com.example.gamemate.domain.notification.service.RedisStreamService=INFO # Gemini gemini.api.url=${GEMINI_URL} @@ -96,4 +94,4 @@ spring.jpa.properties.jakarta.persistence.lock.timeout=3000 # Redis spring.data.redis.host=${REDIS_HOST} -spring.data.redis.port=6379 +spring.data.redis.port=6379 \ No newline at end of file From 26f3492ceb16535a2210118f3f6c14c51ffb54ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 18:43:29 +0900 Subject: [PATCH 204/215] =?UTF-8?q?fix:=20properties=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-noreactive.properties | 1 - src/main/resources/application.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/main/resources/application-noreactive.properties diff --git a/src/main/resources/application-noreactive.properties b/src/main/resources/application-noreactive.properties deleted file mode 100644 index 29df9aa..0000000 --- a/src/main/resources/application-noreactive.properties +++ /dev/null @@ -1 +0,0 @@ -spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f8244b9..8857447 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=gamemate -spring.config.import=optional:file:.env[.properties],optional:classpath:/application-noreactive.properties +spring.config.import=optional:file:.env[.properties] # Environment (prod, dev) spring.profiles.active=dev From 554177d60c90d8c8ca1b3da406e0688bf05d4b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Wed, 5 Feb 2025 19:22:23 +0900 Subject: [PATCH 205/215] =?UTF-8?q?fix:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=97=90=20@Qualifier=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/RefreshTokenService.java | 8 +++++++- .../gamemate/domain/auth/service/TokenService.java | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java index 28395be..890884b 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/RefreshTokenService.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,7 +11,6 @@ import java.time.Duration; @Service -@RequiredArgsConstructor @Transactional public class RefreshTokenService { @@ -18,6 +18,12 @@ public class RefreshTokenService { private final Duration REFRESH_TOKEN_TTL = Duration.ofDays(7); private int refreshTokenMaxAge = 60 * 60 * 24 * 7; // 7일 + public RefreshTokenService( + @Qualifier("refreshTokenRedisTemplate") StringRedisTemplate refreshTokenRedisTemplate + ) { + this.refreshTokenRedisTemplate = refreshTokenRedisTemplate; + } + public void saveRefreshToken(String email, String refreshToken, HttpServletResponse response) { String key = getKey(email); refreshTokenRedisTemplate.opsForValue().set(key, refreshToken, REFRESH_TOKEN_TTL); diff --git a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java index ce004a2..21fce38 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/TokenService.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,7 +15,6 @@ import java.time.Duration; @Service -@RequiredArgsConstructor @Transactional public class TokenService { @@ -22,6 +22,15 @@ public class TokenService { private final RefreshTokenService refreshTokenService; private final StringRedisTemplate tokenBlacklistRedisTemplate; + public TokenService( + JwtTokenProvider jwtTokenProvider, + RefreshTokenService refreshTokenService, + @Qualifier("blacklistRedisTemplate") StringRedisTemplate tokenBlacklistRedisTemplate) { + this.jwtTokenProvider = jwtTokenProvider; + this.refreshTokenService = refreshTokenService; + this.tokenBlacklistRedisTemplate = tokenBlacklistRedisTemplate; + } + public LoginTokenResponseDto generateLoginTokens(User user, HttpServletResponse response) { String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole()); String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); From 0112ba043289b4fd817bfd7ea2af3618a8642a63 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Wed, 5 Feb 2025 22:47:05 +0900 Subject: [PATCH 206/215] =?UTF-8?q?docs=20:=20=EC=9A=B4=EC=98=81=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20properties=20=EC=9D=98=20ddl-auto=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.properties | 2 +- src/main/resources/application-prod.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index e101fa9..28d1453 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,7 +1,7 @@ spring.datasource.url=${MYSQL_DEV_URL} spring.datasource.username=${MYSQL_USERNAME} spring.datasource.password=${MYSQL_PASSWORD} -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=${JPA_HIBERNATE_DDL_DEV} spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always spring.sql.init.data-locations=classpath:data-dev.sql \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 5e88577..c73285f 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -1,6 +1,6 @@ spring.datasource.url=${MYSQL_PROD_URL} spring.datasource.username=${MYSQL_USERNAME} spring.datasource.password=${MYSQL_PASSWORD} -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=${JPA_HIBERNATE_DDL_PROD} spring.jpa.show-sql=false spring.sql.init.mode=never \ No newline at end of file From d015ef6c15614f03cefff6528701babb83b96ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 6 Feb 2025 12:02:08 +0900 Subject: [PATCH 207/215] =?UTF-8?q?fix:=20Redis=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=85=BC=EB=A6=AC?= =?UTF-8?q?=EC=A0=81=20=EB=B6=84=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemate/domain/coupon/entity/Coupon.java | 11 ++++ .../domain/coupon/service/CouponService.java | 56 +++++++++---------- .../global/config/CouponDataSynchronizer.java | 48 ---------------- .../gamemate/global/config/RedisConfig.java | 13 ++++- 4 files changed, 46 insertions(+), 82 deletions(-) delete mode 100644 src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java diff --git a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java index b364572..25e7645 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/gamemate/domain/coupon/entity/Coupon.java @@ -30,6 +30,9 @@ public class Coupon extends BaseEntity { @Column(nullable = false) private Integer totalQuantity; + @Column(nullable = false) + private Integer issuedQuantity = 0; + @Column(nullable = false) private LocalDateTime startAt; @@ -52,5 +55,13 @@ public boolean isIssuable() { LocalDateTime now = LocalDateTime.now(); return now.isAfter(startAt) && now.isBefore(expiredAt); } + + public boolean isExhausted() { + return issuedQuantity >= totalQuantity; + } + + public void incrementIssuedQuantity() { + this.issuedQuantity++; + } } diff --git a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java index 261edbc..cd4d5ab 100644 --- a/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/gamemate/domain/coupon/service/CouponService.java @@ -9,11 +9,11 @@ import com.example.gamemate.domain.coupon.repository.UserCouponRepository; import com.example.gamemate.domain.user.entity.User; import com.example.gamemate.domain.user.enums.Role; +import com.example.gamemate.global.common.aop.DistributedLock; import com.example.gamemate.global.constant.ErrorCode; import com.example.gamemate.global.exception.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,38 +50,32 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto requestDto, U return new CouponCreateResponseDto(savedCoupon); } + @DistributedLock(key = "'LOCK:coupon:' + #couponId") public CouponIssueResponseDto issueCoupon(Long couponId, User loginUser) { - try { - Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId) - .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); - - // 발급 가능 체크 - if (!coupon.isIssuable()) { - throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); - } - - // 수량 체크 - if (coupon.isExhausted()) { - throw new ApiException(ErrorCode.COUPON_EXHAUSTED); - } - - // 중복 발급 체크 - if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { - throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); - } - - // 쿠폰 발급 - coupon.incrementIssuedQuantity(); - UserCoupon userCoupon = new UserCoupon(loginUser, coupon); - userCoupon.updateIsUsed(false); - - UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); - - return new CouponIssueResponseDto(savedUserCoupon); - } catch (PessimisticLockingFailureException e) { - log.error("쿠폰 발급 동시 요청으로 잠금 획득 실패: {}", couponId, e); - throw new ApiException(ErrorCode.COUPON_ISSUE_FAILED); + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new ApiException(ErrorCode.COUPON_NOT_FOUND)); + + // 발급 가능 체크 + if (!coupon.isIssuable()) { + throw new ApiException(ErrorCode.COUPON_NOT_ISSUABLE); + } + + // 중복 발급 체크 + if (userCouponRepository.existsByUserIdAndCouponId(loginUser.getId(), couponId)) { + throw new ApiException(ErrorCode.COUPON_ALREADY_ISSUED); } + + // 수량 체크 + if (coupon.isExhausted()) { + throw new ApiException(ErrorCode.COUPON_EXHAUSTED); + } + + // 쿠폰 발급 + coupon.incrementIssuedQuantity(); + UserCoupon userCoupon = new UserCoupon(loginUser, coupon); + UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon); + + return new CouponIssueResponseDto(savedUserCoupon); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java b/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java deleted file mode 100644 index 290dda0..0000000 --- a/src/main/java/com/example/gamemate/global/config/CouponDataSynchronizer.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.gamemate.global.config; - -import com.example.gamemate.domain.coupon.entity.Coupon; -import com.example.gamemate.domain.coupon.repository.CouponRepository; -import com.example.gamemate.domain.coupon.repository.UserCouponRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -public class CouponDataSynchronizer { - - private static final String COUPON_STOCK_KEY = "coupon:%d:stock"; - - private final CouponRepository couponRepository; - private final UserCouponRepository userCouponRepository; - private final StringRedisTemplate redisTemplate; - - public CouponDataSynchronizer( - CouponRepository couponRepository, - UserCouponRepository userCouponRepository, - @Qualifier("couponRedisTemplate") StringRedisTemplate redisTemplate) { - this.couponRepository = couponRepository; - this.userCouponRepository = userCouponRepository; - this.redisTemplate = redisTemplate; - } - - @EventListener(ApplicationReadyEvent.class) - public void syncCouponStock() { - List coupons = couponRepository.findAll(); - for (Coupon coupon : coupons) { - long issuedCount = userCouponRepository.countByCouponId(coupon.getId()); - long remainingStock = coupon.getTotalQuantity() - issuedCount; - redisTemplate.opsForValue().set(getCouponStockKey(coupon.getId()), - String.valueOf(remainingStock)); - } - } - - - private String getCouponStockKey(Long couponId) { - return String.format(COUPON_STOCK_KEY, couponId); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 1c914a4..e51fd28 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -1,5 +1,8 @@ package com.example.gamemate.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; import org.springframework.context.annotation.Primary; @@ -109,9 +112,13 @@ public StringRedisTemplate blacklistRedisTemplate() { return new StringRedisTemplate(blacklistConnectionFactory()); } - // 쿠폰 RedisTemplate (DB 5) + // 쿠폰 RedissonClient (DB 5) @Bean - public StringRedisTemplate couponRedisTemplate() { - return new StringRedisTemplate(couponConnectionFactory()); + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://127.0.0.1:6379") + .setDatabase(5); + return Redisson.create(config); } } \ No newline at end of file From b35775560690c2df51d55f496036f1405826e1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Thu, 6 Feb 2025 15:07:44 +0900 Subject: [PATCH 208/215] =?UTF-8?q?fix:=20couponConnectionFactory=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/gamemate/global/config/RedisConfig.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index e51fd28..63ab68a 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -71,14 +71,6 @@ public RedisConnectionFactory blacklistConnectionFactory() { return new LettuceConnectionFactory(config); } - // DB 5: 쿠폰 - @Bean - public RedisConnectionFactory couponConnectionFactory() { - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); - config.setDatabase(5); - return new LettuceConnectionFactory(config); - } - // 알림 RedisTemplate (DB 1) @Bean public RedisTemplate notificationRedisTemplate() { From c3d62880e3b8c99f144505b0da80f8ce61a1f2d3 Mon Sep 17 00:00:00 2001 From: Newbiekk Date: Thu, 6 Feb 2025 15:12:50 +0900 Subject: [PATCH 209/215] =?UTF-8?q?docs=20:=20README.md=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 프로젝트 기능, 적용 기술, 기술적 의사결정, 트러블슈팅 내용 추가 --- README.md | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 290 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4524e7b..0166f92 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ |이예지|전수연|고강혁|양제훈| |:---:|:---:|:---:|:---:| +|[@yeji-world](https://github.com/yeji-world)|[@sumyeom](https://github.com/sumyeom)|[@Newbiekk](https://github.com/Newbiekk-kkh)|[@89JHoon](https://github.com/89JHoon)| |BE|BE|BE|BE|
@@ -27,36 +28,316 @@ ### 👥 게임 매칭 시스템 제공 -> * 원하는 게임에 대해 매칭 시스템을 통해 친구를 구할 수 있습니다. -> * 매칭이 완료되면 스케줄링을 통해 일정 시간 후 알림을 제공합니다. +> * 현재 '리그오브레전드' 라는 게임에 대해 매칭 시스템을 통해 친구를 구할 수 있습니다. +> * 내 정보를 입력하고, 원하는 상대방의 조건을 입력하여 매칭 로직을 통해 최대 5인의 추천을 받을 수 있습니다. ### 🎮 게임 정보 확인 -> * 다양한 게임에 대한 정보를 얻을 수 있고 리뷰를 확인 할 수 있습니다. +> * 다양한 게임에 대한 정보를 얻을 수 있고, 각 게임에 대해 사용자들이 작성한 리뷰를 확인 할 수 있습니다. ### ❗️ 게임 추천 > * 내가 원하는 성향을 가진 게임을 추천하는 서비스를 제공합니다. -> * Gemini API 사용 +> * Gemini API 사용해 사용자가 입력한 내용을 기반으로 게임을 추천해줍니다. + ### 📖 게임 커뮤니티 > * 게임에 대한 게시글을 작성하고, 게시글에 댓글 대댓글을 작성할 수 있습니다. +> * 조회수 상위 5개의 게시글을 오늘의 게시글로 선정해 사용자에게 제공합니다. + + +### 👨‍👩‍👦 소셜 기능 + +> * 사용자간의 팔로우 기능을 제공합니다. +> * 게시물 / 게임 리뷰에 '좋아요' 또는 '싫어요'를 달 수 있습니다. + +### 🔔 실시간 알림 기능 + +> * 알림은 SSE를 통해 실시간으로 제공됩니다. +> * 매칭 / 팔로우 / 댓글 / 좋아요 등의 이벤트에 대해 알림이 제공됩니다. + +### 🎫 쿠폰 기능 + +> * 자체 쿠폰을 발급해, 사용자들에게 여러 혜택들을 제공합니다. + +

+ + +## 📚 적용 기술 + +| 분야 | **기술 및 도구** | **목적** | +| --- | --- | --- | +| **애플리케이션 개발** | JDK 17
Spring Boot 3.4.1
IntelliJ IDEA | 1️⃣ **JDK 17**
- 성능, 보안, 개발 효율성을 위한 안정적인 운영 환경
2️⃣ **Spring Boot 3.4.1**
- 프로덕션 환경에서의 안정성과 클라우드 네이티브 기능 강화
3️⃣ **IntelliJ IDEA**
- 개발자 생산성을 극대화하는 강력한 통합 개발 환경(IDE) | +| **인증 / 인가** | Spring Security
JWT
OAuth 2.0 | 1️⃣ **Spring Security**
- 웹 애플리케이션 보안 통합 관리
2️⃣ **JWT**
- 무상태(Stateless) 인증 구현
3️⃣ **OAuth 2.0**
- 제3자 서비스의 안전한 자원 접근 관리 | +| **협업** | Git
GitHub
Slack
Notion
GitHub Project / Issue
Trello
WBS | 1️⃣ **Git & GitHub**
- 코드 협업과 버전 관리
2️⃣ **Slack & Notion**
- 진행 사항에 대해 소통하고, 문서화
3️⃣ **GitHub Project / Issue & Trello & WBS**
- 요구 사항에 대한 일정 및 우선순위 파악, 정리
- 각 일정에 대한 진행 사항 체크 | +| **Database** | RDS(MySQL)
Redis | 1️⃣ **RDS(MySQL)**
- RDS를 통해 DB를 위한 인프라 구축
- 안정적인 관계형 DB로 데이터 관리
2️⃣ **Redis**
- 게시글 조회수 카운팅을 캐시를 활용하여 효과적으로 관리
- Redisson 기반 분산 락을 통해 데이터 일관성 유지
- Redis Stream을 사용해 다중 WAS 환경에서도 알림이 유실되지 않고 전달되도록 관리 | +| **파일 첨부** | AWS S3 | 1️⃣ **AWS S3**
- 대용량 데이터를 안정적으로 유지 관리 | +| **CI/CD** | Docker
GitHub Action
AWS EC2
AWS | 1️⃣ **Docker**
- 일관된 개발 환경 제공
- 배포 및 확장이 용이
2️⃣ **GitHub Action**
- CI/CD 자동화를 구현하고, 배포 프로세스를 효율적으로 관리
3️⃣ **AWS**
- 클라우드 환경에서 안정적인 서비스 운영 | +| **외부 API** | Gemini API
Google OAuth 2.0 API
Kakao OAuth 2.0 API
JavaMail | 1️⃣ **Gemini API**
- 신뢰성 있는 게임 추천을 위한 API
2️⃣ **OAuth 2.0 API (Google & Kakao)**
- 소셜 로그인을 위한 API
3️⃣ **JavaMail**
- 메일 발송을 위한 API |

+## 📝 기술적 의사 결정 + +
+ Gemini AI 활용 방안 + +## 1. 배경 +- 게임 추천 시스템은 단순한 게임 선택을 넘어 **사용자 경험 설계**부터 **윤리적 검증**까지 종합적인 접근이 필요함. +- 플레이어의 **세션 데이터**를 지속적으로 학습에 반영하면서도, 추천의 **근거를 투명하게 제시**하는 것이 장기적 신뢰 확보의 핵심. +- AI를 이용해 게임을 추천하려면 다음 요소들을 종합적으로 분석해야 함: + - **사용자 취향** (장르, 플레이 스타일) + - **보유 기기** (PC, 콘솔, 모바일) + - **최신 트렌드** + - **소셜 데이터** (친구, 스트리머 추천) + - **추천 알고리즘** (콘텐츠 기반, 협업 필터링) + - **할인 정보, 연령 등급** 등의 요소 고려 +- 또한, **사용자 피드백을 반영**하여 지속적으로 추천 시스템을 개선하는 것이 중요함. + +## 2. 선택 이유 +- **구글의 Gemini AI**는 **멀티모달 기능**(텍스트, 이미지, 음성 등)을 지원하며, 강력한 **자연어 처리(NLP)**와 **데이터 분석 기능**을 제공. +- Gemini AI를 활용하면 단순한 게임 추천이 아니라 **사용자와 대화하며 취향을 파악하고 맞춤형 추천을 제공하는 AI 시스템**을 구축 가능. +- 특히, **Google Cloud의 API**와 결합하면 더 정교한 추천 모델을 구축할 수 있어 지속적인 발전이 기대됨. +- 결론적으로, **Gemini AI를 활용하면 더욱 정밀하고 개인화된 게임 추천 시스템 구축**을 기대할 수 있음. + +## 3. 대안 비교 +| 대안 | 장점 | 단점 | +| --- | --- | --- | +| **Microsoft Azure OpenAI + Game Pass 데이터 연계** | - **Xbox Game Pass 데이터**와 연계 가능
- **클라우드 기반 확장성** 우수 | - **Microsoft 생태계** 내에서 활용도가 높음
- **범용적인 추천 시스템 구축에는 한계** | +| **Claude (Anthropic)** | - **긴 문맥을 유지하는 대화 능력** 우수
- **친환경적이고 안전한 AI 설계** | - **게임 추천 알고리즘 구축에는 다소 한계** | +
+ +
+ AWS S3 활용 방안 + +## 1. 배경 +- 커뮤니티에서 **첨부파일(이미지) 관리**는 여러 측면에서 중요함: + - **사용자 경험(UX) 향상**: 이미지가 포함된 게시글은 가독성을 높이고 몰입감을 제공. + - **서버 성능 최적화**: 저장 공간과 로딩 속도를 고려한 최적화 필요. + - **보안 강화**: 불법 콘텐츠 필터링 및 악성 코드 방지를 위한 보안 조치 필수. + - **검색 최적화**: SEO 최적화를 통해 접근성을 높일 수 있음. + - **모바일 대응**: 다양한 네트워크 환경에서도 원활한 사용 가능. +- 효과적인 이미지 관리를 위해 **이미지 압축, AI 필터링, CDN 활용** 등의 전략이 필요하며, 이를 잘 적용하여 **원활한 커뮤니티 운영**을 목표로 함. + +## 2. 선택 이유 +1. **대용량 이미지 저장 가능** +2. **빠른 이미지 로딩 속도** + - 전 세계 어디에서든 **빠르게 로딩 가능** + - 모바일 및 저속 인터넷 환경에서도 **원활한 서비스 제공** +3. **데이터 손실 안전 및 보안 강화** + - **자동 데이터 복제**: 여러 데이터 센터에 복제 저장하여 **데이터 손실 방지** + - **접근 제어 가능**: 퍼블릭/프라이빗 설정을 통해 특정 사용자만 접근 가능 +4. **비용 절감 효과** + - **사용량 기반 과금**으로 불필요한 비용 절감 +5. **결론** + - S3를 사용하면 **이미지 저장이 편리하고, 로딩 속도가 빠르며, 데이터 손실 위험이 낮고, 보안이 뛰어나며, 비용 절감 가능** + - 특히, **많은 사용자가 이미지를 업로드하고 조회하는 커뮤니티 게시판에 적절한 솔루션** + +## 3. 대안 비교 +| 대안 | 장점 | 단점 | +| --- | --- | --- | +| **Azure Blob Storage** | - 마이크로소프트 환경과 연계 용이 | - AWS 대비 **확장성과 글로벌 커버리지 부족**
- 관리 콘솔이 다소 복잡 | +| **Firebase Storage** | - **모바일 앱과의 연동 최적화** | - **대량 이미지 저장보다는 모바일 앱 중심**
- **데이터 관리 기능이 제한적** | +
+ +
+ SSE(Server-Sent Events) 기반의 실시간 알림 시스템 + +## 1. SSE 도입 배경 +- 초기 알림 기능은 **스케줄러를 활용하여 5분마다 미확인된 알림을 이메일로 전송**하는 방식으로 구현됨. +- 그러나 이러한 방식은 **실시간성이 부족하여 즉각적인 알림 제공이 불가능**했음. +- 이를 개선하기 위해 **SSE(Server-Sent Events) 기반의 실시간 알림 시스템**을 도입하게 됨. + +## 2. SSE 선택 이유 +- 실시간 알림을 구현하기 위해 **Short Polling, Long Polling, SSE, WebSocket** 등 다양한 기술을 검토한 결과, **SSE가 가장 적합**하다고 판단됨. +- **SSE(Server-Sent Events) 장점** + ✅ **단방향(one-way) 연결**을 통해 **서버에서 클라이언트로 실시간 알림 전송** + ✅ 기존 **HTTP 프로토콜**을 활용하므로 **설정이 간편** + ✅ WebSocket과 달리 **재연결(자동 복구) 기능 내장** + ✅ 알림과 같이 **단방향 전송이 주를 이루는 서비스에 적합** + +## 3. 대안 비교 + +| 기술 | 동작 방식 | 장점 | 단점 | +| --- | --- | --- | --- | +| **Short Polling** | 클라이언트가 일정 주기마다 서버에 요청 | 구현이 간단함 | 불필요한 요청 증가로 리소스 낭비 | +| **Long Polling** | 서버가 새로운 데이터가 있을 때까지 응답을 지연 | 실시간성 개선 | 다수의 연결 유지 시 서버 부담 증가 | +| **WebSocket** | 클라이언트-서버 간 **양방향 연결 유지** | 쌍방향 통신 가능 | 설정이 복잡하며, HTTP/2 환경에서 오버헤드 증가 가능 | + +## 4. 결론 +- 알림 서비스는 **서버에서 클라이언트로 단방향 메시지 전송이 주된 역할**을 함. +- WebSocket의 **양방향 연결 기능이 불필요**하며, 구현이 간단한 **SSE가 최적의 선택**이었음. +
+
+ 알림 시스템에 Redis Stream 적용 -## 적용 기술 +## 메시지 영속성과 신뢰성 확보 +* 기존 SSE만 사용할 경우 네트워크 문제나 서버 장애 시 알림이 유실될 수 있음 +* Redis Stream은 알림 메시지를 일시적으로 저장하고 관리하여 메시지 전달의 신뢰성을 높임 +## 장애 상황 대응 +* 사용자의 네트워크 연결이 끊기거나 서버에 문제가 생겼을 때도 Stream에 메시지가 보관되어 있어 재접속 시 미전송된 알림을 처리할 수 있음 + +## 시스템 확장성 + +* 향후 시스템이 확장되어 다중 WAS 환경에서 알림을 처리해야 할 경우 Redis Stream을 통해 메시지 큐 역할을 수행하여 분산 시스템에서도 안정적인 알림 처리가 가능 + +## 대안 비교 + +| 기술 | 동작 방식 | 장점 | 단점 | +|------|-----------|------|------| +| **Redis Pub/Sub** | 구독자가 있으면 실시간으로 메시지 전송 | 빠른 실시간 메시징 | 구독자가 없으면 메시지 유실 | +| **Kafka** | 로그 기반 스트리밍 | 강력한 메시지 보장, 대량 데이터 처리 가능 | 설정이 복잡하고 운영 비용이 높음 | +| **Redis Stream** | 메시지 저장 + 스트리밍 | 메시지 유실 방지, 소비자 그룹 지원 | Pub/Sub보다 약간의 설정 필요 | + +## 결론 + +알림 서비스에서는 **메시지 유실 방지가 중요**하므로, 단순 Pub/Sub보다 **Redis Stream이 적합**했습니다. Kafka는 강력한 기능을 제공하지만, 운영 복잡성과 비용 문제 및 대량 데이터 처리가 현재 레벨에서 필요하지 않을 것으로 예상되어 오버 엔지니어링 같았습니다. 이러한 이유들로 Redis Stream을 선택했습니다.

+
+ ## 🚨 Trouble Shooting +
+ 게임추천- 프롬프트 관련 보안 위험 + + ## 문제인식 + +게임 추천 기능중 사용자에게 데이터를 응답 받는 중 SQL 인젝션 위험이 예상됨 + +- **보안 위험 분석** + + ```java + String prompt = String.format("...추가적인 요청은 %s 야", + userGamePreference.getExtraRequest()); + ``` + + - **문제점**: **`extraRequest`**가 프롬프트에 직접 삽입되어 악성 코드 실행 가능 + - **위험**: 프롬프트 조작을 통한 시스템 명령어 주입, 데이터 유출 등의 공격 가능성 + +- **개선 방안** + + - **OWASP 인코딩 라이브러리 적용** + + ```java + import org.owasp.encoder.Encode; + + String safeExtraRequest = Encode.forJava(extraRequest); + ``` + + - **기능**: 특수 문자 자동 이스케이프 + - **예방 공격**: XSS, 명령어 주입 + - **Bean Validation 통합** + + ```java + public class UserGamePreferenceRequestDto { + + @Size(max = 100, message = "추가 요청은 100자 이내로 입력해주세요") + @Pattern(regexp = "^[a-zA-Z0-9\\s]+$", message = "특수 문자는 사용할 수 없습니다") + private String extraRequest; + } + + public class GameRecommendContorller { + @PostMapping + public ResponseEntity createUserGamePreference( + @Valid @RequestBody UserGamePreferenceRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + ``` + + - **장점**: 선언적 검증 규칙 관리 + +- **기대 효과** + + - 입력 데이터의 무결성과 유효성이 크게 향상되어, 악의적인 데이터 주입 시도를 사전에 차단할 수 있음 + - SQL Injection과 같은 데이터베이스 공격 위험이 현저히 감소하여, 데이터베이스의 보안성이 향상 + - 사용자 경험 측면에서도 개선이 이루어져, 유효하지 않은 데이터 입력에 대한 즉각적인 피드백을 제공함으로써 사용자 친화적인 인터페이스를 구현할 수 있음 + +
+ +
+ 특정 유저의 팔로워 조회 시 성능 최적화 + + ## 🔍 기존 코드 문제점 + +```java +public List findFollowers(String email) { + User followee = userRepository.findByEmail(email) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + if (followee.getUserStatus() == UserStatus.WITHDRAW) { + throw new ApiException(ErrorCode.IS_WITHDRAWN_USER); + } // 탈퇴한 회원 예외 처리 + + List followListByFollowee = followRepository.findByFollowee(followee); + + List followersByFollowee = followListByFollowee.stream() + .map(Follow::getFollower) + .filter(follower -> follower.getUserStatus() != UserStatus.WITHDRAW) + .toList(); + + return followersByFollowee.stream() + .map(FollowFindResponseDto::toDto) + .toList(); +} +``` + +위 코드의 **문제점은 FetchType.LAZY로 인해 N+1 문제가 발생**한다는 것입니다. + +- `Follow` 엔티티에서 `getFollower()`를 호출할 때, **각 팔로워에 대한 별도의 쿼리**가 실행됨 +- 결과적으로 팔로워가 1,000명일 경우 **총 1,002개의 쿼리**가 발생 (1개의 사용자 조회 + 1개의 팔로우 리스트 조회 + 1,000개의 팔로워 조회) + +--- + +## 💡 해결 방법: JPQL을 활용한 성능 최적화 + +이 문제를 해결하기 위해 **JPQL을 사용하여 한 번의 쿼리로 필요한 데이터를 가져오도록 수정**하였습니다. + +```java +@Query("SELECT NEW com.example.gamemate.domain.follow.dto.FollowFindResponseDto(f.follower.id, f.follower.nickname) " + + "FROM Follow f " + + "JOIN f.follower " + + "WHERE f.followee.email = :email " + + "AND f.follower.userStatus != 'WITHDRAW'") +List findFollowersDtoByFolloweeEmail(@Param("email") String email); +``` + +✅ **최적화된 코드의 장점** + +- **단 2개의 쿼리만 실행됨** → 기존 1,002개 → 2개 +- **N+1 문제 해결** → `JOIN`을 통해 한 번에 데이터 조회 +- **DTO로 직접 매핑** → 엔티티를 불필요하게 생성하지 않고 바로 DTO로 변환 + +--- + +## 📊 성능 테스트 결과 + +![image (7)](https://github.com/user-attachments/assets/0ca21e4a-33ce-4e8a-9e6c-ea3161099f99) + +| 테스트 환경 | 기존 코드 | 최적화된 코드 | 성능 개선율 | +| --- | --- | --- | --- | +| 팔로워 1,000명 기준 | **752ms** (1,002개 쿼리) | **7ms** (2개 쿼리) | **99% 성능 개선** 🚀 | + +--- + +## 🔎 결론 + +JPQL을 활용한 최적화를 통해: + +- N+1 문제를 해결하여 **실행 시간을 752ms에서 7ms로 99% 단축** +- 불필요한 쿼리를 제거하여 **DB 부하를 대폭 감소** (1,002개 → 2개) +- DTO 직접 매핑으로 **메모리 사용량 최적화** + +이러한 성능 개선을 통해 팔로워가 많은 사용자의 프로필 조회시에도 빠른 응답 속도를 보장할 수 있게 되었습니다. + +


@@ -69,7 +350,8 @@ ## 🌐 Architecture -![image](https://github.com/user-attachments/assets/bbb0be82-e50b-4d66-800f-bdd7ebc01266) +![image (8)](https://github.com/user-attachments/assets/fa1c41cc-58e4-418d-94aa-02330e0e0ba6) + ## 📆 일정 관리 (WBS) @@ -80,6 +362,8 @@ ## 📝 Technologies & Tools 📝 +#### Java 17 | SpringBoot 3.4.1 | MySql 8.0 | QueryDSL 5.0 + From 865bd3425a1f2408773bd857b21d6bbd79b8b123 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Thu, 6 Feb 2025 17:32:41 +0900 Subject: [PATCH 210/215] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 2 ++ docker-compose.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index bec24df..1d7b783 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -66,6 +66,8 @@ jobs: echo "MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }}" >> .env echo "MYSQL_DEV_URL=${{ secrets.MYSQL_DEV_URL }}" >> .env echo "JPA_HIBERNATE_DDL=${{ secrets.JPA_HIBERNATE_DDL }}" >> .env + echo "JPA_HIBERNATE_DDL_PROD=${{ secrets.JPA_HIBERNATE_DDL_PROD }}" >> .env + echo "JPA_HIBERNATE_DDL_DEV=${{ secrets.JPA_HIBERNATE_DDL_DEV }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index b2b4b6e..50b12b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: MYSQL_PROD_URL: "${MYSQL_PROD_URL}" MYSQL_DEV_URL: "${MYSQL_DEV_URL}" JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" + JPA_HIBERNATE_DDL_PROD: "${JPA_HIBERNATE_DDL_PROD}" + JPA_HIBERNATE_DDL_DEV: "${JPA_HIBERNATE_DDL_DEV}" JWT_SECRET: "${JWT_SECRET}" AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}" AWS_SECRET_KEY: "${AWS_SECRET_KEY}" From 854f22a15dc9c56e195f5fd84fb3b640672e7148 Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 7 Feb 2025 00:13:40 +0900 Subject: [PATCH 211/215] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-multi-stage-build.yml | 2 ++ docker-compose.yml | 2 ++ .../java/com/example/gamemate/global/config/RedisConfig.java | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/.github/workflows/docker-multi-stage-build.yml b/.github/workflows/docker-multi-stage-build.yml index bec24df..1d7b783 100644 --- a/.github/workflows/docker-multi-stage-build.yml +++ b/.github/workflows/docker-multi-stage-build.yml @@ -66,6 +66,8 @@ jobs: echo "MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }}" >> .env echo "MYSQL_DEV_URL=${{ secrets.MYSQL_DEV_URL }}" >> .env echo "JPA_HIBERNATE_DDL=${{ secrets.JPA_HIBERNATE_DDL }}" >> .env + echo "JPA_HIBERNATE_DDL_PROD=${{ secrets.JPA_HIBERNATE_DDL_PROD }}" >> .env + echo "JPA_HIBERNATE_DDL_DEV=${{ secrets.JPA_HIBERNATE_DDL_DEV }}" >> .env echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index b2b4b6e..50b12b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: MYSQL_PROD_URL: "${MYSQL_PROD_URL}" MYSQL_DEV_URL: "${MYSQL_DEV_URL}" JPA_HIBERNATE_DDL: "${JPA_HIBERNATE_DDL}" + JPA_HIBERNATE_DDL_PROD: "${JPA_HIBERNATE_DDL_PROD}" + JPA_HIBERNATE_DDL_DEV: "${JPA_HIBERNATE_DDL_DEV}" JWT_SECRET: "${JWT_SECRET}" AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}" AWS_SECRET_KEY: "${AWS_SECRET_KEY}" diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 63ab68a..3620f8b 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -3,6 +3,7 @@ import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -17,6 +18,9 @@ @Configuration public class RedisConfig { + @Value("${spring.data.redis.host}") + private String redisHost; + // 기본 RedisConnectionFactory @Bean @Primary From 4d8c82e65145682156003454d35540061f4b768c Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 7 Feb 2025 00:20:45 +0900 Subject: [PATCH 212/215] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=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 --- .../java/com/example/gamemate/global/config/RedisConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 3620f8b..139425d 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -113,7 +113,7 @@ public StringRedisTemplate blacklistRedisTemplate() { public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() - .setAddress("redis://127.0.0.1:6379") + .setAddress("redis://" + redisHost + ":6379") .setDatabase(5); return Redisson.create(config); } From 16b8f45352a655738685082673cfaa3df15d9c5c Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 7 Feb 2025 08:47:37 +0900 Subject: [PATCH 213/215] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=ED=98=B8=EC=8A=A4=ED=8A=B8=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 --- .../java/com/example/gamemate/global/config/RedisConfig.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/example/gamemate/global/config/RedisConfig.java b/src/main/java/com/example/gamemate/global/config/RedisConfig.java index 139425d..f7cfbd0 100644 --- a/src/main/java/com/example/gamemate/global/config/RedisConfig.java +++ b/src/main/java/com/example/gamemate/global/config/RedisConfig.java @@ -47,6 +47,7 @@ public RedisTemplate redisTemplate() { @Bean public RedisConnectionFactory notificationConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(redisHost); config.setDatabase(1); return new LettuceConnectionFactory(config); } @@ -55,6 +56,7 @@ public RedisConnectionFactory notificationConnectionFactory() { @Bean public RedisConnectionFactory viewCountConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(redisHost); config.setDatabase(2); return new LettuceConnectionFactory(config); } @@ -63,6 +65,7 @@ public RedisConnectionFactory viewCountConnectionFactory() { @Bean public RedisConnectionFactory refreshTokenConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(redisHost); config.setDatabase(3); return new LettuceConnectionFactory(config); } @@ -71,6 +74,7 @@ public RedisConnectionFactory refreshTokenConnectionFactory() { @Bean public RedisConnectionFactory blacklistConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(redisHost); config.setDatabase(4); return new LettuceConnectionFactory(config); } From 5e6a503e0a5ca6d05af1c9bc979e7fa0d044d69d Mon Sep 17 00:00:00 2001 From: sumyeom Date: Fri, 7 Feb 2025 10:47:04 +0900 Subject: [PATCH 214/215] =?UTF-8?q?fix:=20RDS=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=98=EC=97=AC=20docker-compose=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 50b12b8..09b0ea7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,19 +29,5 @@ services: GEMINI_URL: "${GEMINI_URL}" GEMINI_KEY: "${GEMINI_KEY}" REDIS_HOST: "${REDIS_HOST}" - depends_on: - - db - - db: - image: mysql:8.0 - container_name: db - environment: - MYSQL_ROOT_PASSWORD: "${MYSQL_PASSWORD}" - volumes: - - db-data:/var/lib/mysql - ports: - - "3306:3306" -volumes: - db-data: From ba36d11e60080b3733d4ce8128ccfe968ff7d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=88=EC=A7=80?= Date: Fri, 7 Feb 2025 18:22:37 +0900 Subject: [PATCH 215/215] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=88=98=EC=A0=95=EC=9D=BC=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 4 ++ .../gamemate/global/common/BaseEntity.java | 4 ++ src/main/resources/data-dev.sql | 54 ++++++++++--------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java index e36b07f..2f8e6be 100644 --- a/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/gamemate/domain/auth/service/AuthService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.Optional; @Service @@ -72,6 +73,9 @@ public LoginTokenResponseDto localLogin(LocalLoginRequestDto requestDto, HttpSer if(!passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) { throw new ApiException(ErrorCode.INVALID_PASSWORD); } + + findUser.updateModifiedAt(); + return tokenService.generateLoginTokens(findUser, response); } diff --git a/src/main/java/com/example/gamemate/global/common/BaseEntity.java b/src/main/java/com/example/gamemate/global/common/BaseEntity.java index 2ae2ec7..e48fe49 100644 --- a/src/main/java/com/example/gamemate/global/common/BaseEntity.java +++ b/src/main/java/com/example/gamemate/global/common/BaseEntity.java @@ -22,4 +22,8 @@ public abstract class BaseEntity { @LastModifiedDate private LocalDateTime modifiedAt; + public void updateModifiedAt() { + this.modifiedAt = LocalDateTime.now(); + } + } diff --git a/src/main/resources/data-dev.sql b/src/main/resources/data-dev.sql index 48f4224..e4cfa22 100644 --- a/src/main/resources/data-dev.sql +++ b/src/main/resources/data-dev.sql @@ -7,14 +7,16 @@ INSERT INTO `user` ( role, is_premium, user_status, - provider + provider, + created_at, + modified_at ) VALUES - ('user1@test.com', '유저1', '유저닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL'), - ('user2@test.com', '유저2', '유저닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL'), - ('user3@test.com', '유저3', '유저닉3', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL'), - ('user4@test.com', '유저4', '유저닉4', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL'), - ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', true, 'ACTIVE', 'LOCAL'), - ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', false, 'ACTIVE', 'LOCAL'); + ('user1@test.com', '유저1', '유저닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL', now(), now()), + ('user2@test.com', '유저2', '유저닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL', now(), now()), + ('user3@test.com', '유저3', '유저닉3', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', false, 'ACTIVE', 'LOCAL', now(), now()), + ('user4@test.com', '유저4', '유저닉4', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'USER', true, 'ACTIVE', 'LOCAL', now(), now()), + ('admin1@test.com', '관리자1', '관리자닉1', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', true, 'ACTIVE', 'LOCAL', now(), now()), + ('admin2@test.com', '관리자2', '관리자닉2', '$2a$10$HuoYrGAAXUn2LJ3EA8ZTJubGdkLyvlYxbs4cQjG0WYQ0BNDa5Xi2S', 'ADMIN', false, 'ACTIVE', 'LOCAL', now(), now()); -- MatchUserInfo 테이블 데이터 INSERT INTO match_user_info ( @@ -23,14 +25,16 @@ INSERT INTO match_user_info ( game_rank, skill_level, mic_usage, - message + message, + created_at, + modified_at ) VALUES - (1, 'MALE', 'DIAMOND', 5, true, '같이 트롤하실분? 정글러 구함'), - (2, 'FEMALE', 'BRONZE', 1, true, '그냥 즐겜할래요.'), - (3, 'FEMALE', 'GOLD', 3, true, '랭겜 즐기실분 구합니다.'), - (4, 'MALE', 'SILVER', 2, true, '초보 뉴비 환영합니다!'), - (5, 'FEMALE', 'CHALLENGER', 6, false, '빡겜할 듀오 찾습니다.'), - (6, 'MALE', 'PLATINUM', 4, true, '실버 이상만요!'); + (1, 'MALE', 'DIAMOND', 5, true, '같이 트롤하실분? 정글러 구함', now(), now()), + (2, 'FEMALE', 'BRONZE', 1, true, '그냥 즐겜할래요.', now(), now()), + (3, 'FEMALE', 'GOLD', 3, true, '랭겜 즐기실분 구합니다.', now(), now()), + (4, 'MALE', 'SILVER', 2, true, '초보 뉴비 환영합니다!', now(), now()), + (5, 'FEMALE', 'CHALLENGER', 6, false, '빡겜할 듀오 찾습니다.', now(), now()), + (6, 'MALE', 'PLATINUM', 4, true, '실버 이상만요!', now(), now()); -- user_lanes 테이블 데이터 INSERT INTO user_lanes (match_user_info_id, lanes) @@ -63,15 +67,15 @@ VALUES (6, 'SIX_TO_TWELVE'), (6, 'EIGHTEEN_TO_TWENTY_FOUR'); -- game 테이블 데이터 -INSERT INTO game (title, genre, platform, description) +INSERT INTO game (title, genre, platform, description, created_at, modified_at) VALUES - ('라스트 오브 어스', 'Action', 'PlayStation', '종말 이후의 세계를 배경으로 한 스토리 중심의 서바이벌 게임.'), - ('마인크래프트', 'Sandbox', 'PC', '무한히 생성되는 세계에서 블록을 쌓고 모험을 떠나는 게임.'), - ('오버워치', 'Shooter', 'PC', '독특한 능력을 가진 다양한 영웅들이 등장하는 팀 기반 1인칭 슈팅 게임.'), - ('스타듀 밸리', 'Simulation', 'PC', '할아버지의 오래된 농장을 물려받아 경영하는 농장 시뮬레이션 RPG.'), - ('엘든 링', 'RPG', 'PC', '미야자키 히데타카와 조지 R.R. 마틴이 만든 다크 판타지 오픈 월드 액션 RPG.'), - ('피파 23', 'Sports', 'PlayStation', '인기 축구 시뮬레이션 시리즈의 최신작으로, 업데이트된 팀과 향상된 게임플레이 제공.'), - ('콜 오브 듀티: 워존', 'Shooter', 'PC', '콜 오브 듀티 세계관을 배경으로 한 무료 배틀로얄 게임.'), - ('모여봐요 동물의 숲', 'Simulation', 'Nintendo Switch', '무인도에서 자신만의 낙원을 만들어가는 소셜 시뮬레이션 게임.'), - ('포르자 호라이즌 5', 'Racing', 'Xbox', '멕시코의 생동감 넘치고 끊임없이 변화하는 풍경을 배경으로 한 오픈 월드 레이싱 게임.'), - ('할로우 나이트', 'Adventure', 'PC', '광대하고 서로 연결된 세계를 배경으로 한 도전적인 2D 액션 어드벤처 게임.'); \ No newline at end of file + ('라스트 오브 어스', 'Action', 'PlayStation', '종말 이후의 세계를 배경으로 한 스토리 중심의 서바이벌 게임.', now(), now()), + ('마인크래프트', 'Sandbox', 'PC', '무한히 생성되는 세계에서 블록을 쌓고 모험을 떠나는 게임.', now(), now()), + ('오버워치', 'Shooter', 'PC', '독특한 능력을 가진 다양한 영웅들이 등장하는 팀 기반 1인칭 슈팅 게임.', now(), now()), + ('스타듀 밸리', 'Simulation', 'PC', '할아버지의 오래된 농장을 물려받아 경영하는 농장 시뮬레이션 RPG.', now(), now()), + ('엘든 링', 'RPG', 'PC', '미야자키 히데타카와 조지 R.R. 마틴이 만든 다크 판타지 오픈 월드 액션 RPG.', now(), now()), + ('피파 23', 'Sports', 'PlayStation', '인기 축구 시뮬레이션 시리즈의 최신작으로, 업데이트된 팀과 향상된 게임플레이 제공.', now(), now()), + ('콜 오브 듀티: 워존', 'Shooter', 'PC', '콜 오브 듀티 세계관을 배경으로 한 무료 배틀로얄 게임.', now(), now()), + ('모여봐요 동물의 숲', 'Simulation', 'Nintendo Switch', '무인도에서 자신만의 낙원을 만들어가는 소셜 시뮬레이션 게임.', now(), now()), + ('포르자 호라이즌 5', 'Racing', 'Xbox', '멕시코의 생동감 넘치고 끊임없이 변화하는 풍경을 배경으로 한 오픈 월드 레이싱 게임.', now(), now()), + ('할로우 나이트', 'Adventure', 'PC', '광대하고 서로 연결된 세계를 배경으로 한 도전적인 2D 액션 어드벤처 게임.', now(), now()); \ No newline at end of file