From d8cc31d20c7728baf43da21ca10e9ba11c0b1e90 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:16:19 +0900 Subject: [PATCH 001/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보낼 화살 수를 담은 요청 DTO 정의 --- .../be/arrow/dto/request/ArrowSendRequest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 be/src/main/java/yeonba/be/arrow/dto/request/ArrowSendRequest.java diff --git a/be/src/main/java/yeonba/be/arrow/dto/request/ArrowSendRequest.java b/be/src/main/java/yeonba/be/arrow/dto/request/ArrowSendRequest.java new file mode 100644 index 00000000..8e3616e9 --- /dev/null +++ b/be/src/main/java/yeonba/be/arrow/dto/request/ArrowSendRequest.java @@ -0,0 +1,18 @@ +package yeonba.be.arrow.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ArrowSendRequest { + + @Schema( + type = "number", + description = "보낼 화살 수", + example = "10") + @Positive(message = "화살은 1개 이상부터 보낼 수 있습니다.") + private int arrows; +} From 9b490938f7a9ebb22dea55136d6891231fc39596 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:18:29 +0900 Subject: [PATCH 002/138] =?UTF-8?q?feat:=20=ED=99=95=EC=82=B4=20=EC=86=A1?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EB=82=B4=EC=97=AD=20repository,=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화살 송수신 내역 repository 정의 - 보낸 사용자, 받은 사용자를 통해 내역 존재 여부를 확인하는 로직 구현 --- .../arrow/repository/ArrowTransactionRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionRepository.java diff --git a/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionRepository.java b/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionRepository.java new file mode 100644 index 00000000..9dbd2c18 --- /dev/null +++ b/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionRepository.java @@ -0,0 +1,12 @@ +package yeonba.be.arrow.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.arrow.entity.ArrowTransaction; +import yeonba.be.user.entity.User; + +@Repository +public interface ArrowTransactionRepository extends JpaRepository { + + boolean existsBySentUserAndReceivedUser(User sentUser, User receivedUser); +} From e953b8531b3eaeb55d1946a0f4ecaba75edd787b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:23:08 +0900 Subject: [PATCH 003/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EC=86=A1?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EB=82=B4=EC=97=AD=20=EC=A1=B4=EC=9E=AC=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보낸 사용자, 받은 사용자를 통해 송수신 내역 존재 여부를 확인하는 로직 구현 --- .../yeonba/be/arrow/repository/ArrowQuery.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java diff --git a/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java b/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java new file mode 100644 index 00000000..7f67cc48 --- /dev/null +++ b/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java @@ -0,0 +1,18 @@ +package yeonba.be.arrow.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.User; + +@Component +@RequiredArgsConstructor +public class ArrowQuery { + + private final ArrowTransactionRepository arrowTransactionRepository; + + public boolean existsBySentUserAndReceivedUser(User sentUser, User receivedUser) { + + return arrowTransactionRepository + .existsBySentUserAndReceivedUser(sentUser, receivedUser); + } +} From 7a7b7c36928590672539dde4d38369a2a4128a87 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:23:36 +0900 Subject: [PATCH 004/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EC=86=A1?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EB=82=B4=EC=97=AD=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/arrow/repository/ArrowCommand.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 be/src/main/java/yeonba/be/arrow/repository/ArrowCommand.java diff --git a/be/src/main/java/yeonba/be/arrow/repository/ArrowCommand.java b/be/src/main/java/yeonba/be/arrow/repository/ArrowCommand.java new file mode 100644 index 00000000..2b4f04a3 --- /dev/null +++ b/be/src/main/java/yeonba/be/arrow/repository/ArrowCommand.java @@ -0,0 +1,18 @@ +package yeonba.be.arrow.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.arrow.entity.ArrowTransaction; + +@Component +@RequiredArgsConstructor +public class ArrowCommand { + + private final ArrowTransactionRepository arrowTransactionRepository; + + public ArrowTransaction save(ArrowTransaction arrowTransaction){ + + return arrowTransactionRepository.save(arrowTransaction); + } + +} From d781de37c3c4fea5d05a1ce7ac2e71beb2de7976 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:25:05 +0900 Subject: [PATCH 005/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=88=EC=99=B8=20enum=20=EC=A0=95=EC=9D=98(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다음 상황들에 대한 예외 enum을 정의하였다. - 자기 자신에게 화살을 보내는 경우 - 이미 화살을 보낸 사용자인 경우 - 가진 화살 수가 부족해 화살을 보낼 수 없는 경우 --- .../java/yeonba/be/exception/ExceptionType.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/exception/ExceptionType.java b/be/src/main/java/yeonba/be/exception/ExceptionType.java index 6af501e6..5184a778 100644 --- a/be/src/main/java/yeonba/be/exception/ExceptionType.java +++ b/be/src/main/java/yeonba/be/exception/ExceptionType.java @@ -16,7 +16,20 @@ public enum ExceptionType { USER_NOT_FOUND( HttpStatus.BAD_REQUEST, - "해당 사용자가 존재하지 않습니다."); + "해당 사용자가 존재하지 않습니다."), + + // arrow + CAN_NOT_SEND_ARROW_SELF( + HttpStatus.BAD_REQUEST, + "자기 자신에게 화살을 보낼 수 없습니다."), + + ALREADY_SENT_ARROW_USER( + HttpStatus.BAD_REQUEST, + "이미 화살을 보낸 사용자입니다."), + + NOT_ENOUGH_ARROW_TO_SEND( + HttpStatus.BAD_REQUEST, + "가진 화살 수가 부족해 화살을 보낼 수 없습니다."); private final HttpStatus httpStatus; private final String reason; From 8aad1e4c7b65bd8bae676928edc13578fe7ce7e7 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:27:00 +0900 Subject: [PATCH 006/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=99=94=EC=82=B4=20=EA=B0=90=EC=86=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 감소하려는 화살 수가 사용자가 보유한 화살 수보다 클 경우 예외 발생 --- be/src/main/java/yeonba/be/user/entity/User.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 10e3288d..0dbbf53a 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -15,6 +15,8 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import yeonba.be.exception.ExceptionType; +import yeonba.be.exception.GeneralException; @Table(name = "users") @Getter @@ -125,4 +127,12 @@ public void addArrow(int arrow) { this.arrow += arrow; } + + public void minusArrow(int arrow) { + if (this.arrow < arrow) { + throw new GeneralException(ExceptionType.NOT_ENOUGH_ARROW_TO_SEND); + } + + this.arrow -= arrow; + } } From 7dd07aa33e64a3cdf4c3b407bbe8128db154d98e Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:28:34 +0900 Subject: [PATCH 007/138] =?UTF-8?q?refactor:=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=ED=95=84=EB=93=9C,=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=88=EA=B0=80=20DDL=20=EC=86=8D=EC=84=B1=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java b/be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java index aa44810b..5ca5fda9 100644 --- a/be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java +++ b/be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java @@ -39,7 +39,7 @@ public class ArrowTransaction { private int arrows; @CreatedDate - @Column(nullable = false, updatable = false) + @Column(nullable = false) private LocalDateTime createdAt; public ArrowTransaction( From 3a6137d9c51c1ebba4b5847003158f9fce20948b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:32:18 +0900 Subject: [PATCH 008/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 화살 보내기 비즈니스 로직은 다음 과정을 거친다. 1. 자기 자신에게 화살을 보내는 상황 검증 2. 화살을 보낸 사용자에게 또 보내는 상황 검증 3. 화살 내역 저장 4. 보내는 사용자 화살 감소, 화살이 부족할 경우 예외 발생(과정 3 롤백) 5. 받는 사용자 화살 증가 --- .../yeonba/be/arrow/service/ArrowService.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java index 40220e8a..3a962bd0 100644 --- a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java +++ b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java @@ -5,8 +5,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import yeonba.be.arrow.dto.request.ArrowSendRequest; import yeonba.be.arrow.entity.ArrowTransaction; +import yeonba.be.arrow.repository.ArrowCommand; +import yeonba.be.arrow.repository.ArrowQuery; import yeonba.be.arrow.repository.ArrowTransactionCommandRepository; +import yeonba.be.exception.ExceptionType; +import yeonba.be.exception.GeneralException; import yeonba.be.user.entity.User; import yeonba.be.user.service.UserService; @@ -17,6 +22,8 @@ public class ArrowService { private final int DAILY_CHECK_ARROW_COUNT = 10; private final UserService userService; private final ArrowTransactionCommandRepository arrowTransactionCommandRepository; + private final ArrowQuery arrowQuery; + private final ArrowCommand arrowCommand; /* 출석 체크는 다음 과정을 거쳐 이뤄진다. @@ -46,10 +53,54 @@ public void dailyCheck(long userId) { dailyCheckUser.addArrow(DAILY_CHECK_ARROW_COUNT); } + /* + 화살 보내기 비즈니스 로직은 다음 과정을 거친다. + 1. 자기 자신에게 화살을 보내는 상황 검증 + 2. 화살을 보낸 사용자에게 또 보내는 상황 검증 + 3. 화살 내역 저장 + 4. 보내는 사용자 화살 감소, 화살이 부족할 경우 예외 발생 + 5. 받는 사용자 화살 증가 + + */ + @Transactional + public void sendArrow( + long senderId, + long recipientId, + ArrowSendRequest request) { + + if (senderId == recipientId) { + throw new GeneralException(ExceptionType.CAN_NOT_SEND_ARROW_SELF); + } + + User sender = userService.findById(senderId); + User recipient = userService.findById(recipientId); + + if (isAlreadySentArrowUser(sender, recipient)) { + throw new GeneralException(ExceptionType.ALREADY_SENT_ARROW_USER); + } + + int arrows = request.getArrows(); + ArrowTransaction arrowTransaction = new ArrowTransaction( + sender, + recipient, + arrows); + arrowCommand.save(arrowTransaction); + + sender.minusArrow(arrows); + recipient.addArrow(arrows); + } + private boolean isAlreadyCheckedUser(User user, LocalDate dailyCheckDate) { return user.getLastAccessedAt() .toLocalDate() .equals(dailyCheckDate); } + + private boolean isAlreadySentArrowUser(User sender, User recipient) { + + return arrowQuery + .existsBySentUserAndReceivedUser(sender, recipient); + } + } From 9a4cae04739eeeaef84c58ccd94614c5edb4a122 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:33:09 +0900 Subject: [PATCH 009/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/arrow/controller/ArrowController.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java b/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java index e09fcf72..0f2ef38e 100644 --- a/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java +++ b/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java @@ -10,8 +10,10 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import yeonba.be.arrow.dto.UserArrowsResponse; +import yeonba.be.arrow.dto.request.ArrowSendRequest; import yeonba.be.arrow.service.ArrowService; import yeonba.be.util.CustomResponse; @@ -51,18 +53,19 @@ public ResponseEntity> dailyCheck( .body(new CustomResponse<>()); } - @Operation( - summary = "화살 보내기", - description = "다른 사용자에게 화살을 보낼 수 있습니다." - ) - @ApiResponse( - responseCode = "204", - description = "화살 전송 정상 처리" - ) + @Operation(summary = "화살 보내기", description = "다른 사용자에게 화살을 보낼 수 있습니다.") + @ApiResponse(responseCode = "202", description = "화살 전송 정상 처리") @PostMapping("/users/{userId}/arrow") public ResponseEntity> sendArrow( + @RequestAttribute("userId") long senderId, @Parameter(description = "화살 받는 사용자 ID", example = "1") - @PathVariable long userId) { + @PathVariable("userId") long recipientId, + @RequestBody ArrowSendRequest request) { + + arrowService.sendArrow( + senderId, + recipientId, + request); return ResponseEntity .accepted() From 52995c4edb1140aee28cb4469dffc75695d05bd2 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 17:36:21 +0900 Subject: [PATCH 010/138] =?UTF-8?q?refactor:=20=ED=99=94=EC=82=B4=20comman?= =?UTF-8?q?d=20repository=20=EC=82=AD=EC=A0=9C(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - query, command를 service, repository를 중개하는 별도의 계층을 통해 구분 - 이에 따라 기존 command repository 삭제 - 기존 command repository 사용 위치, 화살 command로 대체 --- .../repository/ArrowTransactionCommandRepository.java | 10 ---------- .../java/yeonba/be/arrow/service/ArrowService.java | 4 +--- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionCommandRepository.java diff --git a/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionCommandRepository.java b/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionCommandRepository.java deleted file mode 100644 index c90715a3..00000000 --- a/be/src/main/java/yeonba/be/arrow/repository/ArrowTransactionCommandRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package yeonba.be.arrow.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import yeonba.be.arrow.entity.ArrowTransaction; - -@Repository -public interface ArrowTransactionCommandRepository extends JpaRepository { - -} diff --git a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java index 3a962bd0..0c30f178 100644 --- a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java +++ b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java @@ -9,7 +9,6 @@ import yeonba.be.arrow.entity.ArrowTransaction; import yeonba.be.arrow.repository.ArrowCommand; import yeonba.be.arrow.repository.ArrowQuery; -import yeonba.be.arrow.repository.ArrowTransactionCommandRepository; import yeonba.be.exception.ExceptionType; import yeonba.be.exception.GeneralException; import yeonba.be.user.entity.User; @@ -21,7 +20,6 @@ public class ArrowService { private final int DAILY_CHECK_ARROW_COUNT = 10; private final UserService userService; - private final ArrowTransactionCommandRepository arrowTransactionCommandRepository; private final ArrowQuery arrowQuery; private final ArrowCommand arrowCommand; @@ -47,7 +45,7 @@ public void dailyCheck(long userId) { dailyCheckUser, DAILY_CHECK_ARROW_COUNT ); - arrowTransactionCommandRepository.save(arrowTransaction); + arrowCommand.save(arrowTransaction); dailyCheckUser.updateLastAccessedAt(dailyCheckedAt); dailyCheckUser.addArrow(DAILY_CHECK_ARROW_COUNT); From a2c164d32a4e586df415a65db3ef00b5d592784f Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 9 Mar 2024 20:44:15 +0900 Subject: [PATCH 011/138] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/arrow/service/ArrowService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java index 0c30f178..83e55d39 100644 --- a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java +++ b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java @@ -97,8 +97,7 @@ private boolean isAlreadyCheckedUser(User user, LocalDate dailyCheckDate) { private boolean isAlreadySentArrowUser(User sender, User recipient) { - return arrowQuery - .existsBySentUserAndReceivedUser(sender, recipient); + return arrowQuery.existsBySentUserAndReceivedUser(sender, recipient); } } From 6c7ff1de0a2d4ac746ee49c0ea95d3f2c583ade0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 19:50:23 +0900 Subject: [PATCH 012/138] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=ED=99=94=EC=82=B4=20=EC=A6=9D=EA=B0=90=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서로 대치되는 작업을 수행하는 상황을 잘 표현할 수 있도록 메서드 이름 수정 --- be/src/main/java/yeonba/be/user/entity/User.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 7439b44f..f172d9bd 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -146,8 +146,17 @@ public void updateLastAccessedAt(LocalDateTime accessedAt) { this.lastAccessedAt = accessedAt; } - public void addArrow(int arrow) { + public void plusArrow(int arrow) { this.arrow += arrow; } + + public void minusArrow(int arrow) { + + if (this.arrow < arrow) { + throw new GeneralException(ArrowException.NOT_ENOUGH_ARROW_TO_SEND); + } + + this.arrow -= arrow; + } } From 01f31382f573b52c3047e77c7ecc4c41094df8c0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 19:51:39 +0900 Subject: [PATCH 013/138] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=88=EC=99=B8=20enum=20=EC=B6=94=EA=B0=80(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 화살을 보낸 사용자 예외 - 화살이 부족하여 화살을 보낼 수 없는 예외 --- .../main/java/yeonba/be/exception/ArrowException.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/exception/ArrowException.java b/be/src/main/java/yeonba/be/exception/ArrowException.java index 4d6c31db..7216212d 100644 --- a/be/src/main/java/yeonba/be/exception/ArrowException.java +++ b/be/src/main/java/yeonba/be/exception/ArrowException.java @@ -6,7 +6,15 @@ public enum ArrowException implements BaseException { ALREADY_CHECKED_USER( HttpStatus.BAD_REQUEST, - "이미 출석 체크한 사용자입니다."); + "이미 출석 체크한 사용자입니다."), + + ALREADY_SENT_ARROW_USER( + HttpStatus.BAD_REQUEST, + "이미 화살을 보낸 사용자입니다."), + + NOT_ENOUGH_ARROW_TO_SEND( + HttpStatus.BAD_REQUEST, + "화살이 부족하여 화살을 보낼 수 없습니다."); private final HttpStatus httpStatus; private final String reason; From 95c99d827b1617897cef1966a7e365bdfa6c87c4 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 19:52:19 +0900 Subject: [PATCH 014/138] =?UTF-8?q?refactor:=20=ED=99=94=EC=82=B4=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 가독성 향상을 위해 간단한 이름을 수정 --- be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java b/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java index 7f67cc48..14b5a1e9 100644 --- a/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java +++ b/be/src/main/java/yeonba/be/arrow/repository/ArrowQuery.java @@ -10,7 +10,7 @@ public class ArrowQuery { private final ArrowTransactionRepository arrowTransactionRepository; - public boolean existsBySentUserAndReceivedUser(User sentUser, User receivedUser) { + public boolean isArrowTransactionExist(User sentUser, User receivedUser) { return arrowTransactionRepository .existsBySentUserAndReceivedUser(sentUser, receivedUser); From 672f7bb80b94464dddb262d64fd65e1b734e64c8 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 19:53:26 +0900 Subject: [PATCH 015/138] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 브랜치 작업 내역 병합에 따른 코드 배치 수정 --- .../yeonba/be/arrow/service/ArrowService.java | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java index 4d89c075..4097ae2a 100644 --- a/be/src/main/java/yeonba/be/arrow/service/ArrowService.java +++ b/be/src/main/java/yeonba/be/arrow/service/ArrowService.java @@ -5,8 +5,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import yeonba.be.arrow.dto.UserArrowsResponse; +import yeonba.be.arrow.dto.request.ArrowSendRequest; import yeonba.be.arrow.entity.ArrowTransaction; import yeonba.be.arrow.repository.ArrowCommand; +import yeonba.be.arrow.repository.ArrowQuery; +import yeonba.be.exception.ArrowException; +import yeonba.be.exception.GeneralException; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; @@ -17,6 +21,7 @@ public class ArrowService { private final int DAILY_CHECK_ARROW_COUNT = 10; private final UserQuery userQuery; private final ArrowCommand arrowCommand; + private final ArrowQuery arrowQuery; /* 출석 체크는 다음 과정을 거쳐 이뤄진다. @@ -40,7 +45,7 @@ public void dailyCheck(long userId) { arrowCommand.save(arrowTransaction); dailyCheckUser.updateLastAccessedAt(dailyCheckedAt); - dailyCheckUser.addArrow(DAILY_CHECK_ARROW_COUNT); + dailyCheckUser.plusArrow(DAILY_CHECK_ARROW_COUNT); } @Transactional(readOnly = true) @@ -50,4 +55,39 @@ public UserArrowsResponse getUserArrows(long userId) { return new UserArrowsResponse(user.getArrow()); } + + /* + 화살 보내기 비즈니스 로직은 다음 과정을 거친다. + 1. 자기 자신에게 화살을 보내는 상황 검증 + 2. 화살을 보낸 사용자에게 또 보내는 상황 검증 + 3. 화살 내역 저장 + 4. 보내는 사용자 화살 감소, 화살이 부족할 경우 예외 발생 + 5. 받는 사용자 화살 증가 + + */ + @Transactional + public void sendArrow( + long senderId, + long recipientId, + ArrowSendRequest request) { + + User sender = userQuery.findById(senderId); + User receiver = userQuery.findById(recipientId); + + sender.validateNotSameUser(receiver); + + if (arrowQuery.isArrowTransactionExist(sender, receiver)) { + throw new GeneralException(ArrowException.ALREADY_SENT_ARROW_USER); + } + + int arrows = request.getArrows(); + ArrowTransaction arrowTransaction = new ArrowTransaction( + sender, + receiver, + arrows); + arrowCommand.save(arrowTransaction); + + sender.minusArrow(arrows); + receiver.plusArrow(arrows); + } } From f25c26f26b9f41b83ced7286a154cab6c0f9c521 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 19:54:57 +0900 Subject: [PATCH 016/138] =?UTF-8?q?refactor:=20=ED=99=94=EC=82=B4=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EA=B8=B0=20API=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 좀 더 명확하게 대치되는 의미를 나타낼 수 있도록 화살을 받는 사용자 ID 파라미터 이름을 receiverId로 수정 --- .../main/java/yeonba/be/arrow/controller/ArrowController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java b/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java index 5ba8b3db..c1df75ce 100644 --- a/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java +++ b/be/src/main/java/yeonba/be/arrow/controller/ArrowController.java @@ -56,12 +56,12 @@ public ResponseEntity> dailyCheck( public ResponseEntity> sendArrow( @RequestAttribute("userId") long senderId, @Parameter(description = "화살 받는 사용자 ID", example = "1") - @PathVariable("userId") long recipientId, + @PathVariable("userId") long receiverId, @RequestBody ArrowSendRequest request) { arrowService.sendArrow( senderId, - recipientId, + receiverId, request); return ResponseEntity From 4d26a1568ae74ce694a2f7efe294e29b56714e28 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 20:18:21 +0900 Subject: [PATCH 017/138] =?UTF-8?q?refactor:=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=BD=94=EB=93=9C=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/exception/ArrowException.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/be/src/main/java/yeonba/be/exception/ArrowException.java b/be/src/main/java/yeonba/be/exception/ArrowException.java index 7216212d..3d21cd72 100644 --- a/be/src/main/java/yeonba/be/exception/ArrowException.java +++ b/be/src/main/java/yeonba/be/exception/ArrowException.java @@ -26,11 +26,13 @@ public enum ArrowException implements BaseException { @Override public HttpStatus getHttpStatus() { + return httpStatus; } @Override public String getReason() { + return reason; } } From 7efedf975f600574176389c88f2b110743efaea1 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 21:41:48 +0900 Subject: [PATCH 018/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EC=B9=BC?= =?UTF-8?q?=EB=9F=BC,=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=B6=94=EA=B0=80(#?= =?UTF-8?q?27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이름 칼럼 추가 - 테스트시 활용 가능한 생성자 추가 --- be/src/main/java/yeonba/be/user/entity/Animal.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/entity/Animal.java b/be/src/main/java/yeonba/be/user/entity/Animal.java index 5c20a279..a1c01b87 100644 --- a/be/src/main/java/yeonba/be/user/entity/Animal.java +++ b/be/src/main/java/yeonba/be/user/entity/Animal.java @@ -1,25 +1,29 @@ package yeonba.be.user.entity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.AllArgsConstructor; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Table(name = "animals") @Getter @Entity -@NoArgsConstructor -@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Animal { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private String name; - + public Animal(String name) { + this.name = name; + } } From d77dae4b11db5e2e07b38ee47e63deb3672f76e6 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 22:01:27 +0900 Subject: [PATCH 019/138] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 래퍼 타입 필드들 기본 타입으로 변경 - 프로필 사진 URL 리스트 필드 추가 - 사용자가 가진 총 화살 수 필드 추가 - 음주 성향, 흡연 성향 필드 추가 - 코드 정렬 --- .../dto/response/UserProfileResponse.java | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java index 2f4837a9..43e04524 100644 --- a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java +++ b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java @@ -1,6 +1,7 @@ package yeonba.be.user.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -8,59 +9,74 @@ @AllArgsConstructor public class UserProfileResponse { + @Schema( + type = "array", + description = "프로필 사진 URL들") + private List profilePhotosUrls; + @Schema( type = "string", description = "닉네임", - example = "존잘남" - ) + example = "존잘남") private String nickname; + @Schema( + type = "number", + description = "사용자가 가진 총 화살 수", + example = "10") + private int arrows; + @Schema( type = "number", description = "나이", - example = "23" - ) - private Integer age; + example = "23") + private int age; @Schema( type = "number", description = "키", - example = "177" - ) - private Integer height; + example = "177") + private int height; @Schema( type = "string", description = "활동 지역", - example = "서울시 강남구" - ) + example = "서울") private String activityArea; @Schema( type = "number", description = "사진 싱크로율", - example = "80" - ) - private Integer photoSyncRate; + example = "80") + private double photoSyncRate; + + @Schema( + type = "string", + description = "음주 성향", + example = "가끔") + private String drinkingHabit; + + @Schema( + type = "string", + description = "흡연 성향", + example = "자주") + private String smokingHabit; @Schema( type = "string", description = "음역대", - example = "저음" - ) + example = "저음") private String vocalRange; @Schema( type = "string", description = "닮은 동물상", - example = "여우상" - ) + example = "여우상") private String lookAlikeAnimalName; @Schema( type = "boolean", description = "이전 화살 전송 여부", - example = "false" - ) - private Boolean isAlreadySentArrow; + example = "false") + private boolean isAlreadySentArrow; } From d0bbec93c49de88f6dd4507a7c85a44e62fb4fae Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 22:05:54 +0900 Subject: [PATCH 020/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레퍼런스 타입 not null 필드들, Column 어노테이션 사용 명시 - salt 필드 추가 - 생성 일시, 최종 수정 일시 필드 추가 - JpaAuditing 활용, 생성/최종 수정 일시 필드들 엔티티 저장시 자동 설정 - 새로운 생성자 구성 --- .../main/java/yeonba/be/user/entity/User.java | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index f172d9bd..c1617a39 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -1,6 +1,8 @@ package yeonba.be.user.entity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -12,18 +14,21 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.AllArgsConstructor; +import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import yeonba.be.exception.ArrowException; import yeonba.be.exception.GeneralException; @Table(name = "users") @Getter @Entity -@NoArgsConstructor -@AllArgsConstructor +@EntityListeners(value = AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @EqualsAndHashCode(of = "id") public class User { @@ -31,20 +36,46 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private boolean gender; + + @Column(nullable = false) private String name; + + @Column(nullable = false) private String nickname; + + @Column(nullable = false) private LocalDate birth; + private int age; private int height; + + @Column(nullable = false) private String email; + + @Column(nullable = false) private String encryptedPassword; + + @Column(nullable = false) + private String salt; + + @Column(nullable = false) private String phoneNumber; private int arrow; private double photoSyncRate; private boolean inactiveStatus; + + @Column(nullable = false) private String bodyType; + + @Column(nullable = false) private String job; + + @Column(nullable = false) private String drinkingHabit; + + @Column(nullable = false) private String smokingHabit; + + @Column(nullable = false) private String mbti; @ManyToOne @@ -63,15 +94,25 @@ public class User { List profilePhotos; private LocalDateTime lastAccessedAt; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + private LocalDateTime deletedAt; public User( + boolean gender, String name, String nickname, LocalDate birth, + int age, int height, String email, String encryptedPassword, + String salt, String phoneNumber, int arrow, double photoSyncRate, @@ -80,14 +121,20 @@ public User( String drinkingHabit, String smokingHabit, String mbti, + VocalRange vocalRange, + Animal animal, + Area area, + List profilePhotos, LocalDateTime lastAccessedAt) { - + this.gender = gender; this.name = name; this.nickname = nickname; this.birth = birth; + this.age = age; this.height = height; this.email = email; this.encryptedPassword = encryptedPassword; + this.salt = salt; this.phoneNumber = phoneNumber; this.arrow = arrow; this.photoSyncRate = photoSyncRate; @@ -97,6 +144,10 @@ public User( this.drinkingHabit = drinkingHabit; this.smokingHabit = smokingHabit; this.mbti = mbti; + this.vocalRange = vocalRange; + this.animal = animal; + this.area = area; + this.profilePhotos = profilePhotos; this.lastAccessedAt = lastAccessedAt; } From fa4204d56705542c5242d98223d754444df3fd38 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 22:08:47 +0900 Subject: [PATCH 021/138] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#2?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로필을 조회하는 사용자 ID의 경우 상대방이 화살을 보넀던 사용자인지 확인하기 위해 파라미터로 받는다. --- .../yeonba/be/user/service/UserService.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 38ad63a0..e4553c0a 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -1,12 +1,49 @@ package yeonba.be.user.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import yeonba.be.arrow.repository.ArrowQuery; +import yeonba.be.user.dto.response.UserProfileResponse; +import yeonba.be.user.entity.ProfilePhoto; +import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; @Service @RequiredArgsConstructor public class UserService { + + private final ArrowQuery arrowQuery; private final UserQuery userQuery; + + + @Transactional(readOnly = true) + public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { + + User user = userQuery.findById(userId); + User targetUser = userQuery.findById(targetUserId); + + List profilePhotosUrls = targetUser + .getProfilePhotos().stream() + .map(ProfilePhoto::getPhotoUrl) + .toList(); + + boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); + + return new UserProfileResponse( + profilePhotosUrls, + targetUser.getNickname(), + targetUser.getArrow(), + targetUser.getAge(), + targetUser.getHeight(), + targetUser.getArea().getName(), + targetUser.getPhotoSyncRate(), + targetUser.getDrinkingHabit(), + targetUser.getSmokingHabit(), + targetUser.getVocalRange().getClassification(), + targetUser.getAnimal().getName(), + isAlreadySentArrow); + } } From 87ec03ebf9a10de17f8ca5a53310074329769cd2 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 11 Mar 2024 22:10:38 +0900 Subject: [PATCH 022/138] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/user/controller/UserController.java | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/controller/UserController.java b/be/src/main/java/yeonba/be/user/controller/UserController.java index 31912e30..3ff6f160 100644 --- a/be/src/main/java/yeonba/be/user/controller/UserController.java +++ b/be/src/main/java/yeonba/be/user/controller/UserController.java @@ -19,6 +19,7 @@ import yeonba.be.user.dto.request.UserReportRequest; import yeonba.be.user.dto.response.UserProfileResponse; import yeonba.be.user.dto.response.UserQueryPageResponse; +import yeonba.be.user.service.UserService; import yeonba.be.util.CustomResponse; @Tag(name = "User", description = "사용자 API") @@ -26,6 +27,7 @@ @RequiredArgsConstructor public class UserController { + private final UserService userService; private final ReportService reportService; @Operation( @@ -46,33 +48,19 @@ public ResponseEntity> users( } - @Operation( - summary = "다른 사용자 프로필 조회", - description = "다른 사용자의 프로필을 조회할 수 있습니다." - ) - @ApiResponse( - responseCode = "200", - description = "사용자 프로필 정상 조회" - ) + @Operation(summary = "다른 사용자 프로필 조회", description = "다른 사용자의 프로필을 조회할 수 있습니다.") + @ApiResponse(responseCode = "200", description = "사용자 프로필 정상 조회") @GetMapping("/users/{userId}") public ResponseEntity> profile( + @RequestAttribute("userId") long userId, @Parameter(description = "조회대상 사용자 ID", example = "1") - @PathVariable long userId) { + @PathVariable("userId") long targetUserId) { + + UserProfileResponse response = userService.getTargetUserProfile(userId, targetUserId); return ResponseEntity .ok() - .body(new CustomResponse<>( - new UserProfileResponse( - "존잘남", - 23, - 177, - "서울시 강남구", - 80, - "저음", - "여우상", - false - ) - )); + .body(new CustomResponse<>(response)); } @Operation( From e6f7eb1b129d06e1924781c39ebb462d57b42df0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 15:43:31 +0900 Subject: [PATCH 023/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 요구사항 변경에 따라 음주 성향, 흡연 성향 필드 삭제 - 성별 정보 제공 메서드 추가 --- .../main/java/yeonba/be/user/entity/User.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index c1617a39..ace44b5a 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -69,12 +69,6 @@ public class User { @Column(nullable = false) private String job; - @Column(nullable = false) - private String drinkingHabit; - - @Column(nullable = false) - private String smokingHabit; - @Column(nullable = false) private String mbti; @@ -118,14 +112,11 @@ public User( double photoSyncRate, String bodyType, String job, - String drinkingHabit, - String smokingHabit, String mbti, VocalRange vocalRange, Animal animal, Area area, - List profilePhotos, - LocalDateTime lastAccessedAt) { + List profilePhotos) { this.gender = gender; this.name = name; this.nickname = nickname; @@ -141,14 +132,11 @@ public User( this.inactiveStatus = true; this.bodyType = bodyType; this.job = job; - this.drinkingHabit = drinkingHabit; - this.smokingHabit = smokingHabit; this.mbti = mbti; this.vocalRange = vocalRange; this.animal = animal; this.area = area; this.profilePhotos = profilePhotos; - this.lastAccessedAt = lastAccessedAt; } public void validateSameUser(User user) { @@ -210,4 +198,12 @@ public void minusArrow(int arrow) { this.arrow -= arrow; } + + public String getGender() { + if (this.gender) { + return "남"; + } + + return "여"; + } } From 4aba46d9296d8c74fddde4666e5cb82de328715f Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 15:44:52 +0900 Subject: [PATCH 024/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=91=EB=8B=B5=20DTO=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음주 성향, 흡연 성향 필드 삭제 - 성별 필드 추가 --- .../user/dto/response/UserProfileResponse.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java index 43e04524..246b9a05 100644 --- a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java +++ b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java @@ -14,6 +14,12 @@ public class UserProfileResponse { description = "프로필 사진 URL들") private List profilePhotosUrls; + @Schema( + type = "string", + description = "성별", + example = "남") + private String gender; + @Schema( type = "string", description = "닉네임", @@ -50,18 +56,6 @@ public class UserProfileResponse { example = "80") private double photoSyncRate; - @Schema( - type = "string", - description = "음주 성향", - example = "가끔") - private String drinkingHabit; - - @Schema( - type = "string", - description = "흡연 성향", - example = "자주") - private String smokingHabit; - @Schema( type = "string", description = "음역대", From 6cc2cb659ff44e296752dbc7f6e023022de0bc16 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 15:47:24 +0900 Subject: [PATCH 025/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=83=81=EC=84=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20DTO=20=EC=88=98=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음주 성향, 흡연 성향 필드 삭제 - 생성자 성별 필드 설정 로직 수정 - 코드 정렬 --- .../response/UserProfileDetailResponse.java | 61 +++++-------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java index 31bcd220..08c30eae 100644 --- a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java +++ b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -17,101 +15,74 @@ public class UserProfileDetailResponse { @Schema( type = "array", - description = "프로필 이미지" - ) - @Size(min = 3, max = 3) + description = "프로필 이미지") private List profilePhotoUrls; @Schema( type = "string", description = "성별", - example = "남" - ) + example = "남") private String gender; @Schema( type = "string", description = "이름", - example = "안민재" - ) + example = "안민재") private String name; @Schema( type = "string", description = "생년월일", - example = "1998-01-01" - ) + example = "1998-01-01") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birth; @Schema( type = "number", description = "키", - example = "181" - ) + example = "181") private int height; @Schema( type = "string", description = "이메일", - example = "mj3242@naver.com" - ) + example = "mj3242@naver.com") private String email; @Schema( type = "string", description = "전화번호", - example = "01011112222" - ) + example = "01011112222") private String phoneNumber; @Schema( type = "string", description = "별명", - example = "존잘남" - ) + example = "존잘남") private String nickname; @Schema( type = "number", description = "사진 싱크로율", - example = "80" - ) - private int photoSyncRate; + example = "80") + private double photoSyncRate; @Schema( type = "string", description = "체형", - example = "메른 체형" - ) + example = "메른 체형") private String bodyType; @Schema( type = "string", description = "직업", - example = "학생" - ) + example = "학생") private String job; - @Schema( - type = "string", - description = "음주 성향", - example = "자주" - ) - private String drinkingHabit; - - @Schema( - type = "string", - description = "흡연 성향", - example = "가끔" - ) - private String smokingHabit; - @Schema( type = "string", description = "MBTI", - example = "ISTP" - ) + example = "ISTP") private String mbti; public UserProfileDetailResponse(User user) { @@ -119,18 +90,16 @@ public UserProfileDetailResponse(User user) { this.profilePhotoUrls = user.getProfilePhotos().stream() .map(ProfilePhoto::getPhotoUrl) .toList(); - this.gender = user.isGender() ? "남" : "여"; + this.gender = user.getGender(); this.name = user.getName(); this.birth = user.getBirth(); this.height = user.getHeight(); this.email = user.getEmail(); this.phoneNumber = user.getPhoneNumber(); this.nickname = user.getNickname(); - this.photoSyncRate = (int) (user.getPhotoSyncRate()); + this.photoSyncRate = user.getPhotoSyncRate(); this.bodyType = user.getBodyType(); this.job = user.getJob(); - this.drinkingHabit = user.getDrinkingHabit(); - this.smokingHabit = user.getSmokingHabit(); this.mbti = user.getMbti(); } } From 1d7521839d017e1c874d62678f577441f07dbbb7 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 15:49:26 +0900 Subject: [PATCH 026/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음주 성향, 흡연 성향 삭제 - 성별 정보 응답에 포함토록 수정 - 코드 정렬 --- be/src/main/java/yeonba/be/user/service/UserService.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index e4553c0a..6b1c2956 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -14,11 +14,9 @@ @RequiredArgsConstructor public class UserService { - private final ArrowQuery arrowQuery; private final UserQuery userQuery; - @Transactional(readOnly = true) public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { @@ -34,14 +32,13 @@ public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) return new UserProfileResponse( profilePhotosUrls, + targetUser.getGender(), targetUser.getNickname(), targetUser.getArrow(), targetUser.getAge(), targetUser.getHeight(), targetUser.getArea().getName(), targetUser.getPhotoSyncRate(), - targetUser.getDrinkingHabit(), - targetUser.getSmokingHabit(), targetUser.getVocalRange().getClassification(), targetUser.getAnimal().getName(), isAlreadySentArrow); From 4e54bfda7a8bc535122139819667c1788ed260f1 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:03:52 +0900 Subject: [PATCH 027/138] =?UTF-8?q?chore:=20spring=20mail=20=EC=A2=85?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/be/build.gradle b/be/build.gradle index 44da8a87..5f48c408 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -37,6 +37,9 @@ dependencies { // aws s3 implementation 'software.amazon.awssdk:aws-sdk-java:2.16.83' implementation 'software.amazon.awssdk:s3:2.16.83' + + // spring mail + implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' } tasks.named('bootBuildImage') { From ba20da8b7514816e922869ac73c458d39156eeb1 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:06:48 +0900 Subject: [PATCH 028/138] =?UTF-8?q?chore:=20Gmail=20SMTP=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index f2ba3c03..91b663c1 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -11,3 +11,8 @@ spring: hibernate: show_sql: true format_sql: true + mail: + host: smtp.gmail.com + port: 587 + username: ${GOOGLE_SMTP_USERNAME} + password: ${GOOGLE_SMTP_PASSWORD} From 0d4b90f881ee029a4fafcaaa186c3b161723b11b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:07:26 +0900 Subject: [PATCH 029/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index cb9c5ddb..41aada43 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -1,5 +1,6 @@ package yeonba.be.user.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import yeonba.be.user.entity.User; @@ -7,4 +8,5 @@ @Repository public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } From 78ac728d4391fd0bd34596f13ae315728a6db787 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:07:57 +0900 Subject: [PATCH 030/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserQuery.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 9d32a736..9a47f7c5 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -17,4 +17,10 @@ public User findById(long userId) { return userRepository.findById(userId) .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); } + + public User findByEmail(String email) { + + return userRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } } From 5bd4d1fa860f67af2a1be0aa4d71e1978149d35a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:09:04 +0900 Subject: [PATCH 031/138] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20enum=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서비스에서 사용하는 정규식들을 모아놓는 enum 클래스 정의 - 이메일, 비밀번호 정규식 정의 --- .../main/java/yeonba/be/util/ServiceRegex.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/ServiceRegex.java diff --git a/be/src/main/java/yeonba/be/util/ServiceRegex.java b/be/src/main/java/yeonba/be/util/ServiceRegex.java new file mode 100644 index 00000000..d7531dac --- /dev/null +++ b/be/src/main/java/yeonba/be/util/ServiceRegex.java @@ -0,0 +1,18 @@ +package yeonba.be.util; + +public enum ServiceRegex { + + EMAIL("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"), + PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$"); + + private final String pattern; + + ServiceRegex(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + + return pattern; + } +} From bdc82596f8db54560b25fbb08f0c826416c7e848 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:12:05 +0900 Subject: [PATCH 032/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비밀번호 정규식을 충족하는 임의의 임시 비밀번호를 발급 - Random보다 생성하는 난수를 예측하기 어려워 보안적으로 뛰어난 SecureRandom 사용 --- .../be/util/TemporaryPasswordGenerator.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java diff --git a/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java b/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java new file mode 100644 index 00000000..81c424a8 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java @@ -0,0 +1,58 @@ +package yeonba.be.util; + +import java.security.SecureRandom; +import java.util.Random; + +public class TemporaryPasswordGenerator { + + private static final int MIN_LENGTH = 8; + private static final int MAX_LENGTH = 20; + + private static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String DIGITS = "0123456789"; + private static final String SPECIAL = "~#@!"; + + public static String generatePassword(){ + String pattern = ServiceRegex.PASSWORD.getPattern(); + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(); + + int length = MIN_LENGTH + random.nextInt(MAX_LENGTH - MIN_LENGTH+1); + + while(true){ + sb.setLength(0); + for(int i=0; i Date: Wed, 13 Mar 2024 19:14:36 +0900 Subject: [PATCH 033/138] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=A0=95=EC=9D=98(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spring mail에서 이메일 전송을 위해 제공하는 JavaMailSender 빈 등록 - 필요한 설정 값들은 application.yml에 정의된 값들을 끌어와 사용 - 빈 등록 로직에서 구글 SMTP 서버를 통해 메일을 전송하기 위한 설정 코드 추가 --- .../java/yeonba/be/config/MailConfig.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 be/src/main/java/yeonba/be/config/MailConfig.java diff --git a/be/src/main/java/yeonba/be/config/MailConfig.java b/be/src/main/java/yeonba/be/config/MailConfig.java new file mode 100644 index 00000000..089bb3d8 --- /dev/null +++ b/be/src/main/java/yeonba/be/config/MailConfig.java @@ -0,0 +1,40 @@ +package yeonba.be.config; + +import java.util.Properties; +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; + +@Configuration +public class MailConfig { + + @Value("${spring.mail.host}") + private String serverHost; + + @Value("${spring.mail.port}") + private int serverPort; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(serverHost); + mailSender.setPort(serverPort); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties properties = mailSender.getJavaMailProperties(); + properties.put("mail.transport.protocol", "smtp"); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", "true"); + + return mailSender; + } +} From 811198dffc35b13035ab8df3962aa1d4b401dd6b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:16:40 +0900 Subject: [PATCH 034/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#3?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단순 텍스트 이메일 전송 기능 구현 - 발신자는 application.yml에 정의해논 구글 SMTP 사용자 이름 값으로 설정 --- .../java/yeonba/be/util/EmailService.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/EmailService.java diff --git a/be/src/main/java/yeonba/be/util/EmailService.java b/be/src/main/java/yeonba/be/util/EmailService.java new file mode 100644 index 00000000..3f64c57d --- /dev/null +++ b/be/src/main/java/yeonba/be/util/EmailService.java @@ -0,0 +1,30 @@ +package yeonba.be.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailService { + + @Value("{spring.mail.username}") + private String serviceEmail; + + private final JavaMailSender mailSender; + + public void sendMail( + String to, + String subject, + String text){ + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(serviceEmail); + mailMessage.setTo(to); + mailMessage.setSubject(subject); + mailMessage.setText(text); + + mailSender.send(mailMessage); + } +} From 25703a778cea363faa9873cd7b80a6a7879649f0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:19:23 +0900 Subject: [PATCH 035/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 임시 비밀번호는 다음 과정을 거친다. 1. 요청 이메일 기반 사용자 조회 2. 임시 비밀번호 생성 3. 사용자 비밀번호, 임시 비밀번호로 변경 4. 임시 비밀번호 발급 메일 전송 임시 비밀번호를 암호화하는 로직은 추후 구현 예정 --- .../yeonba/be/login/service/LoginService.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 be/src/main/java/yeonba/be/login/service/LoginService.java diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java new file mode 100644 index 00000000..f12df5e2 --- /dev/null +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -0,0 +1,42 @@ +package yeonba.be.login.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import yeonba.be.login.dto.request.UserPasswordInquiryRequest; +import yeonba.be.user.entity.User; +import yeonba.be.user.repository.UserQuery; +import yeonba.be.util.EmailService; +import yeonba.be.util.TemporaryPasswordGenerator; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; + private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; + private final UserQuery userQuery; + private final EmailService emailService; + + /* + 임시 비밀번호는 다음 과정을 거친다. + 1. 요청 이메일 기반 사용자 조회 + 2. 임시 비밀번호 생성 + 3. 사용자 비밀번호, 임시 비밀번호로 변경 + 4. 임시 비밀번호 발급 메일 전송 + */ + + // TODO : 비밀번호 암호화 로직 추가 + public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { + + String email = request.getEmail(); + User user = userQuery.findByEmail(email); + + String temporaryPassword = TemporaryPasswordGenerator.generatePassword(); + + String encryptedPassword = temporaryPassword; + user.changePassword(encryptedPassword); + + String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); + emailService.sendMail(email, TEMPORARY_PASSWORD_EMAIL_SUBJECT, text); + } +} From a798b4cbbf88cadc03609bd82e301556345ba0de Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:24:31 +0900 Subject: [PATCH 036/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=83=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A0=95=EC=9D=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/login/service/LoginService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index f12df5e2..373c9ecc 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; @@ -26,6 +27,8 @@ public class LoginService { */ // TODO : 비밀번호 암호화 로직 추가 + + @Transactional public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { String email = request.getEmail(); From 1bfde17642da344c2cd88a1b2830a2168218544a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:25:58 +0900 Subject: [PATCH 037/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20DTO=20=EC=88=98=EC=A0=95(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불필요한 기존 생성자 삭제, 기본 생성자 추가 - 비밀번호 형식 제약 조건 추가 - 코드 정렬 --- .../dto/request/UserPasswordInquiryRequest.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java index 14caf0db..02036027 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java @@ -1,17 +1,19 @@ package yeonba.be.login.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Pattern; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter -@AllArgsConstructor +@NoArgsConstructor public class UserPasswordInquiryRequest { - @Schema( - type = "string", + @Schema(type = "string", description = "임시 비밀번호를 받을 이메일", - example = "mj3242@naver.com" - ) + example = "mj3242@naver.com") + @Pattern( + regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", + message = "유효하지 않은 이메일 형식입니다.") private String email; } From 89c4db54873ee71ae8775f3356d8d47bdc31ca8d Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 13 Mar 2024 19:27:14 +0900 Subject: [PATCH 038/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 임시 비밀번호 발급 API 구현 - 명세 속성 수정 - 코드 정렬 --- .../be/login/controller/LoginController.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index a722d5a7..f74207a0 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,12 +18,16 @@ import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.login.dto.response.UserLoginResponse; import yeonba.be.login.dto.response.UserRefreshTokenResponse; +import yeonba.be.login.service.LoginService; import yeonba.be.util.CustomResponse; @Tag(name = "Login", description = "로그인 관련 API") @RestController +@RequiredArgsConstructor public class LoginController { + private final LoginService loginService; + @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") @PostMapping("/users/join") public ResponseEntity> join( @@ -69,18 +74,14 @@ public ResponseEntity> idInquiry( .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); } - @Operation( - summary = "비밀번호 찾기", - description = "이메일로 임시 비밀번호를 발급받을 수 있습니다." - ) - @ApiResponse( - responseCode = "204", - description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리" - ) + @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") + @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") @PostMapping("/users/help/pw-inquiry") public ResponseEntity> passwordInquiry( @RequestBody UserPasswordInquiryRequest request) { + loginService.sendTemporaryPasswordMail(request); + return ResponseEntity .accepted() .body(new CustomResponse<>()); From 50b998261efac075d978d3ba05ff0201d0b67ed8 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 21:20:12 +0900 Subject: [PATCH 039/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EC=97=90?= =?UTF-8?q?=20=EB=94=B0=EB=A5=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=AC=EB=B0=B0?= =?UTF-8?q?=EC=B9=98(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 브랜치 작업 내역 병합에 따른 코드 재배치 --- be/src/main/java/yeonba/be/user/entity/User.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index ace44b5a..22ed5825 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -206,4 +206,11 @@ public String getGender() { return "여"; } + + public List getProfilePhotoUrls() { + + return this.profilePhotos.stream() + .map(ProfilePhoto::getPhotoUrl) + .toList(); + } } From 45adb7a64e0a157a99df73e3cde0ba2a68c5c152 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 21:24:49 +0900 Subject: [PATCH 040/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EC=97=90?= =?UTF-8?q?=20=EB=94=B0=EB=A5=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=AC=EB=B0=B0?= =?UTF-8?q?=EC=B9=98(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 브랜치 작업 내역 병합에 따른 코드 재배치 --- .../be/user/controller/UserController.java | 32 ++++++------------- .../yeonba/be/user/service/UserService.java | 9 +----- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/controller/UserController.java b/be/src/main/java/yeonba/be/user/controller/UserController.java index 02e582a3..d91ca4b6 100644 --- a/be/src/main/java/yeonba/be/user/controller/UserController.java +++ b/be/src/main/java/yeonba/be/user/controller/UserController.java @@ -21,6 +21,7 @@ import yeonba.be.user.dto.response.UserQueryPageResponse; import yeonba.be.user.service.BlockService; import yeonba.be.user.service.FavoriteService; +import yeonba.be.user.service.UserService; import yeonba.be.util.CustomResponse; @Tag(name = "User", description = "사용자 API") @@ -29,8 +30,9 @@ public class UserController { private final BlockService blockService; - private final ReportService reportService; private final FavoriteService favoriteService; + private final ReportService reportService; + private final UserService userService; @Operation( summary = "이성(다른 사용자) 목록 조회", @@ -50,33 +52,19 @@ public ResponseEntity> users( } - @Operation( - summary = "다른 사용자 프로필 조회", - description = "다른 사용자의 프로필을 조회할 수 있습니다." - ) - @ApiResponse( - responseCode = "200", - description = "사용자 프로필 정상 조회" - ) + @Operation(summary = "다른 사용자 프로필 조회", description = "다른 사용자의 프로필을 조회할 수 있습니다.") + @ApiResponse(responseCode = "200", description = "사용자 프로필 정상 조회") @GetMapping("/users/{userId}") public ResponseEntity> profile( + @RequestAttribute("userId") long userId, @Parameter(description = "조회대상 사용자 ID", example = "1") - @PathVariable long userId) { + @PathVariable("userId") long targetUserId) { + + UserProfileResponse response = userService.getTargetUserProfile(userId, targetUserId); return ResponseEntity .ok() - .body(new CustomResponse<>( - new UserProfileResponse( - "존잘남", - 23, - 177, - "서울시 강남구", - 80, - "저음", - "여우상", - false - ) - )); + .body(new CustomResponse<>(response)); } @Operation(summary = "즐겨찾기 등록", description = "다른 사용자를 자신의 즐겨찾기에 등록할 수 있습니다.") diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 6b1c2956..b14aa08b 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -1,12 +1,10 @@ package yeonba.be.user.service; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import yeonba.be.arrow.repository.ArrowQuery; import yeonba.be.user.dto.response.UserProfileResponse; -import yeonba.be.user.entity.ProfilePhoto; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; @@ -23,15 +21,10 @@ public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) User user = userQuery.findById(userId); User targetUser = userQuery.findById(targetUserId); - List profilePhotosUrls = targetUser - .getProfilePhotos().stream() - .map(ProfilePhoto::getPhotoUrl) - .toList(); - boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); return new UserProfileResponse( - profilePhotosUrls, + targetUser.getProfilePhotoUrls(), targetUser.getGender(), targetUser.getNickname(), targetUser.getArrow(), From c8a4c13253829d0a9ee6f59999a777b362eb85ec Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:02:49 +0900 Subject: [PATCH 041/138] =?UTF-8?q?chore:=20sms=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20sdk=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coolsms 서비스에서 제공하는 java sdk 의존성 추가 --- be/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/be/build.gradle b/be/build.gradle index 5f48c408..e7874bdd 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -40,6 +40,9 @@ dependencies { // spring mail implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' + + // coolsms java sdk + implementation 'net.nurigo:sdk:4.3.0' } tasks.named('bootBuildImage') { From 2a98c5d60927ab21994ea974dfa215d5ba5cd85a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:03:33 +0900 Subject: [PATCH 042/138] =?UTF-8?q?chore:=20sms=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sms 서비스를 이용하기 위해 필요한 설정들 추가 --- be/src/main/resources/application.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index 91b663c1..af281e9c 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -1,18 +1,28 @@ spring: config: import: "classpath:application.properties" + datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + jpa: properties: hibernate: show_sql: true format_sql: true + mail: host: smtp.gmail.com port: 587 username: ${GOOGLE_SMTP_USERNAME} password: ${GOOGLE_SMTP_PASSWORD} + + sms: + api_key: ${SMS_API_KEY} + api_secret: ${SMS_API_SECRET} + provider: https://api.coolsms.co.kr + sender: ${SMS_SENDER} + From d7e4f630007599a19006322519ed7c0efc6bfa35 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:05:40 +0900 Subject: [PATCH 043/138] =?UTF-8?q?feat:=20sms=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EC=9D=98(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coolsms에서는 메시지 전송을 수행할 수 있는 DefaultMessageService 제공 - config에서 application.yml을 통해 값을 주입 받아 DefaultMessageService 빈 등록 --- .../main/java/yeonba/be/config/SmsConfig.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 be/src/main/java/yeonba/be/config/SmsConfig.java diff --git a/be/src/main/java/yeonba/be/config/SmsConfig.java b/be/src/main/java/yeonba/be/config/SmsConfig.java new file mode 100644 index 00000000..e9a539f5 --- /dev/null +++ b/be/src/main/java/yeonba/be/config/SmsConfig.java @@ -0,0 +1,25 @@ +package yeonba.be.config; + +import net.nurigo.sdk.message.service.DefaultMessageService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SmsConfig { + + @Value("${spring.sms.api_key}") + private String apiKey; + + @Value("${spring.sms.api_secret}") + private String apiSecret; + + @Value("${spring.sms.provider}") + private String provider; + + @Bean + public DefaultMessageService defaultMessageService() { + + return new DefaultMessageService(apiKey, apiSecret, provider); + } +} From 4e03b0bcaab99658d7bb4527f3befe1296a5bc97 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:09:30 +0900 Subject: [PATCH 044/138] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apache.common.lang3 라이브러리의 RandomStringUtils 이용 - 영어 대소문자, 숫자로 구성된 6자리 랜덤 인증 코드를 생성 - 영어 대소문자 52개(26+26), 숫자 10개로 총 62개의 선택 가능 문자 존재 - 생성 가능한 인증 코드 수, 약 56억 8천만 개(62^6)로 보안성을 확보하였음 --- .../be/util/VerificationCodeGenerator.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/VerificationCodeGenerator.java diff --git a/be/src/main/java/yeonba/be/util/VerificationCodeGenerator.java b/be/src/main/java/yeonba/be/util/VerificationCodeGenerator.java new file mode 100644 index 00000000..a4983809 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/VerificationCodeGenerator.java @@ -0,0 +1,16 @@ +package yeonba.be.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.RandomStringUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class VerificationCodeGenerator { + + private static final int CODE_LENGTH = 6; + + public static String generateVerificationCode() { + + return RandomStringUtils.random(CODE_LENGTH, 0, 0, true, true); + } +} From e4c64aedc5cb66f6bb6a35547a2adf02ade48e42 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:10:11 +0900 Subject: [PATCH 045/138] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9E=90=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/yeonba/be/util/SmsService.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/SmsService.java diff --git a/be/src/main/java/yeonba/be/util/SmsService.java b/be/src/main/java/yeonba/be/util/SmsService.java new file mode 100644 index 00000000..16d3aaed --- /dev/null +++ b/be/src/main/java/yeonba/be/util/SmsService.java @@ -0,0 +1,26 @@ +package yeonba.be.util; + +import lombok.RequiredArgsConstructor; +import net.nurigo.sdk.message.model.Message; +import net.nurigo.sdk.message.request.SingleMessageSendingRequest; +import net.nurigo.sdk.message.service.DefaultMessageService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SmsService { + + @Value("${spring.sms.sender}") + private String sender; + private final DefaultMessageService messageService; + + public void sendMessage(String to, String text) { + Message message = new Message(); + message.setFrom(sender); + message.setTo(to); + message.setText(text); + + messageService.sendOne(new SingleMessageSendingRequest(message)); + } +} From f4b18679497f56c78865638e270050af88ffce68 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:10:42 +0900 Subject: [PATCH 046/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=95=EC=9D=98(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index 41aada43..6e55b18e 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -9,4 +9,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + + boolean existsByPhoneNumber(String phoneNumber); } From 15ec97d2117246f420e122608db503371ce8a415 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:11:06 +0900 Subject: [PATCH 047/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserQuery.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 9a47f7c5..1fe7e0aa 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -23,4 +23,9 @@ public User findByEmail(String email) { return userRepository.findByEmail(email) .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); } + + public boolean isUserExist(String phoneNumber) { + + return userRepository.existsByPhoneNumber(phoneNumber); + } } From a81a435d98fc484c6714ba20398f629ffb1aa323 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 14 Mar 2024 22:13:11 +0900 Subject: [PATCH 048/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=ED=98=95=EC=8B=9D=20=EC=A0=9C=EC=95=BD=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 010으로 시작하며 0~9까지의 숫자로 이뤄진 11자리 문자열만 허용토록 제약 조건 추가 --- .../dto/request/UserPhoneNumberVerifyRequest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserPhoneNumberVerifyRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserPhoneNumberVerifyRequest.java index 694f0314..584b4398 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserPhoneNumberVerifyRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserPhoneNumberVerifyRequest.java @@ -1,17 +1,20 @@ package yeonba.be.login.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Pattern; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter -@AllArgsConstructor +@NoArgsConstructor public class UserPhoneNumberVerifyRequest { @Schema( type = "string", description = "전화번호", - example = "010-1111-2222" - ) + example = "01011112222") + @Pattern( + regexp = "^010\\d{8}$", + message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") private String phoneNumber; } From 8895e633022f34162453a8b6ca4f83fddcd17944 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 15 Mar 2024 14:03:55 +0900 Subject: [PATCH 049/138] =?UTF-8?q?chore:=20application.yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sms.provider 속성도 application.properties 통해 관리하도록 수정 --- be/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index af281e9c..3764aa3b 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -23,6 +23,6 @@ spring: sms: api_key: ${SMS_API_KEY} api_secret: ${SMS_API_SECRET} - provider: https://api.coolsms.co.kr + provider: ${SMS_PROVIDER} sender: ${SMS_SENDER} From cf77ed4ab0a8d0205714d0d462efcbd92f39754d Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 15 Mar 2024 14:08:33 +0900 Subject: [PATCH 050/138] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20sms=20=EC=A0=84=EC=86=A1=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 요청된 전화번호를 통해 사용자 존재 확인 - 사용자가 존재할 경우만 sms로 인증 코드 전송 --- .../yeonba/be/login/service/LoginService.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 373c9ecc..9dd00eec 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -3,11 +3,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.UserException; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; +import yeonba.be.login.dto.request.UserPhoneNumberVerifyRequest; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; import yeonba.be.util.EmailService; +import yeonba.be.util.SmsService; import yeonba.be.util.TemporaryPasswordGenerator; +import yeonba.be.util.VerificationCodeGenerator; @Service @RequiredArgsConstructor @@ -15,8 +20,12 @@ public class LoginService { private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; + private final String VERIFICATION_CODE_MESSAGE = "연바(연애는 바로 지금) 인증 코드 : %s"; + private final UserQuery userQuery; + private final EmailService emailService; + private final SmsService smsService; /* 임시 비밀번호는 다음 과정을 거친다. @@ -30,7 +39,6 @@ public class LoginService { @Transactional public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { - String email = request.getEmail(); User user = userQuery.findByEmail(email); @@ -42,4 +50,16 @@ public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); emailService.sendMail(email, TEMPORARY_PASSWORD_EMAIL_SUBJECT, text); } + + @Transactional(readOnly = true) + public void sendVerificationCodeMessage(UserPhoneNumberVerifyRequest request) { + String phoneNumber = request.getPhoneNumber(); + if (!userQuery.isUserExist(phoneNumber)) { + throw new GeneralException(UserException.USER_NOT_FOUND); + } + + String code = VerificationCodeGenerator.generateVerificationCode(); + String message = String.format(VERIFICATION_CODE_MESSAGE, code); + smsService.sendMessage(phoneNumber, message); + } } From 1db8b3e00498b4d4c84f8c6f7ddedae641b71e4d Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 15 Mar 2024 14:09:17 +0900 Subject: [PATCH 051/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20API=20=EA=B5=AC=ED=98=84(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/login/controller/LoginController.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index f74207a0..cfc19f7e 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -40,17 +41,13 @@ public ResponseEntity> join( .body(new CustomResponse<>(new UserJoinResponse(createdJwt))); } - @Operation( - summary = "전화번호 인증 코드 전송", - description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다." - ) - @ApiResponse( - responseCode = "204", - description = "전화번호 인증 코드 전송 성공" - ) + @Operation(summary = "전화번호 인증 코드 전송", description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다.") + @ApiResponse(responseCode = "202", description = "전화번호 인증 코드 전송 성공") @PostMapping("/users/help/id-inquiry/verification-code") public ResponseEntity> verifyPhoneNumber( - @RequestBody UserPhoneNumberVerifyRequest request) { + @RequestBody @Valid UserPhoneNumberVerifyRequest request) { + + loginService.sendVerificationCodeMessage(request); return ResponseEntity .accepted() From b032ffc6cc5e46a444a8556344be74853e44096e Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:03:28 +0900 Subject: [PATCH 052/138] =?UTF-8?q?chore:=20spring=20data=20redis=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/be/build.gradle b/be/build.gradle index e7874bdd..49e8c449 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -43,6 +43,9 @@ dependencies { // coolsms java sdk implementation 'net.nurigo:sdk:4.3.0' + + // spring data redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('bootBuildImage') { From ccd2145619001f864d7b538ae2b7b99cdc52a2ea Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:03:53 +0900 Subject: [PATCH 053/138] =?UTF-8?q?chore:=20redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index 3764aa3b..b190fe1c 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -26,3 +26,8 @@ spring: provider: ${SMS_PROVIDER} sender: ${SMS_SENDER} + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + From d06aebc71d83df16e59eda922c31e9b44771e145 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:05:13 +0900 Subject: [PATCH 054/138] =?UTF-8?q?feat:=20redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EC=9D=98(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redis 커넥션 팩토리 빈 등록 - CRUD 연산 지원을 위한 RedisTemplate 빈 등록 --- .../java/yeonba/be/config/RedisConfig.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 be/src/main/java/yeonba/be/config/RedisConfig.java diff --git a/be/src/main/java/yeonba/be/config/RedisConfig.java b/be/src/main/java/yeonba/be/config/RedisConfig.java new file mode 100644 index 00000000..7003b41f --- /dev/null +++ b/be/src/main/java/yeonba/be/config/RedisConfig.java @@ -0,0 +1,35 @@ +package yeonba.be.config; + +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.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} From c96a456b855fabba66425addd6457c080fbddec5 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:08:22 +0900 Subject: [PATCH 055/138] =?UTF-8?q?feat:=20redis=20=ED=82=A4-=EA=B0=92=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5,=EC=A1=B0=ED=9A=8C,=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redis를 이용한 저장, 삭제 기능 지원 유틸 클래스 정의 - 만료 시간을 분 단위로 설정하는 저장 로직 구현 - 기본 조회 로직 구현, Optional 리턴 타입을 통해 null 처리 지원 - 기본 삭제 로직 구현 --- .../main/java/yeonba/be/util/RedisUtil.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/RedisUtil.java diff --git a/be/src/main/java/yeonba/be/util/RedisUtil.java b/be/src/main/java/yeonba/be/util/RedisUtil.java new file mode 100644 index 00000000..cdea4feb --- /dev/null +++ b/be/src/main/java/yeonba/be/util/RedisUtil.java @@ -0,0 +1,29 @@ +package yeonba.be.util; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public void putData(String key, String value, long expiredTimeMinutes) { + redisTemplate.opsForValue() + .set(key, value, expiredTimeMinutes, TimeUnit.MINUTES); + } + + public Optional getData(String key) { + Object value = redisTemplate.opsForValue().get(key); + + return Optional.ofNullable(value); + } + + public void deleteData(String key) { + redisTemplate.delete(key); + } +} From 4f6d00198299bf64a7168f2b6398fc3700481251 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:12:41 +0900 Subject: [PATCH 056/138] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20sms=20=EC=A0=84=EC=86=A1=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 코드 sms 전송 이전 redis에 번호를 키로 코드를 저장하는 로직 추가 - 새 인증 코드 발급 이전 기존 발급 내역 삭제 --- .../main/java/yeonba/be/login/service/LoginService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 9dd00eec..2dec3f10 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -10,6 +10,7 @@ import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; import yeonba.be.util.EmailService; +import yeonba.be.util.RedisUtil; import yeonba.be.util.SmsService; import yeonba.be.util.TemporaryPasswordGenerator; import yeonba.be.util.VerificationCodeGenerator; @@ -18,6 +19,8 @@ @RequiredArgsConstructor public class LoginService { + private final long VERIFICATION_CODE_TTL = 5; + private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; private final String VERIFICATION_CODE_MESSAGE = "연바(연애는 바로 지금) 인증 코드 : %s"; @@ -27,6 +30,7 @@ public class LoginService { private final EmailService emailService; private final SmsService smsService; + private final RedisUtil redisUtil; /* 임시 비밀번호는 다음 과정을 거친다. 1. 요청 이메일 기반 사용자 조회 @@ -58,7 +62,12 @@ public void sendVerificationCodeMessage(UserPhoneNumberVerifyRequest request) { throw new GeneralException(UserException.USER_NOT_FOUND); } + // 인증 코드 재발급 요청시 기존 발급 내역 삭제 + redisUtil.deleteData(phoneNumber); + String code = VerificationCodeGenerator.generateVerificationCode(); + redisUtil.putData(phoneNumber, code, VERIFICATION_CODE_TTL); + String message = String.format(VERIFICATION_CODE_MESSAGE, code); smsService.sendMessage(phoneNumber, message); } From 7cdf64eb4f37f238791dbc9de11ea5b39df4a487 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:46:41 +0900 Subject: [PATCH 057/138] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EC=A0=95=EC=9D=98(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 영어 대소문자, 숫자로 이뤄진 6자리 문자열만 허용하는 정규식 enum 정의 --- be/src/main/java/yeonba/be/util/ServiceRegex.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/util/ServiceRegex.java b/be/src/main/java/yeonba/be/util/ServiceRegex.java index d7531dac..bb006931 100644 --- a/be/src/main/java/yeonba/be/util/ServiceRegex.java +++ b/be/src/main/java/yeonba/be/util/ServiceRegex.java @@ -3,7 +3,8 @@ public enum ServiceRegex { EMAIL("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"), - PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$"); + PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$"), + VERIFICATION_CODE("^[A-Za-z0-9]{6}$"); private final String pattern; From ebc659867f7c1ce459132641e52dfa759d025031 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:51:26 +0900 Subject: [PATCH 058/138] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EC=98=88=EC=99=B8=20enum=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 코드 내역이 존재하지 않는 경우(인증 코드 만료도 포함) - 인증 코드가 일치하지 않는 경우 --- .../yeonba/be/exception/LoginException.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 be/src/main/java/yeonba/be/exception/LoginException.java diff --git a/be/src/main/java/yeonba/be/exception/LoginException.java b/be/src/main/java/yeonba/be/exception/LoginException.java new file mode 100644 index 00000000..39104b71 --- /dev/null +++ b/be/src/main/java/yeonba/be/exception/LoginException.java @@ -0,0 +1,32 @@ +package yeonba.be.exception; + +import org.springframework.http.HttpStatus; + +public enum LoginException implements BaseException { + + VERIFICATION_CODE_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "해당 인증 코드 내역이 존재하지 않습니다."), + + VERIFICATION_CODE_NOT_MATCH( + HttpStatus.BAD_REQUEST, + "인증 코드가 일치하지 않습니다."); + + private final HttpStatus httpStatus; + private final String reason; + + LoginException(HttpStatus httpStatus, String reason) { + this.httpStatus = httpStatus; + this.reason = reason; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getReason() { + return reason; + } +} From 4fc18c7ee01af2000d227dac586c428eea0d3fb2 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:51:51 +0900 Subject: [PATCH 059/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95=EC=9D=98(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index 6e55b18e..7f1b19d9 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -11,4 +11,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByPhoneNumber(String phoneNumber); + + Optional findByPhoneNumber(String phoneNumber); } From 8b6e6c245610e32bff8385ee9ccc57b003407974 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:52:17 +0900 Subject: [PATCH 060/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/repository/UserQuery.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 1fe7e0aa..7bd1b9cd 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -28,4 +28,10 @@ public boolean isUserExist(String phoneNumber) { return userRepository.existsByPhoneNumber(phoneNumber); } + + public User findByPhoneNumber(String phoneNumber) { + + return userRepository.findByPhoneNumber(phoneNumber) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } } From 263b1c7ab981271042cd58cff177bc576e8ccefc Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:52:36 +0900 Subject: [PATCH 061/138] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/login/dto/response/UserIdInquiryResponse.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/response/UserIdInquiryResponse.java b/be/src/main/java/yeonba/be/login/dto/response/UserIdInquiryResponse.java index 8c3c569d..1b2f156a 100644 --- a/be/src/main/java/yeonba/be/login/dto/response/UserIdInquiryResponse.java +++ b/be/src/main/java/yeonba/be/login/dto/response/UserIdInquiryResponse.java @@ -11,7 +11,6 @@ public class UserIdInquiryResponse { @Schema( type = "string", description = "이메일", - example = "mj3242@naver.com" - ) + example = "mj3242@naver.com") private String email; } From 1b8e780fd660050f1b2dbb6eac3adfdb2346cdca Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:55:29 +0900 Subject: [PATCH 062/138] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 번호를 발급한 전화번호 필드 추가 - 인증 코드 필드 명세 속성 수정 - 인증 코드 정규식 제약 조건 추가 - 불필요한 기존 생성자, 기본 생성자로 대체 --- .../dto/request/UserIdInquiryRequest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserIdInquiryRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserIdInquiryRequest.java index a42aa3b0..7bc8dc4d 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserIdInquiryRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserIdInquiryRequest.java @@ -1,17 +1,29 @@ package yeonba.be.login.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Pattern; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter -@AllArgsConstructor +@NoArgsConstructor public class UserIdInquiryRequest { + @Schema( + type = "string", + description = "인증 번호를 받은 번호", + example = "01011112222") + @Pattern( + regexp = "^010\\d{8}$", + message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") + private String phoneNumber; + @Schema( type = "string", description = "아이디 찾기 인증 코드", - example = "CYQR-XIUH-DXMA-LZVN" - ) + example = "A1b2C3") + @Pattern( + regexp = "^[A-Za-z0-9]{6}$", + message = "인증 코드는 6자리로 영어대소문자, 숫자로만 이뤄져야 합니다.") private String verificationCode; } From f4193d811db6aca873ccb3b100ff63bb8441d159 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:58:23 +0900 Subject: [PATCH 063/138] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 아이디 찾기 요청은 인증 코드 발급 전화번호, 인증 코드를 포함한다. 아이디 찾기는 다음 과정을 거쳐 이뤄진다. 1. 발급 전화번호를 기반으로 인증 코드 redis에서 조회 2. 인증 코드 일치 확인 3. 전화번호 기반 사용자 조회 4. 확인한 인증 코드 삭제 5. 찾은 이메일을 응답으로 반환 --- .../yeonba/be/login/service/LoginService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 2dec3f10..154b454c 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -1,12 +1,16 @@ package yeonba.be.login.service; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import yeonba.be.exception.GeneralException; +import yeonba.be.exception.LoginException; import yeonba.be.exception.UserException; +import yeonba.be.login.dto.request.UserIdInquiryRequest; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; import yeonba.be.login.dto.request.UserPhoneNumberVerifyRequest; +import yeonba.be.login.dto.response.UserIdInquiryResponse; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; import yeonba.be.util.EmailService; @@ -71,4 +75,25 @@ public void sendVerificationCodeMessage(UserPhoneNumberVerifyRequest request) { String message = String.format(VERIFICATION_CODE_MESSAGE, code); smsService.sendMessage(phoneNumber, message); } + + @Transactional(readOnly = true) + public UserIdInquiryResponse findEmail(UserIdInquiryRequest request) { + String phoneNumber = request.getPhoneNumber(); + String verificationCode = request.getVerificationCode(); + + // 인증 코드 조회 + String foundVerificationCode = (String) redisUtil.getData(phoneNumber) + .orElseThrow(() -> new GeneralException(LoginException.VERIFICATION_CODE_NOT_FOUND)); + + // 인증 코드 일치 확인 + if (!StringUtils.equals(foundVerificationCode, verificationCode)) { + throw new GeneralException(LoginException.VERIFICATION_CODE_NOT_MATCH); + } + + // 핸드폰 번호 기반 사용자 조회 및 인증 코드 내역 삭제 + User user = userQuery.findByPhoneNumber(phoneNumber); + redisUtil.deleteData(phoneNumber); + + return new UserIdInquiryResponse(user.getEmail()); + } } From d638475244a16a393cbe7fbf34bf9ce117a7a31a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 21:58:53 +0900 Subject: [PATCH 064/138] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/login/controller/LoginController.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index cfc19f7e..aa5efcf4 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -54,21 +54,17 @@ public ResponseEntity> verifyPhoneNumber( .body(new CustomResponse<>()); } - @Operation( - summary = "아이디 찾기", - description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다." - ) - @ApiResponse( - responseCode = "200", - description = "아이디 찾기 정상 처리" - ) + @Operation(summary = "아이디 찾기", description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다.") + @ApiResponse(responseCode = "200", description = "아이디 찾기 정상 처리") @PostMapping("/users/help/id-inquiry") public ResponseEntity> idInquiry( @RequestBody UserIdInquiryRequest request) { + UserIdInquiryResponse response = loginService.findEmail(request); + return ResponseEntity .ok() - .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); + .body(new CustomResponse<>(response)); } @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") From cbace66bac28733336bab856c6c0ac829e9d8071 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 16 Mar 2024 22:10:37 +0900 Subject: [PATCH 065/138] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EC=A0=95=EC=9D=98(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/util/ServiceRegex.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/util/ServiceRegex.java b/be/src/main/java/yeonba/be/util/ServiceRegex.java index bb006931..d405f48f 100644 --- a/be/src/main/java/yeonba/be/util/ServiceRegex.java +++ b/be/src/main/java/yeonba/be/util/ServiceRegex.java @@ -4,7 +4,8 @@ public enum ServiceRegex { EMAIL("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"), PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$"), - VERIFICATION_CODE("^[A-Za-z0-9]{6}$"); + VERIFICATION_CODE("^[A-Za-z0-9]{6}$"), + PHONE_NUMBER("^010\\d{8}$"); private final String pattern; From 46f54d1c327e1c299888d91f72c9bbd683773c03 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 18 Mar 2024 14:21:41 +0900 Subject: [PATCH 066/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 요구사항 변경에 따라 필드들 삭제 및 추가 - 필드들 API 명세 - 문자열 필드들 정규식 제약 조건 추가 - 불필요한 생성자 삭제, 기본 생성자로 대체 --- .../be/login/dto/request/UserJoinRequest.java | 213 +++++++++++++++--- 1 file changed, 185 insertions(+), 28 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java index 26715b4f..18d35ef2 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java @@ -1,51 +1,208 @@ package yeonba.be.login.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; +import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; @Getter @NoArgsConstructor -@AllArgsConstructor public class UserJoinRequest { - @Schema(description = "휴대폰 번호", example = "01012345678") - private String phoneNumber; + @Schema( + type = "string", + description = "전화번호", + example = "01011112222") + @Pattern( + regexp = "^010\\d{8}$", + message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") + @NotBlank(message = "전화번호는 반드시 입력되어야 합니다.") + private String phoneNumber; - @Schema(description = "이름", example = "안민재") - private String name; + @Schema( + type = "string", + description = "비밀번호", + example = "Aa1234!@") + @Pattern( + regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$", + message = """ + 비밀번호는 영어대소문자, 숫자, 특수문자(~#@!)를 + 최소 1자씩 포함하며 8~20자 사이여야 합니다.""") + @NotBlank(message = "비밀번호는 반드시 입력되어야 합니다.") + private String password; - @Schema(description = "생일", example = "980315") - private String birth; + @Schema( + type = "string", + description = "비밀번호 확인값", + example = "Aa1234!@") + @NotBlank(message = "비밀번호 확인값은 반드시 입력되어야 합니다.") + private String passwordConfirmation; - @Schema(description = "성별", example = "남자") - private String gender; + @Schema( + type = "string", + description = "이메일", + example = "mj3242@naver.com") + @Pattern( + regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", + message = "유효하지 않은 이메일 형식입니다.") + @NotBlank(message = "이메일은 반드시 입력되어야 합니다.") + private String email; - @Schema(description = "이메일", example = "anminjae@gmail.com") - private String email; + @Schema( + type = "string", + description = "생년월일", + example = "1998-04-08") + @NotNull(message = "생년월일은 반드시 입력되어야 합니다.") + private LocalDate birth; - @Schema(description = "닉네임", example = "calm_min") - private String nickname; + @Schema( + type = "string", + description = "닉네임", + example = "존잘남") + @Pattern( + regexp = "^[a-zA-Z0-9가-힣]{1,8}$", + message = "닉네임은 공백 없이 영어 대소문자,한글,숫자로 구성되어야 하며 최대 8자까지 가능합니다.") + @NotBlank(message = "닉네임은 반드시 입력되어야 합니다.") + private String nickname; - @Schema(description = "키", example = "160") - private int height; + @Schema( + type = "number", + description = "키", + example = "180") + @Positive(message = "키는 양의 정수여야 합니다.") + private int height; - @Schema(description = "활동 지역", example = "서울시 성북구") - private String activityArea; + @Schema( + type = "string", + description = "체형", + example = "마른체형") + @NotBlank(message = "체형은 반드시 입력되어야 합니다.") + private String bodyType; - @Schema(description = "선호하는 동물상 ID", example = "1") - private int preferAnimalId; + @Schema( + type = "string", + description = "직업", + example = "학생") + @NotBlank(message = "직업은 반드시 입력되어야 합니다.") + private String job; - @Schema(description = "닮은 동물상 ID", example = "1") - private int lookAlikeAnimalId; + @Schema( + type = "string", + description = "활동 지역", + example = "서울") + @NotBlank(message = "활동 지역은 반드시 입력되어야 합니다.") + private String activityArea; - @Schema(description = "프로필 이미지", example = "https://avatars.githubusercontent.com/u/156646513?s=200&v=4") - private String[] images; + @Schema( + type = "string", + description = "MBTI", + example = "ESTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "MBTI는 반드시 입력되어야 합니다.") + private String mbti; - @Schema(description = "목소리", example = "중음") - private String vocalRange; + @Schema( + type = "string", + description = "음역대", + example = "저음") + @NotBlank(message = "음역대는 반드시 입력되어야 합니다.") + private String vocalRange; - @Schema(description = "사진 싱그로율", example = "60") - private int photoSyncRate; + @Schema( + type = "array", + description = "프로필 사진 파일들") + @Size(min = 2, max = 2) + private List profilePhotos; + + @Schema( + type = "number", + description = "사진 싱크로율", + example = "80") + @Min( + value = 80, + message = "사진 싱크로율이 80퍼 이상이어야 가입할 수 있습니다.") + private int photoSyncRate; + + @Schema( + type = "string", + description = "닮은 동물상", + example = "강아지상") + @NotBlank(message = "닮은 동물상은 반드시 입력되어야 합니다.") + private String lookAlikeAnimal; + + @Schema( + type = "string", + description = "선호하는 동물상", + example = "강아지상") + @NotBlank(message = "선호하는 동물상은 반드시 입력되어야 합니다.") + private String preferredAnimal; + + @Schema( + type = "string", + description = "선호하는 지역", + example = "서울") + @NotBlank(message = "선호하는 지역은 반드시 입력되어야 합니다.") + private String preferredArea; + + @Schema( + type = "string", + description = "선호하는 음역대", + example = "저음") + @NotBlank(message = "선호하는 음역대는 반드시 입력되어야 합니다.") + private String preferredVocalRange; + + @Schema( + type = "number", + description = "선호하는 나이 하한", + example = "22") + @Positive(message = "선호하는 나이 하한은 양수여야 합니다.") + private int preferredAgeLowerBound; + + @Schema( + type = "number", + description = "선호하는 나이 상한", + example = "30") + @Positive(message = "선호하는 나이 상한은 양수여야 합니다.") + private int preferredAgeUpperBound; + + @Schema( + type = "number", + description = "선호하는 키 하한", + example = "177") + @Positive(message = "선호하는 키 하한은 양수여야 합니다.") + private int preferredHeightLowerBound; + + @Schema( + type = "number", + description = "선호하는 키 상한", + example = "185") + @Positive(message = "선호하는 키 상한은 양수여야 합니다.") + private int preferredHeightUpperBound; + + @Schema( + type = "string", + description = "선호하는 체형", + example = "마른체형") + @NotBlank(message = "선호하는 체형은 반드시 입력되어야 합니다.") + private String preferredBodyType; + + @Schema( + type = "string", + description = "선호하는 MBTI", + example = "ISTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "선호하는 MBTI는 반드시 입력되어야 합니다.") + private String preferredMbti; } From c42075ead84a6152d548a3426bdf357b989cda6f Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 18 Mar 2024 21:09:12 +0900 Subject: [PATCH 067/138] =?UTF-8?q?refactor:=20PasswordEncryptor=20?= =?UTF-8?q?=EC=B5=9C=EC=83=81=EC=9C=84=20util=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비밀번호 암호화 기능을 전역적으로 사용하기에 패키지 경로 변경 - 사용 위치 수정 --- be/src/main/java/yeonba/be/mypage/service/MyPageService.java | 2 +- .../java/yeonba/be/{mypage => }/util/PasswordEncryptor.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename be/src/main/java/yeonba/be/{mypage => }/util/PasswordEncryptor.java (92%) diff --git a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java index 06831c07..6b3aa21d 100644 --- a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java +++ b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java @@ -14,9 +14,9 @@ import yeonba.be.mypage.dto.request.UserUpdateProfileRequest; import yeonba.be.mypage.dto.response.UserProfileDetailResponse; import yeonba.be.mypage.dto.response.UserSimpleProfileResponse; -import yeonba.be.mypage.util.PasswordEncryptor; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; +import yeonba.be.util.PasswordEncryptor; @Service @RequiredArgsConstructor diff --git a/be/src/main/java/yeonba/be/mypage/util/PasswordEncryptor.java b/be/src/main/java/yeonba/be/util/PasswordEncryptor.java similarity index 92% rename from be/src/main/java/yeonba/be/mypage/util/PasswordEncryptor.java rename to be/src/main/java/yeonba/be/util/PasswordEncryptor.java index 8a4d3156..f6a46bea 100644 --- a/be/src/main/java/yeonba/be/mypage/util/PasswordEncryptor.java +++ b/be/src/main/java/yeonba/be/util/PasswordEncryptor.java @@ -1,4 +1,4 @@ -package yeonba.be.mypage.util; +package yeonba.be.util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -9,7 +9,7 @@ public class PasswordEncryptor { public String encrypt(String password, String salt) { - String encryptedPassword = ""; + String encryptedPassword; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); From 0b8cd060d78e39daa393e325298d35900b37e862 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:48:55 +0900 Subject: [PATCH 068/138] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/yeonba/be/user/entity/User.java | 359 +++++++++--------- 1 file changed, 178 insertions(+), 181 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 22ed5825..b4662727 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -32,185 +32,182 @@ @EqualsAndHashCode(of = "id") public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private boolean gender; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String nickname; - - @Column(nullable = false) - private LocalDate birth; - private int age; - private int height; - - @Column(nullable = false) - private String email; - - @Column(nullable = false) - private String encryptedPassword; - - @Column(nullable = false) - private String salt; - - @Column(nullable = false) - private String phoneNumber; - private int arrow; - private double photoSyncRate; - private boolean inactiveStatus; - - @Column(nullable = false) - private String bodyType; - - @Column(nullable = false) - private String job; - - @Column(nullable = false) - private String mbti; - - @ManyToOne - @JoinColumn(name = "vocal_range_id") - private VocalRange vocalRange; - - @ManyToOne - @JoinColumn(name = "animal_id") - private Animal animal; - - @ManyToOne - @JoinColumn(name = "area_id") - private Area area; - - @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) - List profilePhotos; - - private LocalDateTime lastAccessedAt; - - @CreatedDate - private LocalDateTime createdAt; - - @LastModifiedDate - private LocalDateTime updatedAt; - - private LocalDateTime deletedAt; - - public User( - boolean gender, - String name, - String nickname, - LocalDate birth, - int age, - int height, - String email, - String encryptedPassword, - String salt, - String phoneNumber, - int arrow, - double photoSyncRate, - String bodyType, - String job, - String mbti, - VocalRange vocalRange, - Animal animal, - Area area, - List profilePhotos) { - this.gender = gender; - this.name = name; - this.nickname = nickname; - this.birth = birth; - this.age = age; - this.height = height; - this.email = email; - this.encryptedPassword = encryptedPassword; - this.salt = salt; - this.phoneNumber = phoneNumber; - this.arrow = arrow; - this.photoSyncRate = photoSyncRate; - this.inactiveStatus = true; - this.bodyType = bodyType; - this.job = job; - this.mbti = mbti; - this.vocalRange = vocalRange; - this.animal = animal; - this.area = area; - this.profilePhotos = profilePhotos; - } - - public void validateSameUser(User user) { - - if (!this.equals(user)) { - throw new IllegalArgumentException("동일한 사용자가 아닙니다."); - } - } - - public void validateNotSameUser(User user) { - - if (this.equals(user)) { - throw new IllegalArgumentException("동일한 사용자입니다."); - } - } - - public void changePassword(String encryptedNewPassword) { - - this.encryptedPassword = encryptedNewPassword; - } - - /** - * 삭제된 사용자인지 검증 - */ - public void validateDeletedUser(LocalDateTime now) { - - if (this.deletedAt.isAfter(now)) { - throw new IllegalArgumentException("삭제된 사용자입니다."); - } - } - - public void validateDailyCheck(LocalDate dailyCheckDay) { - - if (this.lastAccessedAt.isAfter(dailyCheckDay.atStartOfDay())) { - throw new GeneralException(ArrowException.ALREADY_CHECKED_USER); - } - } - - public String getRepresentativeProfilePhoto() { - - return this.profilePhotos.get(0).getPhotoUrl(); - } - - public void updateLastAccessedAt(LocalDateTime accessedAt) { - - this.lastAccessedAt = accessedAt; - } - - public void plusArrow(int arrow) { - - this.arrow += arrow; - } - - public void minusArrow(int arrow) { - - if (this.arrow < arrow) { - throw new GeneralException(ArrowException.NOT_ENOUGH_ARROW_TO_SEND); - } - - this.arrow -= arrow; - } - - public String getGender() { - if (this.gender) { - return "남"; - } - - return "여"; - } - - public List getProfilePhotoUrls() { - - return this.profilePhotos.stream() - .map(ProfilePhoto::getPhotoUrl) - .toList(); - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private boolean gender; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private LocalDate birth; + private int age; + private int height; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String encryptedPassword; + + @Column(nullable = false) + private String salt; + + @Column(nullable = false) + private String phoneNumber; + private int arrow; + private int photoSyncRate; + private boolean inactive; + + @Column(nullable = false) + private String bodyType; + + @Column(nullable = false) + private String job; + + @Column(nullable = false) + private String mbti; + + @ManyToOne + @JoinColumn(name = "vocal_range_id") + private VocalRange vocalRange; + + @ManyToOne + @JoinColumn(name = "animal_id") + private Animal animal; + + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; + + @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) + List profilePhotos; + + private LocalDateTime lastAccessedAt; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + private LocalDateTime deletedAt; + + public User( + boolean gender, + String name, + String nickname, + LocalDate birth, + int age, + int height, + String email, + String encryptedPassword, + String salt, + String phoneNumber, + int arrow, + int photoSyncRate, + String bodyType, + String job, + String mbti, + VocalRange vocalRange, + Animal animal, + Area area) { + this.gender = gender; + this.name = name; + this.nickname = nickname; + this.birth = birth; + this.age = age; + this.height = height; + this.email = email; + this.encryptedPassword = encryptedPassword; + this.salt = salt; + this.phoneNumber = phoneNumber; + this.arrow = arrow; + this.photoSyncRate = photoSyncRate; + this.inactive = true; + this.bodyType = bodyType; + this.job = job; + this.mbti = mbti; + this.vocalRange = vocalRange; + this.animal = animal; + this.area = area; + } + + public void validateSameUser(User user) { + if (!this.equals(user)) { + throw new IllegalArgumentException("동일한 사용자가 아닙니다."); + } + } + + public void validateNotSameUser(User user) { + + if (this.equals(user)) { + throw new IllegalArgumentException("동일한 사용자입니다."); + } + } + + public void changePassword(String encryptedNewPassword) { + + this.encryptedPassword = encryptedNewPassword; + } + + /** + * 삭제된 사용자인지 검증 + */ + public void validateDeletedUser(LocalDateTime now) { + + if (this.deletedAt.isAfter(now)) { + throw new IllegalArgumentException("삭제된 사용자입니다."); + } + } + + public void validateDailyCheck(LocalDate dailyCheckDay) { + + if (this.lastAccessedAt.isAfter(dailyCheckDay.atStartOfDay())) { + throw new GeneralException(ArrowException.ALREADY_CHECKED_USER); + } + } + + public String getRepresentativeProfilePhoto() { + + return this.profilePhotos.get(0).getPhotoUrl(); + } + + public void updateLastAccessedAt(LocalDateTime accessedAt) { + + this.lastAccessedAt = accessedAt; + } + + public void plusArrow(int arrow) { + + this.arrow += arrow; + } + + public void minusArrow(int arrow) { + + if (this.arrow < arrow) { + throw new GeneralException(ArrowException.NOT_ENOUGH_ARROW_TO_SEND); + } + + this.arrow -= arrow; + } + + public String getGender() { + if (this.gender) { + return "남"; + } + + return "여"; + } + + public List getProfilePhotoUrls() { + + return this.profilePhotos.stream() + .map(ProfilePhoto::getPhotoUrl) + .toList(); + } } From 55c11240f7a39f62793b1e2b0a6d3a03d4a0cdb5 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:52:07 +0900 Subject: [PATCH 069/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=8F=99=EB=AC=BC=EC=83=81=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/user/repository/animal/AnimalRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java diff --git a/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java b/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java new file mode 100644 index 00000000..51ba3785 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java @@ -0,0 +1,12 @@ +package yeonba.be.user.repository.animal; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.Animal; + +@Repository +public interface AnimalRepository extends JpaRepository { + + Optional findByName(String name); +} From 57acf2726c89d023b6dd53077b575f774b22bdef Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:52:35 +0900 Subject: [PATCH 070/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=8B=AE=EC=9D=80=20=EB=8F=99=EB=AC=BC=EC=83=81=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/animal/AnimalQuery.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java diff --git a/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java new file mode 100644 index 00000000..d862125d --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java @@ -0,0 +1,20 @@ +package yeonba.be.user.repository.animal; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.LoginException; +import yeonba.be.user.entity.Animal; + +@Component +@RequiredArgsConstructor +public class AnimalQuery { + + private final AnimalRepository animalRepository; + + public Animal findByName(String name) { + + return animalRepository.findByName(name) + .orElseThrow(() -> new GeneralException(LoginException.ANIMAL_NOT_FOUND)); + } +} From e7f4166b592f817141440a3de678a056a93e9fe4 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:53:04 +0900 Subject: [PATCH 071/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/user/repository/area/AreaRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java diff --git a/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java b/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java new file mode 100644 index 00000000..c5caff70 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java @@ -0,0 +1,12 @@ +package yeonba.be.user.repository.area; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.Area; + +@Repository +public interface AreaRepository extends JpaRepository { + + Optional findByName(String name); +} From 4aa4e3b19d838adf4a75998739aa80cc54feb97a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:54:00 +0900 Subject: [PATCH 072/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/user/repository/area/AreaQuery.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java diff --git a/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java new file mode 100644 index 00000000..f2b388e4 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java @@ -0,0 +1,20 @@ +package yeonba.be.user.repository.area; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.LoginException; +import yeonba.be.user.entity.Area; + +@Component +@RequiredArgsConstructor +public class AreaQuery { + + private final AreaRepository areaRepository; + + public Area findByName(String name) { + + return areaRepository.findByName(name) + .orElseThrow(() -> new GeneralException(LoginException.AREA_NOT_FOUND)); + } +} From 6830a5b8a42e705c1a5ec9f59c522ae1c10c8ae0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:54:28 +0900 Subject: [PATCH 073/138] =?UTF-8?q?feat:=20=EB=B6=84=EB=A5=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9D=8C=EC=97=AD=EB=8C=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/vocalrange/VocalRangeRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java new file mode 100644 index 00000000..0917cdc7 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java @@ -0,0 +1,12 @@ +package yeonba.be.user.repository.vocalrange; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.VocalRange; + +@Repository +public interface VocalRangeRepository extends JpaRepository { + + Optional findByClassification(String classification); +} From aabb78bf100d0a0ab149135c5e4f4c5a3d01f7fb Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:55:00 +0900 Subject: [PATCH 074/138] =?UTF-8?q?feat:=20=EB=B6=84=EB=A5=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9D=8C=EC=97=AD=EB=8C=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vocalrange/VocalRangeQuery.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java new file mode 100644 index 00000000..427db72d --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java @@ -0,0 +1,20 @@ +package yeonba.be.user.repository.vocalrange; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.LoginException; +import yeonba.be.user.entity.VocalRange; + +@Component +@RequiredArgsConstructor +public class VocalRangeQuery { + + private final VocalRangeRepository vocalRangeRepository; + + public VocalRange findBy(String classification) { + + return vocalRangeRepository.findByClassification(classification) + .orElseThrow(() -> new GeneralException(LoginException.VOCAL_RANGE_NOT_FOUND)); + } +} From 56a2f8dd2a62e9c05f21b8f8b020365739776c44 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:55:17 +0900 Subject: [PATCH 075/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=98=B8=20=EC=A1=B0=EA=B1=B4=20repository=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userpreference/UserPreferenceRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java diff --git a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java new file mode 100644 index 00000000..21fc69b4 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java @@ -0,0 +1,10 @@ +package yeonba.be.user.repository.userpreference; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.UserPreference; + +@Repository +public interface UserPreferenceRepository extends JpaRepository { + +} From 0135d7b21feec424b8674ae117ca25409e9320af Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:55:46 +0900 Subject: [PATCH 076/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=98=B8=20=EC=A1=B0=EA=B1=B4=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userpreference/UserPreferenceCommand.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java diff --git a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java new file mode 100644 index 00000000..c6e8de0d --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java @@ -0,0 +1,17 @@ +package yeonba.be.user.repository.userpreference; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.UserPreference; + +@Component +@RequiredArgsConstructor +public class UserPreferenceCommand { + + private final UserPreferenceRepository userPreferenceRepository; + + public UserPreference save(UserPreference userPreference) { + + return userPreferenceRepository.save(userPreference); + } +} From babedeaa53003b2288368507e9657daa1b152f74 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:58:12 +0900 Subject: [PATCH 077/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=98=B8=EC=A1=B0=EA=B1=B4=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이성 추천, 다른 사용자 검색시 사용되는 사용자 선호조건 엔티티 정의 다음 필드들을 포함한다. - 선호하는 나이 하한, 상한 - 선호하는 키 하한, 상한 - 선호하는 MBTI - 선호하는 체형 - 선호하는 음역대 - 선호하는 지역 - 선호하는 동물상 - 선호 조건 소유 사용자 --- .../yeonba/be/user/entity/UserPreference.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/entity/UserPreference.java diff --git a/be/src/main/java/yeonba/be/user/entity/UserPreference.java b/be/src/main/java/yeonba/be/user/entity/UserPreference.java new file mode 100644 index 00000000..62f13191 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/entity/UserPreference.java @@ -0,0 +1,81 @@ +package yeonba.be.user.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "users_preferences") +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserPreference { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private int ageLowerBound; + + @Column(nullable = false) + private int ageUpperBound; + + @Column(nullable = false) + private int heightLowerBound; + + @Column(nullable = false) + private int heightUpperBound; + + @Column(nullable = false) + private String mbti; + + @Column(nullable = false) + private String bodyType; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "vocal_range_id") + private VocalRange vocalRange; + + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; + + @ManyToOne + @JoinColumn(name = "animal_id") + private Animal animal; + + public UserPreference( + int ageLowerBound, + int ageUpperBound, + int heightLowerBound, + int heightUpperBound, + String mbti, + String bodyType, + User user, + VocalRange vocalRange, + Area area, + Animal animal) { + this.ageLowerBound = ageLowerBound; + this.ageUpperBound = ageUpperBound; + this.heightLowerBound = heightLowerBound; + this.heightUpperBound = heightUpperBound; + this.mbti = mbti; + this.bodyType = bodyType; + this.user = user; + this.vocalRange = vocalRange; + this.area = area; + this.animal = animal; + } +} From cfaa96370f8eabc4c168af3f46a08f81432af278 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:58:39 +0900 Subject: [PATCH 078/138] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/user/repository/UserCommand.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/UserCommand.java diff --git a/be/src/main/java/yeonba/be/user/repository/UserCommand.java b/be/src/main/java/yeonba/be/user/repository/UserCommand.java new file mode 100644 index 00000000..5c22f9af --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/UserCommand.java @@ -0,0 +1,17 @@ +package yeonba.be.user.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.User; + +@Component +@RequiredArgsConstructor +public class UserCommand { + + private final UserRepository userRepository; + + public User save(User user) { + + return userRepository.save(user); + } +} From 841ac4d9e0b65c9dd0b82adff233366317250e22 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:59:34 +0900 Subject: [PATCH 079/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20repository=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profilephoto/ProfilePhotoRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoRepository.java diff --git a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoRepository.java b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoRepository.java new file mode 100644 index 00000000..d81e2409 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoRepository.java @@ -0,0 +1,10 @@ +package yeonba.be.user.repository.profilephoto; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.ProfilePhoto; + +@Repository +public interface ProfilePhotoRepository extends JpaRepository { + +} From 11f814210d8eec30ae04111303479556ba2d304e Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 18:59:54 +0900 Subject: [PATCH 080/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profilephoto/ProfilePhotoCommand.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java diff --git a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java new file mode 100644 index 00000000..75ff4067 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java @@ -0,0 +1,17 @@ +package yeonba.be.user.repository.profilephoto; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.ProfilePhoto; + +@Component +@RequiredArgsConstructor +public class ProfilePhotoCommand { + + private final ProfilePhotoRepository profilePhotoRepository; + + public ProfilePhoto save(ProfilePhoto profilePhoto) { + + return profilePhotoRepository.save(profilePhoto); + } +} From 9ada0ef964b266d2fb94aa0b0b63d4e02a961e07 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:18:58 +0900 Subject: [PATCH 081/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20enum=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다음 예외 enum 들을 정의 - 비밀번호와 비밀번호 확인 값 불일치 상황 - 존재하지 않는 음역대 - 존재하지 않는 동물상 - 존재하지 않는 지역 --- .../yeonba/be/exception/JoinException.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 be/src/main/java/yeonba/be/exception/JoinException.java diff --git a/be/src/main/java/yeonba/be/exception/JoinException.java b/be/src/main/java/yeonba/be/exception/JoinException.java new file mode 100644 index 00000000..1117bf3c --- /dev/null +++ b/be/src/main/java/yeonba/be/exception/JoinException.java @@ -0,0 +1,41 @@ +package yeonba.be.exception; + +import org.springframework.http.HttpStatus; + +public enum JoinException implements BaseException { + + PASSWORD_CONFIRMATION_NOT_MATCH( + HttpStatus.BAD_REQUEST, + "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), + + VOCAL_RANGE_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 음역대입니다."), + + ANIMAL_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 동물상입니다."), + + AREA_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 지역입니다."); + + + private final HttpStatus httpStatus; + private final String reason; + + JoinException(HttpStatus httpStatus, String reason) { + this.httpStatus = httpStatus; + this.reason = reason; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getReason() { + return reason; + } +} From c2758a1f1aeb9e36aac69691dac231ee1271ae1b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:20:14 +0900 Subject: [PATCH 082/138] =?UTF-8?q?refactor:=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9D=8C=EC=97=AD=EB=8C=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잘못된 인덴트 수정, 코드 정렬 - 회원가입 관련 예외 enum을 사용토록 로직 수정 --- .../be/user/repository/vocalrange/VocalRangeQuery.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java index 427db72d..7b58e366 100644 --- a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import yeonba.be.exception.GeneralException; -import yeonba.be.exception.LoginException; +import yeonba.be.exception.JoinException; import yeonba.be.user.entity.VocalRange; @Component @@ -14,7 +14,7 @@ public class VocalRangeQuery { public VocalRange findBy(String classification) { - return vocalRangeRepository.findByClassification(classification) - .orElseThrow(() -> new GeneralException(LoginException.VOCAL_RANGE_NOT_FOUND)); + return vocalRangeRepository.findByClassification(classification) + .orElseThrow(() -> new GeneralException(JoinException.VOCAL_RANGE_NOT_FOUND)); } } From a836b55001658fb16c0ccecdd14cfea413370dd0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:21:09 +0900 Subject: [PATCH 083/138] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=8B=AE=EC=9D=80=20=EB=8F=99=EB=AC=BC?= =?UTF-8?q?=EC=83=81=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잘못된 들여쓰기 수정, 코드 정렬 - 회원가입 관련 예외 enum을 이용하도록 수정 --- .../be/user/repository/animal/AnimalQuery.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java index d862125d..f35d305f 100644 --- a/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java @@ -3,18 +3,18 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import yeonba.be.exception.GeneralException; -import yeonba.be.exception.LoginException; +import yeonba.be.exception.JoinException; import yeonba.be.user.entity.Animal; @Component @RequiredArgsConstructor public class AnimalQuery { - private final AnimalRepository animalRepository; + private final AnimalRepository animalRepository; - public Animal findByName(String name) { + public Animal findByName(String name) { - return animalRepository.findByName(name) - .orElseThrow(() -> new GeneralException(LoginException.ANIMAL_NOT_FOUND)); - } + return animalRepository.findByName(name) + .orElseThrow(() -> new GeneralException(JoinException.ANIMAL_NOT_FOUND)); + } } From d72b3dedded0e90d4b71690b93c8c838185e53d5 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:23:04 +0900 Subject: [PATCH 084/138] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잘못된 들여쓰기, 코드 정렬 수정 - 회원가입 관련 예외 enum 사용토록 수정 --- .../yeonba/be/user/repository/area/AreaQuery.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java index f2b388e4..6e696287 100644 --- a/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java @@ -3,18 +3,18 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import yeonba.be.exception.GeneralException; -import yeonba.be.exception.LoginException; +import yeonba.be.exception.JoinException; import yeonba.be.user.entity.Area; @Component @RequiredArgsConstructor public class AreaQuery { - private final AreaRepository areaRepository; + private final AreaRepository areaRepository; - public Area findByName(String name) { + public Area findByName(String name) { - return areaRepository.findByName(name) - .orElseThrow(() -> new GeneralException(LoginException.AREA_NOT_FOUND)); - } + return areaRepository.findByName(name) + .orElseThrow(() -> new GeneralException(JoinException.AREA_NOT_FOUND)); + } } From 4dc2a884cd574ddfc1cf5e175e2d7352c5baead5 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:23:55 +0900 Subject: [PATCH 085/138] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0,=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC=EB=A1=9C=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/user/entity/ProfilePhoto.java | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java b/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java index 55357ca4..2ede8b48 100644 --- a/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java +++ b/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java @@ -1,6 +1,7 @@ package yeonba.be.user.entity; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -8,31 +9,33 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Table(name = "profile_photos") @Getter @Entity +@EntityListeners(value = AuditingEntityListener.class) @NoArgsConstructor -@AllArgsConstructor public class ProfilePhoto { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne - @JoinColumn(name = "user_id") - private User user; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; - private String photoUrl; - private LocalDateTime createdAt; + private String photoUrl; - public ProfilePhoto(User user, String photoUrl, LocalDateTime createdAt) { - this.user = user; - this.photoUrl = photoUrl; - this.createdAt = createdAt; - } + @CreatedDate + private LocalDateTime createdAt; + + public ProfilePhoto(User user, String photoUrl) { + this.user = user; + this.photoUrl = photoUrl; + } } From 38ad65b57369903e0fabec5ea995a4efb15de1d1 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:25:47 +0900 Subject: [PATCH 086/138] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 생성자 삭제, @ModelAttribute 사용을 위한 생성자 추가 - 잘못된 들여쓰기 수정을 위한 코드 정렬 수정 --- .../be/login/dto/request/UserJoinRequest.java | 397 +++++++++--------- 1 file changed, 207 insertions(+), 190 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java index 18d35ef2..3ac58cc2 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java @@ -9,200 +9,217 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.util.List; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import org.springframework.web.multipart.MultipartFile; @Getter -@NoArgsConstructor +@AllArgsConstructor public class UserJoinRequest { - @Schema( - type = "string", - description = "전화번호", - example = "01011112222") - @Pattern( - regexp = "^010\\d{8}$", - message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") - @NotBlank(message = "전화번호는 반드시 입력되어야 합니다.") - private String phoneNumber; - - @Schema( - type = "string", - description = "비밀번호", - example = "Aa1234!@") - @Pattern( - regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$", - message = """ - 비밀번호는 영어대소문자, 숫자, 특수문자(~#@!)를 - 최소 1자씩 포함하며 8~20자 사이여야 합니다.""") - @NotBlank(message = "비밀번호는 반드시 입력되어야 합니다.") - private String password; - - @Schema( - type = "string", - description = "비밀번호 확인값", - example = "Aa1234!@") - @NotBlank(message = "비밀번호 확인값은 반드시 입력되어야 합니다.") - private String passwordConfirmation; - - @Schema( - type = "string", - description = "이메일", - example = "mj3242@naver.com") - @Pattern( - regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", - message = "유효하지 않은 이메일 형식입니다.") - @NotBlank(message = "이메일은 반드시 입력되어야 합니다.") - private String email; - - @Schema( - type = "string", - description = "생년월일", - example = "1998-04-08") - @NotNull(message = "생년월일은 반드시 입력되어야 합니다.") - private LocalDate birth; - - @Schema( - type = "string", - description = "닉네임", - example = "존잘남") - @Pattern( - regexp = "^[a-zA-Z0-9가-힣]{1,8}$", - message = "닉네임은 공백 없이 영어 대소문자,한글,숫자로 구성되어야 하며 최대 8자까지 가능합니다.") - @NotBlank(message = "닉네임은 반드시 입력되어야 합니다.") - private String nickname; - - @Schema( - type = "number", - description = "키", - example = "180") - @Positive(message = "키는 양의 정수여야 합니다.") - private int height; - - @Schema( - type = "string", - description = "체형", - example = "마른체형") - @NotBlank(message = "체형은 반드시 입력되어야 합니다.") - private String bodyType; - - @Schema( - type = "string", - description = "직업", - example = "학생") - @NotBlank(message = "직업은 반드시 입력되어야 합니다.") - private String job; - - @Schema( - type = "string", - description = "활동 지역", - example = "서울") - @NotBlank(message = "활동 지역은 반드시 입력되어야 합니다.") - private String activityArea; - - @Schema( - type = "string", - description = "MBTI", - example = "ESTJ") - @Pattern( - regexp = "^[EI][SN][TF][JP]$", - message = "유효하지 않은 MBTI 형식입니다.") - @NotBlank(message = "MBTI는 반드시 입력되어야 합니다.") - private String mbti; - - @Schema( - type = "string", - description = "음역대", - example = "저음") - @NotBlank(message = "음역대는 반드시 입력되어야 합니다.") - private String vocalRange; - - @Schema( - type = "array", - description = "프로필 사진 파일들") - @Size(min = 2, max = 2) - private List profilePhotos; - - @Schema( - type = "number", - description = "사진 싱크로율", - example = "80") - @Min( - value = 80, - message = "사진 싱크로율이 80퍼 이상이어야 가입할 수 있습니다.") - private int photoSyncRate; - - @Schema( - type = "string", - description = "닮은 동물상", - example = "강아지상") - @NotBlank(message = "닮은 동물상은 반드시 입력되어야 합니다.") - private String lookAlikeAnimal; - - @Schema( - type = "string", - description = "선호하는 동물상", - example = "강아지상") - @NotBlank(message = "선호하는 동물상은 반드시 입력되어야 합니다.") - private String preferredAnimal; - - @Schema( - type = "string", - description = "선호하는 지역", - example = "서울") - @NotBlank(message = "선호하는 지역은 반드시 입력되어야 합니다.") - private String preferredArea; - - @Schema( - type = "string", - description = "선호하는 음역대", - example = "저음") - @NotBlank(message = "선호하는 음역대는 반드시 입력되어야 합니다.") - private String preferredVocalRange; - - @Schema( - type = "number", - description = "선호하는 나이 하한", - example = "22") - @Positive(message = "선호하는 나이 하한은 양수여야 합니다.") - private int preferredAgeLowerBound; - - @Schema( - type = "number", - description = "선호하는 나이 상한", - example = "30") - @Positive(message = "선호하는 나이 상한은 양수여야 합니다.") - private int preferredAgeUpperBound; - - @Schema( - type = "number", - description = "선호하는 키 하한", - example = "177") - @Positive(message = "선호하는 키 하한은 양수여야 합니다.") - private int preferredHeightLowerBound; - - @Schema( - type = "number", - description = "선호하는 키 상한", - example = "185") - @Positive(message = "선호하는 키 상한은 양수여야 합니다.") - private int preferredHeightUpperBound; - - @Schema( - type = "string", - description = "선호하는 체형", - example = "마른체형") - @NotBlank(message = "선호하는 체형은 반드시 입력되어야 합니다.") - private String preferredBodyType; - - @Schema( - type = "string", - description = "선호하는 MBTI", - example = "ISTJ") - @Pattern( - regexp = "^[EI][SN][TF][JP]$", - message = "유효하지 않은 MBTI 형식입니다.") - @NotBlank(message = "선호하는 MBTI는 반드시 입력되어야 합니다.") - private String preferredMbti; + @Schema( + type = "string", + description = "성별", + example = "남") + @Pattern( + regexp = "^(남|여)$", + message = "성별은 남 또는 여만 가능합니다.") + @NotBlank(message = "성별은 반드시 입력되어야 합니다.") + private String gender; + + @Schema( + type = "string", + description = "전화번호", + example = "01011112222") + @Pattern( + regexp = "^010\\d{8}$", + message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") + @NotBlank(message = "전화번호는 반드시 입력되어야 합니다.") + private String phoneNumber; + + @Schema( + type = "string", + description = "비밀번호", + example = "Aa1234!@") + @Pattern( + regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$", + message = """ + 비밀번호는 영어대소문자, 숫자, 특수문자(~#@!)를 + 최소 1자씩 포함하며 8~20자 사이여야 합니다.""") + @NotBlank(message = "비밀번호는 반드시 입력되어야 합니다.") + private String password; + + @Schema( + type = "string", + description = "비밀번호 확인값", + example = "Aa1234!@") + @NotBlank(message = "비밀번호 확인값은 반드시 입력되어야 합니다.") + private String passwordConfirmation; + + @Schema( + type = "string", + description = "이메일", + example = "mj3242@naver.com") + @Pattern( + regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", + message = "유효하지 않은 이메일 형식입니다.") + @NotBlank(message = "이메일은 반드시 입력되어야 합니다.") + private String email; + + @Schema( + type = "string", + description = "생년월일", + example = "1998-04-08") + @NotNull(message = "생년월일은 반드시 입력되어야 합니다.") + private LocalDate birth; + + @Schema( + type = "string", + description = "이름", + example = "안민재") + @NotBlank(message = " 이름은 반드시 입력되어야 합니다.") + private String name; + + @Schema( + type = "string", + description = "닉네임", + example = "존잘남") + @Pattern( + regexp = "^[a-zA-Z0-9가-힣]{1,8}$", + message = "닉네임은 공백 없이 영어 대소문자,한글,숫자로 구성되어야 하며 최대 8자까지 가능합니다.") + @NotBlank(message = "닉네임은 반드시 입력되어야 합니다.") + private String nickname; + + @Schema( + type = "number", + description = "키", + example = "180") + @Positive(message = "키는 양의 정수여야 합니다.") + private int height; + + @Schema( + type = "string", + description = "체형", + example = "마른체형") + @NotBlank(message = "체형은 반드시 입력되어야 합니다.") + private String bodyType; + + @Schema( + type = "string", + description = "직업", + example = "학생") + @NotBlank(message = "직업은 반드시 입력되어야 합니다.") + private String job; + + @Schema( + type = "string", + description = "활동 지역", + example = "서울") + @NotBlank(message = "활동 지역은 반드시 입력되어야 합니다.") + private String activityArea; + + @Schema( + type = "string", + description = "MBTI", + example = "ESTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "MBTI는 반드시 입력되어야 합니다.") + private String mbti; + + @Schema( + type = "string", + description = "음역대", + example = "저음") + @NotBlank(message = "음역대는 반드시 입력되어야 합니다.") + private String vocalRange; + + @Schema( + type = "array", + description = "프로필 사진 파일들") + @Size(min = 2, max = 2) + private List profilePhotos; + + @Schema( + type = "number", + description = "사진 싱크로율", + example = "80") + @Min( + value = 80, + message = "사진 싱크로율이 80퍼 이상이어야 가입할 수 있습니다.") + private int photoSyncRate; + + @Schema( + type = "string", + description = "닮은 동물상", + example = "강아지상") + @NotBlank(message = "닮은 동물상은 반드시 입력되어야 합니다.") + private String lookAlikeAnimal; + + @Schema( + type = "string", + description = "선호하는 동물상", + example = "강아지상") + @NotBlank(message = "선호하는 동물상은 반드시 입력되어야 합니다.") + private String preferredAnimal; + + @Schema( + type = "string", + description = "선호하는 지역", + example = "서울") + @NotBlank(message = "선호하는 지역은 반드시 입력되어야 합니다.") + private String preferredArea; + + @Schema( + type = "string", + description = "선호하는 음역대", + example = "저음") + @NotBlank(message = "선호하는 음역대는 반드시 입력되어야 합니다.") + private String preferredVocalRange; + + @Schema( + type = "number", + description = "선호하는 나이 하한", + example = "22") + @Positive(message = "선호하는 나이 하한은 양수여야 합니다.") + private int preferredAgeLowerBound; + + @Schema( + type = "number", + description = "선호하는 나이 상한", + example = "30") + @Positive(message = "선호하는 나이 상한은 양수여야 합니다.") + private int preferredAgeUpperBound; + + @Schema( + type = "number", + description = "선호하는 키 하한", + example = "177") + @Positive(message = "선호하는 키 하한은 양수여야 합니다.") + private int preferredHeightLowerBound; + + @Schema( + type = "number", + description = "선호하는 키 상한", + example = "185") + @Positive(message = "선호하는 키 상한은 양수여야 합니다.") + private int preferredHeightUpperBound; + + @Schema( + type = "string", + description = "선호하는 체형", + example = "마른체형") + @NotBlank(message = "선호하는 체형은 반드시 입력되어야 합니다.") + private String preferredBodyType; + + @Schema( + type = "string", + description = "선호하는 MBTI", + example = "ISTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "선호하는 MBTI는 반드시 입력되어야 합니다.") + private String preferredMbti; } From 0cdb65309fcd414c79e32956daaca17349b3fea0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:29:21 +0900 Subject: [PATCH 087/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9D=91=EB=8B=B5=20DTO=20=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입시 바로 서비스를 이용할 수 있도록 access token과 refresh token을 바로 지급하는 형태로 구성 --- .../be/login/dto/response/UserJoinResponse.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java b/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java index 892e2d19..7d193895 100644 --- a/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java +++ b/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java @@ -8,6 +8,15 @@ @AllArgsConstructor public class UserJoinResponse { - @Schema(description = "JWT", example = "header.payload.signature") - private String jwt; + @Schema( + type = "string", + description = "access token", + example = "header.payload,signature") + private String accessToken; + + @Schema( + type = "string", + description = "refresh token", + example = "header.payload.signature") + private String refreshToken; } From 71c6087a9249fa2fa153c9f6cd03c5598e36f166 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:34:40 +0900 Subject: [PATCH 088/138] =?UTF-8?q?feat:=20=EC=84=B1=EB=B3=84=20enum=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 성별 enum 정의 - 문자열 성별 값을 enum으로 치환할 수 있는 편의 메서드 구현 --- .../java/yeonba/be/user/enums/Gender.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/enums/Gender.java diff --git a/be/src/main/java/yeonba/be/user/enums/Gender.java b/be/src/main/java/yeonba/be/user/enums/Gender.java new file mode 100644 index 00000000..b8dba826 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/enums/Gender.java @@ -0,0 +1,26 @@ +package yeonba.be.user.enums; + +import org.apache.commons.lang3.StringUtils; + +public enum Gender { + + MALE("남", true), + FEMALE("여", false); + + public final String genderString; + public final boolean genderBoolean; + + Gender(String genderString, boolean genderBoolean) { + this.genderString = genderString; + this.genderBoolean = genderBoolean; + } + + public static Gender fromGenderString(String genderString) { + if (StringUtils.equals(genderString, MALE.genderString)) { + + return MALE; + } + + return FEMALE; + } +} From 68d7de5d4214b5f9ceb3ef843db16db71b2d6812 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:35:32 +0900 Subject: [PATCH 089/138] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=95=94=ED=98=B8=ED=99=94=EC=8B=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20salt=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/yeonba/be/util/SaltGenerator.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/SaltGenerator.java diff --git a/be/src/main/java/yeonba/be/util/SaltGenerator.java b/be/src/main/java/yeonba/be/util/SaltGenerator.java new file mode 100644 index 00000000..53d19f8c --- /dev/null +++ b/be/src/main/java/yeonba/be/util/SaltGenerator.java @@ -0,0 +1,23 @@ +package yeonba.be.util; + +import java.security.SecureRandom; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.codec.binary.Base64; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SaltGenerator { + + private static final int MIN_SALT_LENGTH = 16; + private static final int MAX_SALT_LENGTH = 256; + + public static String generateRandomSalt(){ + SecureRandom random = new SecureRandom(); + int length = random.nextInt(MIN_SALT_LENGTH, MAX_SALT_LENGTH); + + byte[] salt = new byte[length]; + random.nextBytes(salt); + + return Base64.encodeBase64String(salt); + } +} From 5cecb7d0abf5ce5c147bb76eb591afb1f4f450db Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:36:35 +0900 Subject: [PATCH 090/138] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/login/service/LoginService.java | 97 ++++++++++--------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 154b454c..dbdf72b9 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -23,18 +23,19 @@ @RequiredArgsConstructor public class LoginService { - private final long VERIFICATION_CODE_TTL = 5; + private final long VERIFICATION_CODE_TTL = 5; - private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; - private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; - private final String VERIFICATION_CODE_MESSAGE = "연바(연애는 바로 지금) 인증 코드 : %s"; + private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; + private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; + private final String VERIFICATION_CODE_MESSAGE = "연바(연애는 바로 지금) 인증 코드 : %s"; - private final UserQuery userQuery; + private final UserQuery userQuery; - private final EmailService emailService; - private final SmsService smsService; + private final EmailService emailService; + private final SmsService smsService; + + private final RedisUtil redisUtil; - private final RedisUtil redisUtil; /* 임시 비밀번호는 다음 과정을 거친다. 1. 요청 이메일 기반 사용자 조회 @@ -43,57 +44,57 @@ public class LoginService { 4. 임시 비밀번호 발급 메일 전송 */ - // TODO : 비밀번호 암호화 로직 추가 + // TODO : 비밀번호 암호화 로직 추가 - @Transactional - public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { - String email = request.getEmail(); - User user = userQuery.findByEmail(email); + @Transactional + public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { + String email = request.getEmail(); + User user = userQuery.findByEmail(email); - String temporaryPassword = TemporaryPasswordGenerator.generatePassword(); + String temporaryPassword = TemporaryPasswordGenerator.generatePassword(); - String encryptedPassword = temporaryPassword; - user.changePassword(encryptedPassword); + String encryptedPassword = temporaryPassword; + user.changePassword(encryptedPassword); - String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); - emailService.sendMail(email, TEMPORARY_PASSWORD_EMAIL_SUBJECT, text); - } + String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); + emailService.sendMail(email, TEMPORARY_PASSWORD_EMAIL_SUBJECT, text); + } - @Transactional(readOnly = true) - public void sendVerificationCodeMessage(UserPhoneNumberVerifyRequest request) { - String phoneNumber = request.getPhoneNumber(); - if (!userQuery.isUserExist(phoneNumber)) { - throw new GeneralException(UserException.USER_NOT_FOUND); - } + @Transactional(readOnly = true) + public void sendVerificationCodeMessage(UserPhoneNumberVerifyRequest request) { + String phoneNumber = request.getPhoneNumber(); + if (!userQuery.isUserExist(phoneNumber)) { + throw new GeneralException(UserException.USER_NOT_FOUND); + } - // 인증 코드 재발급 요청시 기존 발급 내역 삭제 - redisUtil.deleteData(phoneNumber); + // 인증 코드 재발급 요청시 기존 발급 내역 삭제 + redisUtil.deleteData(phoneNumber); - String code = VerificationCodeGenerator.generateVerificationCode(); - redisUtil.putData(phoneNumber, code, VERIFICATION_CODE_TTL); + String code = VerificationCodeGenerator.generateVerificationCode(); + redisUtil.putData(phoneNumber, code, VERIFICATION_CODE_TTL); - String message = String.format(VERIFICATION_CODE_MESSAGE, code); - smsService.sendMessage(phoneNumber, message); - } + String message = String.format(VERIFICATION_CODE_MESSAGE, code); + smsService.sendMessage(phoneNumber, message); + } - @Transactional(readOnly = true) - public UserIdInquiryResponse findEmail(UserIdInquiryRequest request) { - String phoneNumber = request.getPhoneNumber(); - String verificationCode = request.getVerificationCode(); + @Transactional(readOnly = true) + public UserIdInquiryResponse findEmail(UserIdInquiryRequest request) { + String phoneNumber = request.getPhoneNumber(); + String verificationCode = request.getVerificationCode(); - // 인증 코드 조회 - String foundVerificationCode = (String) redisUtil.getData(phoneNumber) - .orElseThrow(() -> new GeneralException(LoginException.VERIFICATION_CODE_NOT_FOUND)); + // 인증 코드 조회 + String foundVerificationCode = (String) redisUtil.getData(phoneNumber) + .orElseThrow(() -> new GeneralException(LoginException.VERIFICATION_CODE_NOT_FOUND)); - // 인증 코드 일치 확인 - if (!StringUtils.equals(foundVerificationCode, verificationCode)) { - throw new GeneralException(LoginException.VERIFICATION_CODE_NOT_MATCH); - } + // 인증 코드 일치 확인 + if (!StringUtils.equals(foundVerificationCode, verificationCode)) { + throw new GeneralException(LoginException.VERIFICATION_CODE_NOT_MATCH); + } - // 핸드폰 번호 기반 사용자 조회 및 인증 코드 내역 삭제 - User user = userQuery.findByPhoneNumber(phoneNumber); - redisUtil.deleteData(phoneNumber); + // 핸드폰 번호 기반 사용자 조회 및 인증 코드 내역 삭제 + User user = userQuery.findByPhoneNumber(phoneNumber); + redisUtil.deleteData(phoneNumber); - return new UserIdInquiryResponse(user.getEmail()); - } + return new UserIdInquiryResponse(user.getEmail()); + } } From db35a10e7c650c20eb93eaa4e745e9911f0a0782 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:38:04 +0900 Subject: [PATCH 091/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 엔티티 생성 및 저장 로직 - 프로필 사진 엔티티 생성 및 저장 로직 - 사용자 선호조건 엔티티 생성 및 저장 로직 --- .../yeonba/be/user/service/UserService.java | 174 +++++++++++++++--- 1 file changed, 150 insertions(+), 24 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index b14aa08b..f5bd9a40 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -1,39 +1,165 @@ package yeonba.be.user.service; +import java.time.LocalDate; +import java.time.Period; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import yeonba.be.arrow.repository.ArrowQuery; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.JoinException; +import yeonba.be.login.dto.request.UserJoinRequest; import yeonba.be.user.dto.response.UserProfileResponse; +import yeonba.be.user.entity.Animal; +import yeonba.be.user.entity.Area; +import yeonba.be.user.entity.ProfilePhoto; import yeonba.be.user.entity.User; +import yeonba.be.user.entity.UserPreference; +import yeonba.be.user.entity.VocalRange; +import yeonba.be.user.enums.Gender; +import yeonba.be.user.repository.UserCommand; import yeonba.be.user.repository.UserQuery; +import yeonba.be.user.repository.animal.AnimalQuery; +import yeonba.be.user.repository.area.AreaQuery; +import yeonba.be.user.repository.profilephoto.ProfilePhotoCommand; +import yeonba.be.user.repository.userpreference.UserPreferenceCommand; +import yeonba.be.user.repository.vocalrange.VocalRangeQuery; +import yeonba.be.util.PasswordEncryptor; +import yeonba.be.util.SaltGenerator; @Service @RequiredArgsConstructor public class UserService { - private final ArrowQuery arrowQuery; - private final UserQuery userQuery; - - @Transactional(readOnly = true) - public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { - - User user = userQuery.findById(userId); - User targetUser = userQuery.findById(targetUserId); - - boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); - - return new UserProfileResponse( - targetUser.getProfilePhotoUrls(), - targetUser.getGender(), - targetUser.getNickname(), - targetUser.getArrow(), - targetUser.getAge(), - targetUser.getHeight(), - targetUser.getArea().getName(), - targetUser.getPhotoSyncRate(), - targetUser.getVocalRange().getClassification(), - targetUser.getAnimal().getName(), - isAlreadySentArrow); - } + private final int JOIN_REWARD_ARROWS = 30; + + private final ProfilePhotoCommand profilePhotoCommand; + private final UserCommand userCommand; + private final UserPreferenceCommand userPreferenceCommand; + + private final AnimalQuery animalQuery; + private final AreaQuery areaQuery; + private final ArrowQuery arrowQuery; + private final UserQuery userQuery; + private final VocalRangeQuery vocalRangeQuery; + + private final PasswordEncryptor passwordEncryptor; + + + @Transactional(readOnly = true) + public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { + + User user = userQuery.findById(userId); + User targetUser = userQuery.findById(targetUserId); + + boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); + + return new UserProfileResponse( + targetUser.getProfilePhotoUrls(), + targetUser.getGender(), + targetUser.getNickname(), + targetUser.getArrow(), + targetUser.getAge(), + targetUser.getHeight(), + targetUser.getArea().getName(), + targetUser.getPhotoSyncRate(), + targetUser.getVocalRange().getClassification(), + targetUser.getAnimal().getName(), + isAlreadySentArrow); + } + + @Transactional + public User saveUser(UserJoinRequest request) { + + // 성별 판별 + Gender gender = Gender.fromGenderString(request.getGender()); + + // 나이 계산 + LocalDate birth = request.getBirth(); + int age = Period.between(birth, LocalDate.now()).getYears(); + + // 비밀빈호, 비밀번호 확인 값 일치 확인 + String password = request.getPassword(); + String passwordConfirmation = request.getPasswordConfirmation(); + if (!StringUtils.equals(password, passwordConfirmation)) { + throw new GeneralException(JoinException.PASSWORD_CONFIRMATION_NOT_MATCH); + } + + // salt 생성 및 비밀번호 암호화 + String salt = SaltGenerator.generateRandomSalt(); + String encryptedPassword = passwordEncryptor.encrypt(password, salt); + + // 음역대, 동물상, 지역 조회 + VocalRange vocalRange = vocalRangeQuery.findBy(request.getVocalRange()); + Animal animal = animalQuery.findByName(request.getLookAlikeAnimal()); + Area area = areaQuery.findByName(request.getActivityArea()); + + // 사용자 생성 및 저장 + User user = new User( + gender.genderBoolean, + request.getName(), + request.getNickname(), + request.getBirth(), + age, + request.getHeight(), + request.getEmail(), + encryptedPassword, + salt, + request.getPhoneNumber(), + JOIN_REWARD_ARROWS, + request.getPhotoSyncRate(), + request.getBodyType(), + request.getJob(), + request.getMbti(), + vocalRange, + animal, + area); + + return userCommand.save(user); + } + + + @Transactional + public void saveProfilePhotos(User user, UserJoinRequest request) { + List photoFiles = request.getProfilePhotos(); + + //TODO: 프로필 사진 업로드 및 사진 URL 생성 로직 구현 + + List photosUrls = photoFiles.stream() + .map(MultipartFile::getName) + .toList(); + + List profilePhotos = new ArrayList<>(); + for (String photosUrl : photosUrls) { + profilePhotos.add(new ProfilePhoto(user, photosUrl)); + } + + profilePhotos.forEach(profilePhotoCommand::save); + } + + @Transactional + public void saveUserPreference(User user, UserJoinRequest request) { + + // 선호 음역대, 동물상, 지역 조회 + Animal preferredAnimal = animalQuery.findByName(request.getPreferredAnimal()); + VocalRange preferredVocalRange = vocalRangeQuery.findBy(request.getPreferredVocalRange()); + Area preferredArea = areaQuery.findByName(request.getPreferredArea()); + + UserPreference userPreference = new UserPreference( + request.getPreferredAgeLowerBound(), + request.getPreferredAgeUpperBound(), + request.getPreferredHeightLowerBound(), + request.getPreferredHeightUpperBound(), + request.getMbti(), + request.getBodyType(), + user, + preferredVocalRange, + preferredArea, + preferredAnimal); + userPreferenceCommand.save(userPreference); + } } From c032f5902f7edf91d087899e780dca18227ad2e6 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:43:12 +0900 Subject: [PATCH 092/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B4=88=EC=95=88=20=EA=B5=AC=EC=84=B1(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입 비즈니스 로직은 다음 과정을 거쳐 이뤄진다. 1. 사용자 엔티티 생성 및 저장 2. 프로필 사진 엔티티 생성 및 저장 3. 사용자 선호조건 엔티티 생성 및 저장 4. access token, refresh token 발급 --- .../yeonba/be/user/service/JoinService.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/service/JoinService.java diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java new file mode 100644 index 00000000..fe47a341 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -0,0 +1,30 @@ +package yeonba.be.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import yeonba.be.login.dto.request.UserJoinRequest; +import yeonba.be.login.dto.response.UserJoinResponse; +import yeonba.be.user.entity.User; + +@Service +@RequiredArgsConstructor +public class JoinService { + + private final UserService userService; + + /* + 회원 가입 비즈니스 로직 과정 + 1. 사용자 엔티티 저장 + 2. 프로필 사진 엔티티 저장 + 3. 사용자 선호조건 저장 + */ + @Transactional + public UserJoinResponse join(UserJoinRequest request) { + User user = userService.saveUser(request); + userService.saveProfilePhotos(user, request); + userService.saveUserPreference(user, request); + + return new UserJoinResponse("access_token", "refresh_token"); + } +} From 03bf52661c2924c02d63d18a0c8b2df20dd5f47c Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 21 Mar 2024 21:44:35 +0900 Subject: [PATCH 093/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=B4=88=EC=95=88=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 API 구현 - 잘못된 들여쓰기 수정을 위한 코드 정렬 --- .../be/login/controller/LoginController.java | 177 +++++++++--------- 1 file changed, 90 insertions(+), 87 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index aa5efcf4..e08a128c 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -6,6 +6,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -20,6 +21,7 @@ import yeonba.be.login.dto.response.UserLoginResponse; import yeonba.be.login.dto.response.UserRefreshTokenResponse; import yeonba.be.login.service.LoginService; +import yeonba.be.user.service.JoinService; import yeonba.be.util.CustomResponse; @Tag(name = "Login", description = "로그인 관련 API") @@ -27,91 +29,92 @@ @RequiredArgsConstructor public class LoginController { - private final LoginService loginService; - - @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") - @PostMapping("/users/join") - public ResponseEntity> join( - @RequestBody UserJoinRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserJoinResponse(createdJwt))); - } - - @Operation(summary = "전화번호 인증 코드 전송", description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다.") - @ApiResponse(responseCode = "202", description = "전화번호 인증 코드 전송 성공") - @PostMapping("/users/help/id-inquiry/verification-code") - public ResponseEntity> verifyPhoneNumber( - @RequestBody @Valid UserPhoneNumberVerifyRequest request) { - - loginService.sendVerificationCodeMessage(request); - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "아이디 찾기", description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다.") - @ApiResponse(responseCode = "200", description = "아이디 찾기 정상 처리") - @PostMapping("/users/help/id-inquiry") - public ResponseEntity> idInquiry( - @RequestBody UserIdInquiryRequest request) { - - UserIdInquiryResponse response = loginService.findEmail(request); - - return ResponseEntity - .ok() - .body(new CustomResponse<>(response)); - } - - @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") - @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") - @PostMapping("/users/help/pw-inquiry") - public ResponseEntity> passwordInquiry( - @RequestBody UserPasswordInquiryRequest request) { - - loginService.sendTemporaryPasswordMail(request); - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") - @PostMapping("/users/login") - public ResponseEntity> login( - @RequestBody UserLoginRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>( - new UserLoginResponse( - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" - ))); - } - - @Operation( - summary = "access token 재발급", - description = "refresh token을 통해 access token을 재발급받을 수 있습니다." - ) - @PostMapping("/users/refresh") - public ResponseEntity> refresh( - @RequestBody UserRefreshTokenRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); - } + private final LoginService loginService; + private final JoinService joinService; + + @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") + @PostMapping(path = "/users/join", consumes = "multipart/form-data") + public ResponseEntity> join( + @Valid @ModelAttribute UserJoinRequest request) { + + UserJoinResponse response = joinService.join(request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + + @Operation(summary = "전화번호 인증 코드 전송", description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다.") + @ApiResponse(responseCode = "202", description = "전화번호 인증 코드 전송 성공") + @PostMapping("/users/help/id-inquiry/verification-code") + public ResponseEntity> verifyPhoneNumber( + @RequestBody @Valid UserPhoneNumberVerifyRequest request) { + + loginService.sendVerificationCodeMessage(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "아이디 찾기", description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다.") + @ApiResponse(responseCode = "200", description = "아이디 찾기 정상 처리") + @PostMapping("/users/help/id-inquiry") + public ResponseEntity> idInquiry( + @RequestBody UserIdInquiryRequest request) { + + UserIdInquiryResponse response = loginService.findEmail(request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + + @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") + @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") + @PostMapping("/users/help/pw-inquiry") + public ResponseEntity> passwordInquiry( + @RequestBody UserPasswordInquiryRequest request) { + + loginService.sendTemporaryPasswordMail(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") + @PostMapping("/users/login") + public ResponseEntity> login( + @RequestBody UserLoginRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>( + new UserLoginResponse( + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" + ))); + } + + @Operation( + summary = "access token 재발급", + description = "refresh token을 통해 access token을 재발급받을 수 있습니다." + ) + @PostMapping("/users/refresh") + public ResponseEntity> refresh( + @RequestBody UserRefreshTokenRequest request) { + + String createdJwt = "created"; + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); + } } From f5b9bd8f716ecf3f4765dffcfd93cce1ddbe41ed Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 18:51:42 +0900 Subject: [PATCH 094/138] =?UTF-8?q?refactor:=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9D=8C=EC=97=AD=EB=8C=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/user/repository/vocalrange/VocalRangeQuery.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java index 7b58e366..56dc80c0 100644 --- a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class VocalRangeQuery { - private final VocalRangeRepository vocalRangeRepository; + private final VocalRangeRepository vocalRangeRepository; - public VocalRange findBy(String classification) { + public VocalRange find(String classification) { - return vocalRangeRepository.findByClassification(classification) - .orElseThrow(() -> new GeneralException(JoinException.VOCAL_RANGE_NOT_FOUND)); - } + return vocalRangeRepository.findByClassification(classification) + .orElseThrow(() -> new GeneralException(JoinException.VOCAL_RANGE_NOT_FOUND)); + } } From 0127f4bed1d6f1fb5baf2ef12d23c717125faa80 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 18:53:00 +0900 Subject: [PATCH 095/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 무조건 일괄적 List 형태로 저장되는 특성을 고려해 List 파라미터로 받아 일괄 저장하도록 로직 수정 --- .../repository/profilephoto/ProfilePhotoCommand.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java index 75ff4067..362e77bd 100644 --- a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java +++ b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java @@ -1,5 +1,6 @@ package yeonba.be.user.repository.profilephoto; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import yeonba.be.user.entity.ProfilePhoto; @@ -8,10 +9,10 @@ @RequiredArgsConstructor public class ProfilePhotoCommand { - private final ProfilePhotoRepository profilePhotoRepository; + private final ProfilePhotoRepository profilePhotoRepository; - public ProfilePhoto save(ProfilePhoto profilePhoto) { + public List save(List profilePhotos) { - return profilePhotoRepository.save(profilePhoto); - } + return profilePhotoRepository.saveAll(profilePhotos); + } } From 85181cccb62f62c86314a11000a47278bbf38078 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 19:05:22 +0900 Subject: [PATCH 096/138] =?UTF-8?q?feat:=20S3=20=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로필 사진 업로드는 다음 과정을 거쳐 이뤄진다. 1. 사진 파일들 확장자 검증 2. 파일 식별을 위한 키 생성, profilephoto/{userId}-{photo idx} 형식 3. 사진 파일 업로드 요청 생성 및 업로드 수행 --- .../main/java/yeonba/be/util/S3Service.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/S3Service.java diff --git a/be/src/main/java/yeonba/be/util/S3Service.java b/be/src/main/java/yeonba/be/util/S3Service.java new file mode 100644 index 00000000..e71360e9 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/S3Service.java @@ -0,0 +1,73 @@ +package yeonba.be.util; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import yeonba.be.user.entity.User; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final String ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX = "^(.+)\\.(jpg|jpeg|png)$"; + + private final S3Client s3Client; + + @Value("${S3_BUCKET_NAME}") + private String bucketName; + + /* + 프로필 사진 업로드는 다음 과정을 거쳐 이뤄진다. + 1. 사진 파일들 확장자 검증 + 2. 파일 식별을 위한 키 생성, profilephoto/{userId}-{photo idx} 형식 + 3. 사진 파일 업로드 요청 생성 및 업로드 수행 + */ + public List uploadProfilePhotos(List profilePhotos, User user) { + + if (!validateProfilePhotosExtensions(profilePhotos)) { + throw new IllegalArgumentException("jpg, jpeg, png 확장자 형식의 파일만 허용됩니다."); + } + + List uploadedProfilePhotosUrls = new ArrayList<>(); + long userId = user.getId(); + + for (int profilePhotoIdx = 0; profilePhotoIdx < profilePhotos.size(); profilePhotoIdx++) { + + MultipartFile profilePhoto = profilePhotos.get(profilePhotoIdx); + String key = String.format("profilephoto/%d-%d", userId, profilePhotoIdx); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentDisposition("inline") + .contentType(profilePhoto.getContentType()) + .build(); + + try { + s3Client.putObject(putObjectRequest, + RequestBody.fromInputStream(profilePhoto.getInputStream(), + profilePhoto.getSize())); + uploadedProfilePhotosUrls.add(key); + } catch (Exception e) { + throw new IllegalStateException( + String.format("Failed to upload file : %s", profilePhoto.getOriginalFilename()) + , e); + } + } + + return uploadedProfilePhotosUrls; + } + + private boolean validateProfilePhotosExtensions(List profilePhotos) { + + return profilePhotos.stream() + .allMatch(profilePhoto -> profilePhoto.getOriginalFilename() + .matches(ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX)); + } +} From 449f44246f2eb16551bc5ae87612886e4227f30a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 19:06:46 +0900 Subject: [PATCH 097/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=EC=97=90=20S3=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/user/service/UserService.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index f5bd9a40..76e1d26a 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -2,7 +2,6 @@ import java.time.LocalDate; import java.time.Period; -import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -29,6 +28,7 @@ import yeonba.be.user.repository.userpreference.UserPreferenceCommand; import yeonba.be.user.repository.vocalrange.VocalRangeQuery; import yeonba.be.util.PasswordEncryptor; +import yeonba.be.util.S3Service; import yeonba.be.util.SaltGenerator; @Service @@ -49,6 +49,8 @@ public class UserService { private final PasswordEncryptor passwordEncryptor; + private final S3Service s3Service; + @Transactional(readOnly = true) public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { @@ -94,7 +96,7 @@ public User saveUser(UserJoinRequest request) { String encryptedPassword = passwordEncryptor.encrypt(password, salt); // 음역대, 동물상, 지역 조회 - VocalRange vocalRange = vocalRangeQuery.findBy(request.getVocalRange()); + VocalRange vocalRange = vocalRangeQuery.find(request.getVocalRange()); Animal animal = animalQuery.findByName(request.getLookAlikeAnimal()); Area area = areaQuery.findByName(request.getActivityArea()); @@ -127,18 +129,12 @@ public User saveUser(UserJoinRequest request) { public void saveProfilePhotos(User user, UserJoinRequest request) { List photoFiles = request.getProfilePhotos(); - //TODO: 프로필 사진 업로드 및 사진 URL 생성 로직 구현 - - List photosUrls = photoFiles.stream() - .map(MultipartFile::getName) + List profilePhotoUrls = s3Service.uploadProfilePhotos(photoFiles, user); + List profilePhotos = profilePhotoUrls.stream() + .map(profilePhotoUrl -> new ProfilePhoto(user, profilePhotoUrl)) .toList(); - List profilePhotos = new ArrayList<>(); - for (String photosUrl : photosUrls) { - profilePhotos.add(new ProfilePhoto(user, photosUrl)); - } - - profilePhotos.forEach(profilePhotoCommand::save); + profilePhotoCommand.save(profilePhotos); } @Transactional @@ -146,7 +142,7 @@ public void saveUserPreference(User user, UserJoinRequest request) { // 선호 음역대, 동물상, 지역 조회 Animal preferredAnimal = animalQuery.findByName(request.getPreferredAnimal()); - VocalRange preferredVocalRange = vocalRangeQuery.findBy(request.getPreferredVocalRange()); + VocalRange preferredVocalRange = vocalRangeQuery.find(request.getPreferredVocalRange()); Area preferredArea = areaQuery.findByName(request.getPreferredArea()); UserPreference userPreference = new UserPreference( From 3fbedcbaf1e705dda50cbcd964763f68380d9b7f Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:54:03 +0900 Subject: [PATCH 098/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=BB=AC=EB=A0=89=EC=85=98=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20&=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NPE 방지를 위한 컬렉션 필드 초기화 설정 - 순수한 객체 관계를 고려한 프로필 사진 엔티티 연관관계 설정 로직 구성 --- be/src/main/java/yeonba/be/user/entity/User.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index b4662727..72b174c0 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -13,6 +13,7 @@ import jakarta.persistence.Table; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; import lombok.EqualsAndHashCode; @@ -85,7 +86,7 @@ public class User { private Area area; @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) - List profilePhotos; + List profilePhotos = new ArrayList<>(); private LocalDateTime lastAccessedAt; @@ -210,4 +211,8 @@ public List getProfilePhotoUrls() { .map(ProfilePhoto::getPhotoUrl) .toList(); } + + public void updateProfilePhotos(List profilePhotos) { + this.profilePhotos = profilePhotos; + } } From b8a1aa00a318728318266bb135256442d40f540e Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:55:27 +0900 Subject: [PATCH 099/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20=EB=8B=89=EB=84=A4=EC=9E=84,?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 사용중인 닉네임 확인 로직 정의 - 이미 사용중인 이메일 확인 로직 정의 --- .../java/yeonba/be/user/repository/UserRepository.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index 7f1b19d9..dc09bf03 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -8,9 +8,13 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmail(String email); - boolean existsByPhoneNumber(String phoneNumber); + boolean existsByPhoneNumber(String phoneNumber); - Optional findByPhoneNumber(String phoneNumber); + Optional findByPhoneNumber(String phoneNumber); + + boolean existsByNickname(String nickname); + + boolean existsByEmail(String email); } From 348aa8f7c66059b746587e39a17d9da44c46d7ac Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:55:53 +0900 Subject: [PATCH 100/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20=EB=8B=89=EB=84=A4=EC=9E=84,?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/user/repository/UserQuery.java | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 7bd1b9cd..2c582830 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -10,28 +10,38 @@ @RequiredArgsConstructor public class UserQuery { - private final UserRepository userRepository; + private final UserRepository userRepository; - public User findById(long userId) { + public User findById(long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } - public User findByEmail(String email) { + public User findByEmail(String email) { - return userRepository.findByEmail(email) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } - public boolean isUserExist(String phoneNumber) { + public boolean isUserExist(String phoneNumber) { - return userRepository.existsByPhoneNumber(phoneNumber); - } + return userRepository.existsByPhoneNumber(phoneNumber); + } - public User findByPhoneNumber(String phoneNumber) { + public User findByPhoneNumber(String phoneNumber) { - return userRepository.findByPhoneNumber(phoneNumber) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findByPhoneNumber(phoneNumber) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } + + public boolean isAlreadyUsedNickname(String nickname) { + + return userRepository.existsByNickname(nickname); + } + + public boolean isAlreadyUsedEmail(String email) { + + return userRepository.existsByEmail(email); + } } From 0a123bf808b733ca1076902f0a985c15e63bff22 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:58:02 +0900 Subject: [PATCH 101/138] =?UTF-8?q?feat:=20=EB=9E=9C=EB=8D=A4=20salt=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - salt의 길이 고정 32비트(4바이트)로 변경 - 32비트 길이 salt 만으로 대부분의 보안적 위협 충분히 커버 가능 - base64 인코딩에 따라 로직에서 생성되는 salt 문자열의 길이는 8 --- .../java/yeonba/be/util/SaltGenerator.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/SaltGenerator.java b/be/src/main/java/yeonba/be/util/SaltGenerator.java index 53d19f8c..64b61fd4 100644 --- a/be/src/main/java/yeonba/be/util/SaltGenerator.java +++ b/be/src/main/java/yeonba/be/util/SaltGenerator.java @@ -5,19 +5,23 @@ import lombok.NoArgsConstructor; import org.apache.commons.codec.binary.Base64; +/* + - 32비트(4바이트) 길이 salt 만으로 대부분의 보안적 위협은 충분히 커버 가능 + - base64 인코딩에 따라 아래 로직에서 생성되는 salt 문자열의 길이는 8 + */ + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class SaltGenerator { - private static final int MIN_SALT_LENGTH = 16; - private static final int MAX_SALT_LENGTH = 256; + private static final int SALT_BYTE_LENGTH = 4; + + public static String generateRandomSalt() { - public static String generateRandomSalt(){ - SecureRandom random = new SecureRandom(); - int length = random.nextInt(MIN_SALT_LENGTH, MAX_SALT_LENGTH); + SecureRandom random = new SecureRandom(); - byte[] salt = new byte[length]; - random.nextBytes(salt); + byte[] salt = new byte[SALT_BYTE_LENGTH]; + random.nextBytes(salt); - return Base64.encodeBase64String(salt); - } + return Base64.encodeBase64String(salt); + } } From eb97f428bc4f8771eda7b0a36e45ba379985592a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:58:44 +0900 Subject: [PATCH 102/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC/?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=98=88=EC=99=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 사용 중인 이메일 예외 enum 정의 - 이미 사용 중인 닉네임 예외 enum 정의 --- .../java/yeonba/be/exception/JoinException.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/exception/JoinException.java b/be/src/main/java/yeonba/be/exception/JoinException.java index 1117bf3c..5b939598 100644 --- a/be/src/main/java/yeonba/be/exception/JoinException.java +++ b/be/src/main/java/yeonba/be/exception/JoinException.java @@ -6,19 +6,27 @@ public enum JoinException implements BaseException { PASSWORD_CONFIRMATION_NOT_MATCH( HttpStatus.BAD_REQUEST, - "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), + "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), VOCAL_RANGE_NOT_FOUND( HttpStatus.BAD_REQUEST, - "존재하지 않는 음역대입니다."), + "존재하지 않는 음역대입니다."), ANIMAL_NOT_FOUND( HttpStatus.BAD_REQUEST, - "존재하지 않는 동물상입니다."), + "존재하지 않는 동물상입니다."), AREA_NOT_FOUND( HttpStatus.BAD_REQUEST, - "존재하지 않는 지역입니다."); + "존재하지 않는 지역입니다."), + + ALREADY_USED_EMAIL( + HttpStatus.BAD_REQUEST, + "이미 사용 중인 이메일입니다."), + + ALREADY_USED_NICKNAME( + HttpStatus.BAD_REQUEST, + "이미 사용 중인 닉네임입니다."); private final HttpStatus httpStatus; From d0531eb689af77da4105692027e3edcde6d4ba48 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 21:59:52 +0900 Subject: [PATCH 103/138] =?UTF-8?q?feat:=20s3=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EB=90=9C=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/yeonba/be/util/S3Service.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/be/src/main/java/yeonba/be/util/S3Service.java b/be/src/main/java/yeonba/be/util/S3Service.java index e71360e9..be4ad246 100644 --- a/be/src/main/java/yeonba/be/util/S3Service.java +++ b/be/src/main/java/yeonba/be/util/S3Service.java @@ -8,7 +8,9 @@ import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import yeonba.be.user.entity.ProfilePhoto; import yeonba.be.user.entity.User; @Service @@ -64,6 +66,25 @@ public List uploadProfilePhotos(List profilePhotos, User return uploadedProfilePhotosUrls; } + public void deleteProfilePhotos(List profilePhotos) { + + for (ProfilePhoto profilePhoto : profilePhotos) { + String photoUrl = profilePhoto.getPhotoUrl(); + + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(photoUrl) + .build(); + + try { + s3Client.deleteObject(deleteObjectRequest); + } catch (Exception e) { + throw new IllegalStateException( + String.format("Failed to delete file, key : %s", photoUrl), e); + } + } + } + private boolean validateProfilePhotosExtensions(List profilePhotos) { return profilePhotos.stream() From cf87994b9de0b6b4bb1f1aaae7371a03d63e4979 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 22:01:11 +0900 Subject: [PATCH 104/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 엔티티 저장 로직에 이미 사용 중인 이메일/닉네임 검증 로직 추가 - 프로필 사진 저장 로직에 사용자 엔티티와의 연관관계 설정 로직 추가 --- .../yeonba/be/user/service/UserService.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 76e1d26a..988fbe31 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -77,12 +77,15 @@ public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) @Transactional public User saveUser(UserJoinRequest request) { - // 성별 판별 - Gender gender = Gender.fromGenderString(request.getGender()); + // 이미 사용 중인 이메일인지 확인 + if (userQuery.isAlreadyUsedEmail(request.getEmail())) { + throw new GeneralException(JoinException.ALREADY_USED_EMAIL); + } - // 나이 계산 - LocalDate birth = request.getBirth(); - int age = Period.between(birth, LocalDate.now()).getYears(); + // 이미 사용 중인 닉네임인지 확인 + if (userQuery.isAlreadyUsedNickname(request.getNickname())) { + throw new GeneralException(JoinException.ALREADY_USED_NICKNAME); + } // 비밀빈호, 비밀번호 확인 값 일치 확인 String password = request.getPassword(); @@ -91,6 +94,13 @@ public User saveUser(UserJoinRequest request) { throw new GeneralException(JoinException.PASSWORD_CONFIRMATION_NOT_MATCH); } + // 성별 판별 + Gender gender = Gender.fromGenderString(request.getGender()); + + // 나이 계산 + LocalDate birth = request.getBirth(); + int age = Period.between(birth, LocalDate.now()).getYears(); + // salt 생성 및 비밀번호 암호화 String salt = SaltGenerator.generateRandomSalt(); String encryptedPassword = passwordEncryptor.encrypt(password, salt); @@ -124,7 +134,6 @@ public User saveUser(UserJoinRequest request) { return userCommand.save(user); } - @Transactional public void saveProfilePhotos(User user, UserJoinRequest request) { List photoFiles = request.getProfilePhotos(); @@ -135,6 +144,7 @@ public void saveProfilePhotos(User user, UserJoinRequest request) { .toList(); profilePhotoCommand.save(profilePhotos); + user.updateProfilePhotos(profilePhotos); } @Transactional From d59ab4ac1d5900b8f821ce2e9247cdcf62e48224 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 22:02:53 +0900 Subject: [PATCH 105/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=82=AD=EC=A0=9C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=95=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 과정중 트랜잭션 롤백 발생시 업로드된 프로필 사진 삭제를 위해 정의 - 업로드된 프로필 사진 엔티티를 필드를 통해 전달 --- .../be/user/event/ProfilePhotoDeletionEvent.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java new file mode 100644 index 00000000..4b7280c9 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java @@ -0,0 +1,13 @@ +package yeonba.be.user.event; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import yeonba.be.user.entity.ProfilePhoto; + +@Getter +@RequiredArgsConstructor +public class ProfilePhotoDeletionEvent { + + private final List profilePhotos; +} \ No newline at end of file From a7cc07da53f3eec43aa3d94bc83d083da64f8f76 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 22:04:15 +0900 Subject: [PATCH 106/138] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=82=AD=EC=A0=9C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=EC=A0=95=EC=9D=98(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 과정중 트랜잭션 롤백 발생시 프로필 사진 삭제 이벤트 발생 - 이벤트를 통해 삭제 대상인 프로필 사진 엔티티들을 전달 받음 - S3 서비스를 통해 해당 엔티티들의 삭제 작업 진행 --- .../event/ProfilePhotoDeletionListener.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java new file mode 100644 index 00000000..0a203c63 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java @@ -0,0 +1,20 @@ +package yeonba.be.user.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import yeonba.be.util.S3Service; + +@Component +@RequiredArgsConstructor +public class ProfilePhotoDeletionListener { + + private final S3Service s3Service; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void onProfilePhotoDeletion(ProfilePhotoDeletionEvent event) { + + s3Service.deleteProfilePhotos(event.getProfilePhotos()); + } +} From 535343d4337ecefa205852c4c309cf005a4b644b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Fri, 22 Mar 2024 22:06:27 +0900 Subject: [PATCH 107/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EB=A1=A4=EB=B0=B1=EC=8B=9C=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=EB=90=9C=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=82=AD=EC=A0=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입 도중 예외 상황 등으로 인해 트랜잭션 롤백시 이미 s3에 업로드된 프로필 사진들을 삭제하기 위한 이벤트 발행 로직 추가 --- .../main/java/yeonba/be/user/service/JoinService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index fe47a341..54b5060c 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -1,17 +1,20 @@ package yeonba.be.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import yeonba.be.login.dto.request.UserJoinRequest; import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.user.entity.User; +import yeonba.be.user.event.ProfilePhotoDeletionEvent; @Service @RequiredArgsConstructor public class JoinService { - private final UserService userService; + private final ApplicationEventPublisher eventPublisher; + private final UserService userService; /* 회원 가입 비즈니스 로직 과정 @@ -23,6 +26,12 @@ public class JoinService { public UserJoinResponse join(UserJoinRequest request) { User user = userService.saveUser(request); userService.saveProfilePhotos(user, request); + + // 트랜잭션 롤백시 업로드된 프로필 사진 삭제를 위해 이벤트 발행 + ProfilePhotoDeletionEvent event = new ProfilePhotoDeletionEvent( + user.getProfilePhotos()); + eventPublisher.publishEvent(event); + userService.saveUserPreference(user, request); return new UserJoinResponse("access_token", "refresh_token"); From 05a9d25cd1072eae658b60d82435447316833c5f Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 23 Mar 2024 18:23:52 +0900 Subject: [PATCH 108/138] =?UTF-8?q?chore:=20jjwt=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jwt 활용을 위해 사용되는 jjwt 라이브러리 의존성 추가 --- be/build.gradle | 74 ++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/be/build.gradle b/be/build.gradle index 49e8c449..cf4bf033 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -1,57 +1,63 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.8' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.1.8' + id 'io.spring.dependency-management' version '1.1.4' } group = 'yeonba' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'io.projectreactor:reactor-test' - - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - - // aws s3 - implementation 'software.amazon.awssdk:aws-sdk-java:2.16.83' - implementation 'software.amazon.awssdk:s3:2.16.83' - - // spring mail - implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' - - // coolsms java sdk - implementation 'net.nurigo:sdk:4.3.0' - - // spring data redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + // aws s3 + implementation 'software.amazon.awssdk:aws-sdk-java:2.16.83' + implementation 'software.amazon.awssdk:s3:2.16.83' + + // spring mail + implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' + + // coolsms java sdk + implementation 'net.nurigo:sdk:4.3.0' + + // spring data redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // jwt + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'com.sun.xml.bind:jaxb-impl:2.3.3' + implementation 'com.sun.xml.bind:jaxb-core:2.3.0.1' } tasks.named('bootBuildImage') { - builder = 'paketobuildpacks/builder-jammy-base:latest' + builder = 'paketobuildpacks/builder-jammy-base:latest' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } From 5de7d2097c5307f7b56a61c618c3168e3c67349b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 23 Mar 2024 18:39:48 +0900 Subject: [PATCH 109/138] =?UTF-8?q?feat:=20access/refresh=20token=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 시크릿 키 값의 경우 무작위 64바이트 바이너리를 16진수로 인코딩한 문자열 사용 - jwt 포맷의 access/refresh token 발급 로직 추가 - 만료 시간을 계산하는 공통 로직, 별도의 메서드로 추출 - 사용자 엔티티를 기반으로 jwt를 생성하는 공통 로직, 별도의 메서드로 추출 - access token의 보편적인 존속기간은 수 시간가량 따라서 8시간으로 설정 - refresh token의 보편적인 존속기간은 수 일에서 수 개월 내외 따라서 10일로 설정 - jwt의 일반적인 관행에 따라 사용자 이메일을 subject로 설정 - 사용자 엔티티의 PK를 클레임으로 설정하여 인가시 사용할 수 있도록 구성 - 검증 로직에선 시그니처의 유효성과 JWT 만료 여부를 검증 --- be/src/main/java/yeonba/be/util/JwtUtil.java | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 be/src/main/java/yeonba/be/util/JwtUtil.java diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java new file mode 100644 index 00000000..b3e6636a --- /dev/null +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -0,0 +1,73 @@ +package yeonba.be.util; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.User; + +@Component +public class JwtUtil { + + private final Duration ACCESS_TOKEN_DURATION = Duration.of(8, ChronoUnit.HOURS); + private final Duration REFRESH_TOKEN_DURATION = Duration.of(10, ChronoUnit.DAYS); + + @Value("${JWT_SECRET}") + private String jwtSecret; + + public String generateAccessToken(User user, Date issuedAt) { + + Date expiredAt = getExpiredAt(issuedAt, ACCESS_TOKEN_DURATION); + + return generateUserJwt(user, issuedAt, expiredAt); + } + + public String generateRefreshToken(User user, Date issuedAt) { + + Date expiredAt = getExpiredAt(issuedAt, REFRESH_TOKEN_DURATION); + + return generateUserJwt(user, issuedAt, expiredAt); + } + + private Date getExpiredAt(Date issuedAt, Duration duration) { + + Instant instant = issuedAt.toInstant() + .plusMillis(duration.toMillis()); + + return Date.from(instant); + } + + private String generateUserJwt( + User user, + Date issuedAt, + Date expiredAt) { + + return Jwts.builder() + .setSubject(user.getEmail()) + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .claim("userId", user.getId()) + .signWith(SignatureAlgorithm.HS256, jwtSecret) + .compact(); + } + + public void validateJwt(String jwt) { + + try { + Jwts.parser() + .setSigningKey(jwtSecret) + .parseClaimsJws(jwt); + + } catch (SignatureException e) { + throw new IllegalStateException("유효하지 않은 JWT 시그니처입니다.", e); + } catch (ExpiredJwtException e) { + throw new IllegalStateException("만료된 JWT입니다.", e); + } + } +} From c94a7e49368a204cd117ddb117bf38f723e7a643 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 23 Mar 2024 18:41:15 +0900 Subject: [PATCH 110/138] =?UTF-8?q?feat:=20refresh=20token=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 refresh token을 저장할 필드 추가 - 사용자 refresh token 업데이트 로직 추가 --- be/src/main/java/yeonba/be/user/entity/User.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 72b174c0..c4403c1b 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -73,6 +73,8 @@ public class User { @Column(nullable = false) private String mbti; + private String refreshToken; + @ManyToOne @JoinColumn(name = "vocal_range_id") private VocalRange vocalRange; @@ -215,4 +217,8 @@ public List getProfilePhotoUrls() { public void updateProfilePhotos(List profilePhotos) { this.profilePhotos = profilePhotos; } + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } From afdaeef889ae5a290b8b4e3aa34fc1cedfae4b70 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sat, 23 Mar 2024 18:44:32 +0900 Subject: [PATCH 111/138] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81,=20access/refresh=20token=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 정상 완료의 결과로 access token과 refresh token 제공 - 회원가입 비즈니스 로직에 access token, refresh token 발급 로직 추가 - 새 refresh token 발급시 사용자 refresh token 업데이트 수행 --- .../java/yeonba/be/user/service/JoinService.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index 54b5060c..96494b8f 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -1,5 +1,6 @@ package yeonba.be.user.service; +import java.util.Date; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -8,6 +9,7 @@ import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.user.entity.User; import yeonba.be.user.event.ProfilePhotoDeletionEvent; +import yeonba.be.util.JwtUtil; @Service @RequiredArgsConstructor @@ -16,11 +18,15 @@ public class JoinService { private final ApplicationEventPublisher eventPublisher; private final UserService userService; + private final JwtUtil jwtUtil; + /* 회원 가입 비즈니스 로직 과정 1. 사용자 엔티티 저장 2. 프로필 사진 엔티티 저장 3. 사용자 선호조건 저장 + 4. access token 및 refresh token 발급 + 5. 사용자 refresh token 업데이트 */ @Transactional public UserJoinResponse join(UserJoinRequest request) { @@ -34,6 +40,14 @@ public UserJoinResponse join(UserJoinRequest request) { userService.saveUserPreference(user, request); - return new UserJoinResponse("access_token", "refresh_token"); + // access token, refresh token 발급 + Date issuedAt = new Date(); + String accessToken = jwtUtil.generateAccessToken(user, issuedAt); + String refreshToken = jwtUtil.generateRefreshToken(user, issuedAt); + + // 사용자 refresh token 업데이트 + user.updateRefreshToken(refreshToken); + + return new UserJoinResponse(accessToken, refreshToken); } } From 3f2a04c7f06d6442f2e35861dd3a896b159eb2d8 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 15:25:01 +0900 Subject: [PATCH 112/138] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/yeonba/be/user/service/JoinService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index 96494b8f..1732c722 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -21,12 +21,12 @@ public class JoinService { private final JwtUtil jwtUtil; /* - 회원 가입 비즈니스 로직 과정 - 1. 사용자 엔티티 저장 - 2. 프로필 사진 엔티티 저장 - 3. 사용자 선호조건 저장 - 4. access token 및 refresh token 발급 - 5. 사용자 refresh token 업데이트 + 회원 가입 비즈니스 로직 과정 + 1. 사용자 엔티티 저장 + 2. 프로필 사진 엔티티 저장 + 3. 사용자 선호조건 저장 + 4. access token 및 refresh token 발급 + 5. 사용자 refresh token 업데이트 */ @Transactional public UserJoinResponse join(UserJoinRequest request) { From c2f30c4bb27b83ef36cf47f7ce4683155a99c66a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 15:39:59 +0900 Subject: [PATCH 113/138] =?UTF-8?q?refactor:=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=20=EA=B0=92=20Geneder=20enum=20=EB=B3=80=ED=99=98=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/yeonba/be/user/enums/Gender.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/enums/Gender.java b/be/src/main/java/yeonba/be/user/enums/Gender.java index b8dba826..6e57d776 100644 --- a/be/src/main/java/yeonba/be/user/enums/Gender.java +++ b/be/src/main/java/yeonba/be/user/enums/Gender.java @@ -4,23 +4,24 @@ public enum Gender { - MALE("남", true), - FEMALE("여", false); + MALE("남", true), + FEMALE("여", false); - public final String genderString; - public final boolean genderBoolean; + public final String genderString; + public final boolean genderBoolean; - Gender(String genderString, boolean genderBoolean) { - this.genderString = genderString; - this.genderBoolean = genderBoolean; - } + Gender(String genderString, boolean genderBoolean) { + this.genderString = genderString; + this.genderBoolean = genderBoolean; + } - public static Gender fromGenderString(String genderString) { - if (StringUtils.equals(genderString, MALE.genderString)) { + public static Gender from(String genderString) { - return MALE; - } + if (StringUtils.equals(genderString, MALE.genderString)) { - return FEMALE; - } + return MALE; + } + + return FEMALE; + } } From f0173c4d98fd2a3ba53b57dabb5a26c398fe025e Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 15:40:44 +0900 Subject: [PATCH 114/138] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/service/JoinService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index 1732c722..bb0d8301 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -30,6 +30,7 @@ public class JoinService { */ @Transactional public UserJoinResponse join(UserJoinRequest request) { + User user = userService.saveUser(request); userService.saveProfilePhotos(user, request); From 5143d9d7aa78295990e531dca13d117831dae228 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 15:42:31 +0900 Subject: [PATCH 115/138] =?UTF-8?q?refactor:=20Gender=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EA=B0=92=20=EB=B3=80=ED=99=98=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=82=AC=EC=9A=A9=20=EC=9C=84=EC=B9=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/service/UserService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 988fbe31..b8ed43a3 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -95,7 +95,7 @@ public User saveUser(UserJoinRequest request) { } // 성별 판별 - Gender gender = Gender.fromGenderString(request.getGender()); + Gender gender = Gender.from(request.getGender()); // 나이 계산 LocalDate birth = request.getBirth(); @@ -136,6 +136,7 @@ public User saveUser(UserJoinRequest request) { @Transactional public void saveProfilePhotos(User user, UserJoinRequest request) { + List photoFiles = request.getProfilePhotos(); List profilePhotoUrls = s3Service.uploadProfilePhotos(photoFiles, user); From f90f944c937f9f2274bd8d9ae13c7b54032f5c9b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 16:24:07 +0900 Subject: [PATCH 116/138] =?UTF-8?q?feat:=20salt=20=EA=B8=B8=EC=9D=B4=2032?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보안적으로 안전한 32자로 salt 길이 변경 --- be/src/main/java/yeonba/be/util/SaltGenerator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/SaltGenerator.java b/be/src/main/java/yeonba/be/util/SaltGenerator.java index 64b61fd4..56ee849c 100644 --- a/be/src/main/java/yeonba/be/util/SaltGenerator.java +++ b/be/src/main/java/yeonba/be/util/SaltGenerator.java @@ -6,14 +6,14 @@ import org.apache.commons.codec.binary.Base64; /* - - 32비트(4바이트) 길이 salt 만으로 대부분의 보안적 위협은 충분히 커버 가능 - - base64 인코딩에 따라 아래 로직에서 생성되는 salt 문자열의 길이는 8 + - 32바이트 길이의 salt만으로 대부분의 보안적 위협은 커버 가능 + - 이하 로직을 통해 생성되는 salt 문자열의 길이는 약 44자 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class SaltGenerator { - private static final int SALT_BYTE_LENGTH = 4; + private static final int SALT_BYTE_LENGTH = 32; public static String generateRandomSalt() { From a477b66b69fb45040fbb1592a712f3f842266609 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 17:15:56 +0900 Subject: [PATCH 117/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/config/SmsConfig.java | 6 +++--- be/src/main/java/yeonba/be/user/entity/User.java | 16 +++++++++++++--- be/src/main/java/yeonba/be/util/SmsService.java | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/be/src/main/java/yeonba/be/config/SmsConfig.java b/be/src/main/java/yeonba/be/config/SmsConfig.java index e9a539f5..7ab42382 100644 --- a/be/src/main/java/yeonba/be/config/SmsConfig.java +++ b/be/src/main/java/yeonba/be/config/SmsConfig.java @@ -8,13 +8,13 @@ @Configuration public class SmsConfig { - @Value("${spring.sms.api_key}") + @Value("${SMS_API_KEY}") private String apiKey; - @Value("${spring.sms.api_secret}") + @Value("${SMS_API_SECRET}") private String apiSecret; - @Value("${spring.sms.provider}") + @Value("${SMS_PROVIDER}") private String provider; @Bean diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 11b7e6fb..18e275e9 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -72,6 +72,8 @@ public class User { @Column(nullable = false) private String mbti; + private String refreshToken; + @ManyToOne @JoinColumn(name = "vocal_range_id") private VocalRange vocalRange; @@ -115,8 +117,7 @@ public User( String mbti, VocalRange vocalRange, Animal animal, - Area area, - List profilePhotos) { + Area area) { this.gender = gender; this.name = name; this.nickname = nickname; @@ -136,7 +137,6 @@ public User( this.vocalRange = vocalRange; this.animal = animal; this.area = area; - this.profilePhotos = profilePhotos; } public void validateSameUser(User user) { @@ -214,4 +214,14 @@ public List getProfilePhotoUrls() { .map(ProfilePhoto::getPhotoUrl) .toList(); } + + public void updateProfilePhotos(List profilePhotos) { + + this.profilePhotos = profilePhotos; + } + + public void updateRefreshToken(String refreshToken) { + + this.refreshToken = refreshToken; + } } diff --git a/be/src/main/java/yeonba/be/util/SmsService.java b/be/src/main/java/yeonba/be/util/SmsService.java index 16d3aaed..c350e441 100644 --- a/be/src/main/java/yeonba/be/util/SmsService.java +++ b/be/src/main/java/yeonba/be/util/SmsService.java @@ -11,7 +11,7 @@ @RequiredArgsConstructor public class SmsService { - @Value("${spring.sms.sender}") + @Value("${SMS_SENDER}") private String sender; private final DefaultMessageService messageService; From 19e6d6873a5b13c971dbfd6c4e7a3c910d259400 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 27 Mar 2024 17:37:05 +0900 Subject: [PATCH 118/138] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/login/service/LoginService.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 462b289e..4c130c4b 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -14,6 +14,7 @@ import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; import yeonba.be.util.EmailService; +import yeonba.be.util.PasswordEncryptor; import yeonba.be.util.RedisUtil; import yeonba.be.util.SmsService; import yeonba.be.util.TemporaryPasswordGenerator; @@ -34,18 +35,16 @@ public class LoginService { private final EmailService emailService; private final SmsService smsService; + private final PasswordEncryptor passwordEncryptor; private final RedisUtil redisUtil; - /* - 임시 비밀번호는 다음 과정을 거친다. - 1. 요청 이메일 기반 사용자 조회 - 2. 임시 비밀번호 생성 - 3. 사용자 비밀번호, 임시 비밀번호로 변경 - 4. 임시 비밀번호 발급 메일 전송 - */ - - // TODO : 비밀번호 암호화 로직 추가 - + /* + 임시 비밀번호는 다음 과정을 거친다. + 1. 요청 이메일 기반 사용자 조회 + 2. 임시 비밀번호 생성 + 3. 사용자 비밀번호, 임시 비밀번호로 변경 + 4. 임시 비밀번호 발급 메일 전송 + */ @Transactional public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { @@ -54,7 +53,7 @@ public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { String temporaryPassword = TemporaryPasswordGenerator.generatePassword(); - String encryptedPassword = temporaryPassword; + String encryptedPassword = passwordEncryptor.encrypt(temporaryPassword, user.getSalt()); user.changePassword(encryptedPassword); String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); From cad76e7105b337e9ab20d84025f3fac2ccb5195a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 28 Mar 2024 16:00:41 +0900 Subject: [PATCH 119/138] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeonba/be/exception/JoinException.java | 67 +-- .../be/login/controller/LoginController.java | 192 ++++---- .../be/login/dto/request/UserJoinRequest.java | 410 +++++++++--------- .../login/dto/response/UserJoinResponse.java | 20 +- .../response/UserProfileDetailResponse.java | 5 +- .../dto/response/UserProfileResponse.java | 108 ++--- .../yeonba/be/user/entity/ProfilePhoto.java | 27 +- .../yeonba/be/user/entity/UserPreference.java | 100 ++--- .../java/yeonba/be/user/enums/Gender.java | 28 +- .../user/event/ProfilePhotoDeletionEvent.java | 2 +- .../event/ProfilePhotoDeletionListener.java | 10 +- .../be/user/repository/UserCommand.java | 8 +- .../yeonba/be/user/repository/UserQuery.java | 44 +- .../be/user/repository/UserRepository.java | 10 +- .../user/repository/animal/AnimalQuery.java | 10 +- .../repository/animal/AnimalRepository.java | 2 +- .../be/user/repository/area/AreaQuery.java | 10 +- .../user/repository/area/AreaRepository.java | 2 +- .../profilephoto/ProfilePhotoCommand.java | 8 +- .../userpreference/UserPreferenceCommand.java | 8 +- .../vocalrange/VocalRangeQuery.java | 10 +- .../vocalrange/VocalRangeRepository.java | 2 +- .../yeonba/be/user/service/JoinService.java | 72 +-- .../yeonba/be/user/service/UserService.java | 268 ++++++------ be/src/main/java/yeonba/be/util/JwtUtil.java | 80 ++-- .../main/java/yeonba/be/util/S3Service.java | 148 +++---- .../java/yeonba/be/util/SaltGenerator.java | 18 +- 27 files changed, 836 insertions(+), 833 deletions(-) diff --git a/be/src/main/java/yeonba/be/exception/JoinException.java b/be/src/main/java/yeonba/be/exception/JoinException.java index 5b939598..ab86b32d 100644 --- a/be/src/main/java/yeonba/be/exception/JoinException.java +++ b/be/src/main/java/yeonba/be/exception/JoinException.java @@ -4,46 +4,49 @@ public enum JoinException implements BaseException { - PASSWORD_CONFIRMATION_NOT_MATCH( - HttpStatus.BAD_REQUEST, - "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), + PASSWORD_CONFIRMATION_NOT_MATCH( + HttpStatus.BAD_REQUEST, + "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), - VOCAL_RANGE_NOT_FOUND( - HttpStatus.BAD_REQUEST, - "존재하지 않는 음역대입니다."), + VOCAL_RANGE_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 음역대입니다."), - ANIMAL_NOT_FOUND( - HttpStatus.BAD_REQUEST, - "존재하지 않는 동물상입니다."), + ANIMAL_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 동물상입니다."), - AREA_NOT_FOUND( - HttpStatus.BAD_REQUEST, - "존재하지 않는 지역입니다."), + AREA_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "존재하지 않는 지역입니다."), - ALREADY_USED_EMAIL( - HttpStatus.BAD_REQUEST, - "이미 사용 중인 이메일입니다."), + ALREADY_USED_EMAIL( + HttpStatus.BAD_REQUEST, + "이미 사용 중인 이메일입니다."), - ALREADY_USED_NICKNAME( - HttpStatus.BAD_REQUEST, - "이미 사용 중인 닉네임입니다."); + ALREADY_USED_NICKNAME( + HttpStatus.BAD_REQUEST, + "이미 사용 중인 닉네임입니다."); - private final HttpStatus httpStatus; - private final String reason; + private final HttpStatus httpStatus; + private final String reason; - JoinException(HttpStatus httpStatus, String reason) { - this.httpStatus = httpStatus; - this.reason = reason; - } + JoinException(HttpStatus httpStatus, String reason) { - @Override - public HttpStatus getHttpStatus() { - return httpStatus; - } + this.httpStatus = httpStatus; + this.reason = reason; + } - @Override - public String getReason() { - return reason; - } + @Override + public HttpStatus getHttpStatus() { + + return httpStatus; + } + + @Override + public String getReason() { + + return reason; + } } diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index 2b4b1d55..84372f25 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -29,100 +29,100 @@ @RequiredArgsConstructor public class LoginController { - private final LoginService loginService; - private final JoinService joinService; - - @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") - @PostMapping(path = "/users/join", consumes = "multipart/form-data") - public ResponseEntity> join( - @Valid @ModelAttribute UserJoinRequest request) { - - UserJoinResponse response = joinService.join(request); - - return ResponseEntity - .ok() - .body(new CustomResponse<>(response)); - } - - @Operation( - summary = "전화번호 인증 코드 전송", - description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다." - ) - @ApiResponse( - responseCode = "204", - description = "전화번호 인증 코드 전송 성공" - ) - @PostMapping("/users/help/id-inquiry/verification-code") - public ResponseEntity> verifyPhoneNumber( - @RequestBody UserPhoneNumberVerifyRequest request) { - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation( - summary = "아이디 찾기", - description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다." - ) - @ApiResponse( - responseCode = "200", - description = "아이디 찾기 정상 처리" - ) - @PostMapping("/users/help/id-inquiry") - public ResponseEntity> idInquiry( - @RequestBody UserIdInquiryRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); - } - - @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") - @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") - @PostMapping("/users/help/pw-inquiry") - public ResponseEntity> passwordInquiry( - @Valid @RequestBody UserPasswordInquiryRequest request) { - - loginService.sendTemporaryPasswordMail(request); - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") - @PostMapping("/users/login") - public ResponseEntity> login( - @RequestBody UserLoginRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>( - new UserLoginResponse( - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" - ))); - } - - @Operation( - summary = "access token 재발급", - description = "refresh token을 통해 access token을 재발급받을 수 있습니다." - ) - @PostMapping("/users/refresh") - public ResponseEntity> refresh( - @RequestBody UserRefreshTokenRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); - } + private final LoginService loginService; + private final JoinService joinService; + + @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") + @PostMapping(path = "/users/join", consumes = "multipart/form-data") + public ResponseEntity> join( + @Valid @ModelAttribute UserJoinRequest request) { + + UserJoinResponse response = joinService.join(request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + + @Operation( + summary = "전화번호 인증 코드 전송", + description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다." + ) + @ApiResponse( + responseCode = "204", + description = "전화번호 인증 코드 전송 성공" + ) + @PostMapping("/users/help/id-inquiry/verification-code") + public ResponseEntity> verifyPhoneNumber( + @RequestBody UserPhoneNumberVerifyRequest request) { + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation( + summary = "아이디 찾기", + description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다." + ) + @ApiResponse( + responseCode = "200", + description = "아이디 찾기 정상 처리" + ) + @PostMapping("/users/help/id-inquiry") + public ResponseEntity> idInquiry( + @RequestBody UserIdInquiryRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); + } + + @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") + @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") + @PostMapping("/users/help/pw-inquiry") + public ResponseEntity> passwordInquiry( + @Valid @RequestBody UserPasswordInquiryRequest request) { + + loginService.sendTemporaryPasswordMail(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") + @PostMapping("/users/login") + public ResponseEntity> login( + @RequestBody UserLoginRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>( + new UserLoginResponse( + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" + ))); + } + + @Operation( + summary = "access token 재발급", + description = "refresh token을 통해 access token을 재발급받을 수 있습니다." + ) + @PostMapping("/users/refresh") + public ResponseEntity> refresh( + @RequestBody UserRefreshTokenRequest request) { + + String createdJwt = "created"; + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); + } } diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java index 3ac58cc2..bafd2d58 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserJoinRequest.java @@ -17,209 +17,209 @@ @AllArgsConstructor public class UserJoinRequest { - @Schema( - type = "string", - description = "성별", - example = "남") - @Pattern( - regexp = "^(남|여)$", - message = "성별은 남 또는 여만 가능합니다.") - @NotBlank(message = "성별은 반드시 입력되어야 합니다.") - private String gender; - - @Schema( - type = "string", - description = "전화번호", - example = "01011112222") - @Pattern( - regexp = "^010\\d{8}$", - message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") - @NotBlank(message = "전화번호는 반드시 입력되어야 합니다.") - private String phoneNumber; - - @Schema( - type = "string", - description = "비밀번호", - example = "Aa1234!@") - @Pattern( - regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$", - message = """ - 비밀번호는 영어대소문자, 숫자, 특수문자(~#@!)를 - 최소 1자씩 포함하며 8~20자 사이여야 합니다.""") - @NotBlank(message = "비밀번호는 반드시 입력되어야 합니다.") - private String password; - - @Schema( - type = "string", - description = "비밀번호 확인값", - example = "Aa1234!@") - @NotBlank(message = "비밀번호 확인값은 반드시 입력되어야 합니다.") - private String passwordConfirmation; - - @Schema( - type = "string", - description = "이메일", - example = "mj3242@naver.com") - @Pattern( - regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", - message = "유효하지 않은 이메일 형식입니다.") - @NotBlank(message = "이메일은 반드시 입력되어야 합니다.") - private String email; - - @Schema( - type = "string", - description = "생년월일", - example = "1998-04-08") - @NotNull(message = "생년월일은 반드시 입력되어야 합니다.") - private LocalDate birth; - - @Schema( - type = "string", - description = "이름", - example = "안민재") - @NotBlank(message = " 이름은 반드시 입력되어야 합니다.") - private String name; - - @Schema( - type = "string", - description = "닉네임", - example = "존잘남") - @Pattern( - regexp = "^[a-zA-Z0-9가-힣]{1,8}$", - message = "닉네임은 공백 없이 영어 대소문자,한글,숫자로 구성되어야 하며 최대 8자까지 가능합니다.") - @NotBlank(message = "닉네임은 반드시 입력되어야 합니다.") - private String nickname; - - @Schema( - type = "number", - description = "키", - example = "180") - @Positive(message = "키는 양의 정수여야 합니다.") - private int height; - - @Schema( - type = "string", - description = "체형", - example = "마른체형") - @NotBlank(message = "체형은 반드시 입력되어야 합니다.") - private String bodyType; - - @Schema( - type = "string", - description = "직업", - example = "학생") - @NotBlank(message = "직업은 반드시 입력되어야 합니다.") - private String job; - - @Schema( - type = "string", - description = "활동 지역", - example = "서울") - @NotBlank(message = "활동 지역은 반드시 입력되어야 합니다.") - private String activityArea; - - @Schema( - type = "string", - description = "MBTI", - example = "ESTJ") - @Pattern( - regexp = "^[EI][SN][TF][JP]$", - message = "유효하지 않은 MBTI 형식입니다.") - @NotBlank(message = "MBTI는 반드시 입력되어야 합니다.") - private String mbti; - - @Schema( - type = "string", - description = "음역대", - example = "저음") - @NotBlank(message = "음역대는 반드시 입력되어야 합니다.") - private String vocalRange; - - @Schema( - type = "array", - description = "프로필 사진 파일들") - @Size(min = 2, max = 2) - private List profilePhotos; - - @Schema( - type = "number", - description = "사진 싱크로율", - example = "80") - @Min( - value = 80, - message = "사진 싱크로율이 80퍼 이상이어야 가입할 수 있습니다.") - private int photoSyncRate; - - @Schema( - type = "string", - description = "닮은 동물상", - example = "강아지상") - @NotBlank(message = "닮은 동물상은 반드시 입력되어야 합니다.") - private String lookAlikeAnimal; - - @Schema( - type = "string", - description = "선호하는 동물상", - example = "강아지상") - @NotBlank(message = "선호하는 동물상은 반드시 입력되어야 합니다.") - private String preferredAnimal; - - @Schema( - type = "string", - description = "선호하는 지역", - example = "서울") - @NotBlank(message = "선호하는 지역은 반드시 입력되어야 합니다.") - private String preferredArea; - - @Schema( - type = "string", - description = "선호하는 음역대", - example = "저음") - @NotBlank(message = "선호하는 음역대는 반드시 입력되어야 합니다.") - private String preferredVocalRange; - - @Schema( - type = "number", - description = "선호하는 나이 하한", - example = "22") - @Positive(message = "선호하는 나이 하한은 양수여야 합니다.") - private int preferredAgeLowerBound; - - @Schema( - type = "number", - description = "선호하는 나이 상한", - example = "30") - @Positive(message = "선호하는 나이 상한은 양수여야 합니다.") - private int preferredAgeUpperBound; - - @Schema( - type = "number", - description = "선호하는 키 하한", - example = "177") - @Positive(message = "선호하는 키 하한은 양수여야 합니다.") - private int preferredHeightLowerBound; - - @Schema( - type = "number", - description = "선호하는 키 상한", - example = "185") - @Positive(message = "선호하는 키 상한은 양수여야 합니다.") - private int preferredHeightUpperBound; - - @Schema( - type = "string", - description = "선호하는 체형", - example = "마른체형") - @NotBlank(message = "선호하는 체형은 반드시 입력되어야 합니다.") - private String preferredBodyType; - - @Schema( - type = "string", - description = "선호하는 MBTI", - example = "ISTJ") - @Pattern( - regexp = "^[EI][SN][TF][JP]$", - message = "유효하지 않은 MBTI 형식입니다.") - @NotBlank(message = "선호하는 MBTI는 반드시 입력되어야 합니다.") - private String preferredMbti; + @Schema( + type = "string", + description = "성별", + example = "남") + @Pattern( + regexp = "^(남|여)$", + message = "성별은 남 또는 여만 가능합니다.") + @NotBlank(message = "성별은 반드시 입력되어야 합니다.") + private String gender; + + @Schema( + type = "string", + description = "전화번호", + example = "01011112222") + @Pattern( + regexp = "^010\\d{8}$", + message = "전화번호는 11자리 010으로 시작하며 하이픈(-) 없이 0~9의 숫자로 이뤄져야 합니다.") + @NotBlank(message = "전화번호는 반드시 입력되어야 합니다.") + private String phoneNumber; + + @Schema( + type = "string", + description = "비밀번호", + example = "Aa1234!@") + @Pattern( + regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$", + message = """ + 비밀번호는 영어대소문자, 숫자, 특수문자(~#@!)를 + 최소 1자씩 포함하며 8~20자 사이여야 합니다.""") + @NotBlank(message = "비밀번호는 반드시 입력되어야 합니다.") + private String password; + + @Schema( + type = "string", + description = "비밀번호 확인값", + example = "Aa1234!@") + @NotBlank(message = "비밀번호 확인값은 반드시 입력되어야 합니다.") + private String passwordConfirmation; + + @Schema( + type = "string", + description = "이메일", + example = "mj3242@naver.com") + @Pattern( + regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", + message = "유효하지 않은 이메일 형식입니다.") + @NotBlank(message = "이메일은 반드시 입력되어야 합니다.") + private String email; + + @Schema( + type = "string", + description = "생년월일", + example = "1998-04-08") + @NotNull(message = "생년월일은 반드시 입력되어야 합니다.") + private LocalDate birth; + + @Schema( + type = "string", + description = "이름", + example = "안민재") + @NotBlank(message = " 이름은 반드시 입력되어야 합니다.") + private String name; + + @Schema( + type = "string", + description = "닉네임", + example = "존잘남") + @Pattern( + regexp = "^[a-zA-Z0-9가-힣]{1,8}$", + message = "닉네임은 공백 없이 영어 대소문자,한글,숫자로 구성되어야 하며 최대 8자까지 가능합니다.") + @NotBlank(message = "닉네임은 반드시 입력되어야 합니다.") + private String nickname; + + @Schema( + type = "number", + description = "키", + example = "180") + @Positive(message = "키는 양의 정수여야 합니다.") + private int height; + + @Schema( + type = "string", + description = "체형", + example = "마른체형") + @NotBlank(message = "체형은 반드시 입력되어야 합니다.") + private String bodyType; + + @Schema( + type = "string", + description = "직업", + example = "학생") + @NotBlank(message = "직업은 반드시 입력되어야 합니다.") + private String job; + + @Schema( + type = "string", + description = "활동 지역", + example = "서울") + @NotBlank(message = "활동 지역은 반드시 입력되어야 합니다.") + private String activityArea; + + @Schema( + type = "string", + description = "MBTI", + example = "ESTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "MBTI는 반드시 입력되어야 합니다.") + private String mbti; + + @Schema( + type = "string", + description = "음역대", + example = "저음") + @NotBlank(message = "음역대는 반드시 입력되어야 합니다.") + private String vocalRange; + + @Schema( + type = "array", + description = "프로필 사진 파일들") + @Size(min = 2, max = 2) + private List profilePhotos; + + @Schema( + type = "number", + description = "사진 싱크로율", + example = "80") + @Min( + value = 80, + message = "사진 싱크로율이 80퍼 이상이어야 가입할 수 있습니다.") + private int photoSyncRate; + + @Schema( + type = "string", + description = "닮은 동물상", + example = "강아지상") + @NotBlank(message = "닮은 동물상은 반드시 입력되어야 합니다.") + private String lookAlikeAnimal; + + @Schema( + type = "string", + description = "선호하는 동물상", + example = "강아지상") + @NotBlank(message = "선호하는 동물상은 반드시 입력되어야 합니다.") + private String preferredAnimal; + + @Schema( + type = "string", + description = "선호하는 지역", + example = "서울") + @NotBlank(message = "선호하는 지역은 반드시 입력되어야 합니다.") + private String preferredArea; + + @Schema( + type = "string", + description = "선호하는 음역대", + example = "저음") + @NotBlank(message = "선호하는 음역대는 반드시 입력되어야 합니다.") + private String preferredVocalRange; + + @Schema( + type = "number", + description = "선호하는 나이 하한", + example = "22") + @Positive(message = "선호하는 나이 하한은 양수여야 합니다.") + private int preferredAgeLowerBound; + + @Schema( + type = "number", + description = "선호하는 나이 상한", + example = "30") + @Positive(message = "선호하는 나이 상한은 양수여야 합니다.") + private int preferredAgeUpperBound; + + @Schema( + type = "number", + description = "선호하는 키 하한", + example = "177") + @Positive(message = "선호하는 키 하한은 양수여야 합니다.") + private int preferredHeightLowerBound; + + @Schema( + type = "number", + description = "선호하는 키 상한", + example = "185") + @Positive(message = "선호하는 키 상한은 양수여야 합니다.") + private int preferredHeightUpperBound; + + @Schema( + type = "string", + description = "선호하는 체형", + example = "마른체형") + @NotBlank(message = "선호하는 체형은 반드시 입력되어야 합니다.") + private String preferredBodyType; + + @Schema( + type = "string", + description = "선호하는 MBTI", + example = "ISTJ") + @Pattern( + regexp = "^[EI][SN][TF][JP]$", + message = "유효하지 않은 MBTI 형식입니다.") + @NotBlank(message = "선호하는 MBTI는 반드시 입력되어야 합니다.") + private String preferredMbti; } diff --git a/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java b/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java index 7d193895..aa608c54 100644 --- a/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java +++ b/be/src/main/java/yeonba/be/login/dto/response/UserJoinResponse.java @@ -8,15 +8,15 @@ @AllArgsConstructor public class UserJoinResponse { - @Schema( - type = "string", - description = "access token", - example = "header.payload,signature") - private String accessToken; + @Schema( + type = "string", + description = "access token", + example = "header.payload,signature") + private String accessToken; - @Schema( - type = "string", - description = "refresh token", - example = "header.payload.signature") - private String refreshToken; + @Schema( + type = "string", + description = "refresh token", + example = "header.payload.signature") + private String refreshToken; } diff --git a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java index 08c30eae..5feee1a1 100644 --- a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java +++ b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java @@ -6,7 +6,6 @@ import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; -import yeonba.be.user.entity.ProfilePhoto; import yeonba.be.user.entity.User; @Getter @@ -87,9 +86,7 @@ public class UserProfileDetailResponse { public UserProfileDetailResponse(User user) { - this.profilePhotoUrls = user.getProfilePhotos().stream() - .map(ProfilePhoto::getPhotoUrl) - .toList(); + this.profilePhotoUrls = user.getProfilePhotoUrls(); this.gender = user.getGender(); this.name = user.getName(); this.birth = user.getBirth(); diff --git a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java index 246b9a05..3aea5ad0 100644 --- a/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java +++ b/be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java @@ -9,68 +9,68 @@ @AllArgsConstructor public class UserProfileResponse { - @Schema( - type = "array", - description = "프로필 사진 URL들") - private List profilePhotosUrls; + @Schema( + type = "array", + description = "프로필 사진 URL들") + private List profilePhotosUrls; - @Schema( - type = "string", - description = "성별", - example = "남") - private String gender; + @Schema( + type = "string", + description = "성별", + example = "남") + private String gender; - @Schema( - type = "string", - description = "닉네임", - example = "존잘남") - private String nickname; + @Schema( + type = "string", + description = "닉네임", + example = "존잘남") + private String nickname; - @Schema( - type = "number", - description = "사용자가 가진 총 화살 수", - example = "10") - private int arrows; + @Schema( + type = "number", + description = "사용자가 가진 총 화살 수", + example = "10") + private int arrows; - @Schema( - type = "number", - description = "나이", - example = "23") - private int age; + @Schema( + type = "number", + description = "나이", + example = "23") + private int age; - @Schema( - type = "number", - description = "키", - example = "177") - private int height; + @Schema( + type = "number", + description = "키", + example = "177") + private int height; - @Schema( - type = "string", - description = "활동 지역", - example = "서울") - private String activityArea; + @Schema( + type = "string", + description = "활동 지역", + example = "서울") + private String activityArea; - @Schema( - type = "number", - description = "사진 싱크로율", - example = "80") - private double photoSyncRate; + @Schema( + type = "number", + description = "사진 싱크로율", + example = "80") + private int photoSyncRate; - @Schema( - type = "string", - description = "음역대", - example = "저음") - private String vocalRange; + @Schema( + type = "string", + description = "음역대", + example = "저음") + private String vocalRange; - @Schema( - type = "string", - description = "닮은 동물상", - example = "여우상") - private String lookAlikeAnimalName; + @Schema( + type = "string", + description = "닮은 동물상", + example = "여우상") + private String lookAlikeAnimalName; - @Schema( - type = "boolean", - description = "이전 화살 전송 여부", - example = "false") - private boolean isAlreadySentArrow; + @Schema( + type = "boolean", + description = "이전 화살 전송 여부", + example = "false") + private boolean isAlreadySentArrow; } diff --git a/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java b/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java index 2ede8b48..2353d5c0 100644 --- a/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java +++ b/be/src/main/java/yeonba/be/user/entity/ProfilePhoto.java @@ -21,21 +21,22 @@ @NoArgsConstructor public class ProfilePhoto { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne - @JoinColumn(name = "user_id") - private User user; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; - private String photoUrl; + private String photoUrl; - @CreatedDate - private LocalDateTime createdAt; + @CreatedDate + private LocalDateTime createdAt; - public ProfilePhoto(User user, String photoUrl) { - this.user = user; - this.photoUrl = photoUrl; - } + public ProfilePhoto(User user, String photoUrl) { + + this.user = user; + this.photoUrl = photoUrl; + } } diff --git a/be/src/main/java/yeonba/be/user/entity/UserPreference.java b/be/src/main/java/yeonba/be/user/entity/UserPreference.java index 62f13191..eb3f3e18 100644 --- a/be/src/main/java/yeonba/be/user/entity/UserPreference.java +++ b/be/src/main/java/yeonba/be/user/entity/UserPreference.java @@ -18,64 +18,66 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class UserPreference { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(nullable = false) - private int ageLowerBound; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @Column(nullable = false) - private int ageUpperBound; + @Column(nullable = false) + private int ageLowerBound; - @Column(nullable = false) - private int heightLowerBound; + @Column(nullable = false) + private int ageUpperBound; - @Column(nullable = false) - private int heightUpperBound; + @Column(nullable = false) + private int heightLowerBound; - @Column(nullable = false) - private String mbti; + @Column(nullable = false) + private int heightUpperBound; - @Column(nullable = false) - private String bodyType; + @Column(nullable = false) + private String mbti; - @OneToOne - @JoinColumn(name = "user_id") - private User user; + @Column(nullable = false) + private String bodyType; - @ManyToOne - @JoinColumn(name = "vocal_range_id") - private VocalRange vocalRange; + @OneToOne + @JoinColumn(name = "user_id") + private User user; - @ManyToOne - @JoinColumn(name = "area_id") - private Area area; + @ManyToOne + @JoinColumn(name = "vocal_range_id") + private VocalRange vocalRange; - @ManyToOne - @JoinColumn(name = "animal_id") - private Animal animal; + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; - public UserPreference( - int ageLowerBound, - int ageUpperBound, - int heightLowerBound, - int heightUpperBound, - String mbti, - String bodyType, - User user, - VocalRange vocalRange, - Area area, - Animal animal) { - this.ageLowerBound = ageLowerBound; - this.ageUpperBound = ageUpperBound; - this.heightLowerBound = heightLowerBound; - this.heightUpperBound = heightUpperBound; - this.mbti = mbti; - this.bodyType = bodyType; - this.user = user; - this.vocalRange = vocalRange; - this.area = area; - this.animal = animal; - } + @ManyToOne + @JoinColumn(name = "animal_id") + private Animal animal; + + public UserPreference( + int ageLowerBound, + int ageUpperBound, + int heightLowerBound, + int heightUpperBound, + String mbti, + String bodyType, + User user, + VocalRange vocalRange, + Area area, + Animal animal) { + + this.ageLowerBound = ageLowerBound; + this.ageUpperBound = ageUpperBound; + this.heightLowerBound = heightLowerBound; + this.heightUpperBound = heightUpperBound; + this.mbti = mbti; + this.bodyType = bodyType; + this.user = user; + this.vocalRange = vocalRange; + this.area = area; + this.animal = animal; + } } diff --git a/be/src/main/java/yeonba/be/user/enums/Gender.java b/be/src/main/java/yeonba/be/user/enums/Gender.java index 6e57d776..0668205c 100644 --- a/be/src/main/java/yeonba/be/user/enums/Gender.java +++ b/be/src/main/java/yeonba/be/user/enums/Gender.java @@ -4,24 +4,24 @@ public enum Gender { - MALE("남", true), - FEMALE("여", false); + MALE("남", true), + FEMALE("여", false); - public final String genderString; - public final boolean genderBoolean; + public final String genderString; + public final boolean genderBoolean; - Gender(String genderString, boolean genderBoolean) { - this.genderString = genderString; - this.genderBoolean = genderBoolean; - } + Gender(String genderString, boolean genderBoolean) { + this.genderString = genderString; + this.genderBoolean = genderBoolean; + } - public static Gender from(String genderString) { + public static Gender from(String genderString) { - if (StringUtils.equals(genderString, MALE.genderString)) { + if (StringUtils.equals(genderString, MALE.genderString)) { - return MALE; - } + return MALE; + } - return FEMALE; - } + return FEMALE; + } } diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java index 4b7280c9..50a4609a 100644 --- a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java +++ b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java @@ -9,5 +9,5 @@ @RequiredArgsConstructor public class ProfilePhotoDeletionEvent { - private final List profilePhotos; + private final List profilePhotos; } \ No newline at end of file diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java index 0a203c63..54fe9404 100644 --- a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java +++ b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class ProfilePhotoDeletionListener { - private final S3Service s3Service; + private final S3Service s3Service; - @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) - public void onProfilePhotoDeletion(ProfilePhotoDeletionEvent event) { + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void onProfilePhotoDeletion(ProfilePhotoDeletionEvent event) { - s3Service.deleteProfilePhotos(event.getProfilePhotos()); - } + s3Service.deleteProfilePhotos(event.getProfilePhotos()); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/UserCommand.java b/be/src/main/java/yeonba/be/user/repository/UserCommand.java index 5c22f9af..d2c077bc 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserCommand.java +++ b/be/src/main/java/yeonba/be/user/repository/UserCommand.java @@ -8,10 +8,10 @@ @RequiredArgsConstructor public class UserCommand { - private final UserRepository userRepository; + private final UserRepository userRepository; - public User save(User user) { + public User save(User user) { - return userRepository.save(user); - } + return userRepository.save(user); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 2c582830..91a8c3e0 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -10,38 +10,38 @@ @RequiredArgsConstructor public class UserQuery { - private final UserRepository userRepository; + private final UserRepository userRepository; - public User findById(long userId) { + public User findById(long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } - public User findByEmail(String email) { + public User findByEmail(String email) { - return userRepository.findByEmail(email) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } - public boolean isUserExist(String phoneNumber) { + public boolean isUserExist(String phoneNumber) { - return userRepository.existsByPhoneNumber(phoneNumber); - } + return userRepository.existsByPhoneNumber(phoneNumber); + } - public User findByPhoneNumber(String phoneNumber) { + public User findByPhoneNumber(String phoneNumber) { - return userRepository.findByPhoneNumber(phoneNumber) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findByPhoneNumber(phoneNumber) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } - public boolean isAlreadyUsedNickname(String nickname) { + public boolean isAlreadyUsedNickname(String nickname) { - return userRepository.existsByNickname(nickname); - } + return userRepository.existsByNickname(nickname); + } - public boolean isAlreadyUsedEmail(String email) { + public boolean isAlreadyUsedEmail(String email) { - return userRepository.existsByEmail(email); - } + return userRepository.existsByEmail(email); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index dc09bf03..ac457eb7 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -8,13 +8,13 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmail(String email); - boolean existsByPhoneNumber(String phoneNumber); + boolean existsByPhoneNumber(String phoneNumber); - Optional findByPhoneNumber(String phoneNumber); + Optional findByPhoneNumber(String phoneNumber); - boolean existsByNickname(String nickname); + boolean existsByNickname(String nickname); - boolean existsByEmail(String email); + boolean existsByEmail(String email); } diff --git a/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java index f35d305f..1cfbd8c3 100644 --- a/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/animal/AnimalQuery.java @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class AnimalQuery { - private final AnimalRepository animalRepository; + private final AnimalRepository animalRepository; - public Animal findByName(String name) { + public Animal findByName(String name) { - return animalRepository.findByName(name) - .orElseThrow(() -> new GeneralException(JoinException.ANIMAL_NOT_FOUND)); - } + return animalRepository.findByName(name) + .orElseThrow(() -> new GeneralException(JoinException.ANIMAL_NOT_FOUND)); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java b/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java index 51ba3785..2a40e91e 100644 --- a/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/animal/AnimalRepository.java @@ -8,5 +8,5 @@ @Repository public interface AnimalRepository extends JpaRepository { - Optional findByName(String name); + Optional findByName(String name); } diff --git a/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java index 6e696287..d803a69c 100644 --- a/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/area/AreaQuery.java @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class AreaQuery { - private final AreaRepository areaRepository; + private final AreaRepository areaRepository; - public Area findByName(String name) { + public Area findByName(String name) { - return areaRepository.findByName(name) - .orElseThrow(() -> new GeneralException(JoinException.AREA_NOT_FOUND)); - } + return areaRepository.findByName(name) + .orElseThrow(() -> new GeneralException(JoinException.AREA_NOT_FOUND)); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java b/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java index c5caff70..de0e5074 100644 --- a/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/area/AreaRepository.java @@ -8,5 +8,5 @@ @Repository public interface AreaRepository extends JpaRepository { - Optional findByName(String name); + Optional findByName(String name); } diff --git a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java index 362e77bd..f59b02e4 100644 --- a/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java +++ b/be/src/main/java/yeonba/be/user/repository/profilephoto/ProfilePhotoCommand.java @@ -9,10 +9,10 @@ @RequiredArgsConstructor public class ProfilePhotoCommand { - private final ProfilePhotoRepository profilePhotoRepository; + private final ProfilePhotoRepository profilePhotoRepository; - public List save(List profilePhotos) { + public List save(List profilePhotos) { - return profilePhotoRepository.saveAll(profilePhotos); - } + return profilePhotoRepository.saveAll(profilePhotos); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java index c6e8de0d..809b5d9c 100644 --- a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java +++ b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceCommand.java @@ -8,10 +8,10 @@ @RequiredArgsConstructor public class UserPreferenceCommand { - private final UserPreferenceRepository userPreferenceRepository; + private final UserPreferenceRepository userPreferenceRepository; - public UserPreference save(UserPreference userPreference) { + public UserPreference save(UserPreference userPreference) { - return userPreferenceRepository.save(userPreference); - } + return userPreferenceRepository.save(userPreference); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java index 56dc80c0..24a11a90 100644 --- a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeQuery.java @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class VocalRangeQuery { - private final VocalRangeRepository vocalRangeRepository; + private final VocalRangeRepository vocalRangeRepository; - public VocalRange find(String classification) { + public VocalRange findBy(String classification) { - return vocalRangeRepository.findByClassification(classification) - .orElseThrow(() -> new GeneralException(JoinException.VOCAL_RANGE_NOT_FOUND)); - } + return vocalRangeRepository.findByClassification(classification) + .orElseThrow(() -> new GeneralException(JoinException.VOCAL_RANGE_NOT_FOUND)); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java index 0917cdc7..504b94d3 100644 --- a/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/vocalrange/VocalRangeRepository.java @@ -8,5 +8,5 @@ @Repository public interface VocalRangeRepository extends JpaRepository { - Optional findByClassification(String classification); + Optional findByClassification(String classification); } diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index bb0d8301..11a43faa 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -15,40 +15,40 @@ @RequiredArgsConstructor public class JoinService { - private final ApplicationEventPublisher eventPublisher; - private final UserService userService; - - private final JwtUtil jwtUtil; - - /* - 회원 가입 비즈니스 로직 과정 - 1. 사용자 엔티티 저장 - 2. 프로필 사진 엔티티 저장 - 3. 사용자 선호조건 저장 - 4. access token 및 refresh token 발급 - 5. 사용자 refresh token 업데이트 - */ - @Transactional - public UserJoinResponse join(UserJoinRequest request) { - - User user = userService.saveUser(request); - userService.saveProfilePhotos(user, request); - - // 트랜잭션 롤백시 업로드된 프로필 사진 삭제를 위해 이벤트 발행 - ProfilePhotoDeletionEvent event = new ProfilePhotoDeletionEvent( - user.getProfilePhotos()); - eventPublisher.publishEvent(event); - - userService.saveUserPreference(user, request); - - // access token, refresh token 발급 - Date issuedAt = new Date(); - String accessToken = jwtUtil.generateAccessToken(user, issuedAt); - String refreshToken = jwtUtil.generateRefreshToken(user, issuedAt); - - // 사용자 refresh token 업데이트 - user.updateRefreshToken(refreshToken); - - return new UserJoinResponse(accessToken, refreshToken); - } + private final ApplicationEventPublisher eventPublisher; + private final UserService userService; + + private final JwtUtil jwtUtil; + + /* + 회원 가입 비즈니스 로직 과정 + 1. 사용자 엔티티 저장 + 2. 프로필 사진 엔티티 저장 + 3. 사용자 선호조건 저장 + 4. access token 및 refresh token 발급 + 5. 사용자 refresh token 업데이트 + */ + @Transactional + public UserJoinResponse join(UserJoinRequest request) { + + User user = userService.saveUser(request); + userService.saveProfilePhotos(user, request); + + // 트랜잭션 롤백시 업로드된 프로필 사진 삭제를 위해 이벤트 발행 + ProfilePhotoDeletionEvent event = new ProfilePhotoDeletionEvent( + user.getProfilePhotos()); + eventPublisher.publishEvent(event); + + userService.saveUserPreference(user, request); + + // access token, refresh token 발급 + Date issuedAt = new Date(); + String accessToken = jwtUtil.generateAccessToken(user, issuedAt); + String refreshToken = jwtUtil.generateRefreshToken(user, issuedAt); + + // 사용자 refresh token 업데이트 + user.updateRefreshToken(refreshToken); + + return new UserJoinResponse(accessToken, refreshToken); + } } diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index b8ed43a3..930d250d 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -35,138 +35,138 @@ @RequiredArgsConstructor public class UserService { - private final int JOIN_REWARD_ARROWS = 30; - - private final ProfilePhotoCommand profilePhotoCommand; - private final UserCommand userCommand; - private final UserPreferenceCommand userPreferenceCommand; - - private final AnimalQuery animalQuery; - private final AreaQuery areaQuery; - private final ArrowQuery arrowQuery; - private final UserQuery userQuery; - private final VocalRangeQuery vocalRangeQuery; - - private final PasswordEncryptor passwordEncryptor; - - private final S3Service s3Service; - - - @Transactional(readOnly = true) - public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { - - User user = userQuery.findById(userId); - User targetUser = userQuery.findById(targetUserId); - - boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); - - return new UserProfileResponse( - targetUser.getProfilePhotoUrls(), - targetUser.getGender(), - targetUser.getNickname(), - targetUser.getArrow(), - targetUser.getAge(), - targetUser.getHeight(), - targetUser.getArea().getName(), - targetUser.getPhotoSyncRate(), - targetUser.getVocalRange().getClassification(), - targetUser.getAnimal().getName(), - isAlreadySentArrow); - } - - @Transactional - public User saveUser(UserJoinRequest request) { - - // 이미 사용 중인 이메일인지 확인 - if (userQuery.isAlreadyUsedEmail(request.getEmail())) { - throw new GeneralException(JoinException.ALREADY_USED_EMAIL); - } - - // 이미 사용 중인 닉네임인지 확인 - if (userQuery.isAlreadyUsedNickname(request.getNickname())) { - throw new GeneralException(JoinException.ALREADY_USED_NICKNAME); - } - - // 비밀빈호, 비밀번호 확인 값 일치 확인 - String password = request.getPassword(); - String passwordConfirmation = request.getPasswordConfirmation(); - if (!StringUtils.equals(password, passwordConfirmation)) { - throw new GeneralException(JoinException.PASSWORD_CONFIRMATION_NOT_MATCH); - } - - // 성별 판별 - Gender gender = Gender.from(request.getGender()); - - // 나이 계산 - LocalDate birth = request.getBirth(); - int age = Period.between(birth, LocalDate.now()).getYears(); - - // salt 생성 및 비밀번호 암호화 - String salt = SaltGenerator.generateRandomSalt(); - String encryptedPassword = passwordEncryptor.encrypt(password, salt); - - // 음역대, 동물상, 지역 조회 - VocalRange vocalRange = vocalRangeQuery.find(request.getVocalRange()); - Animal animal = animalQuery.findByName(request.getLookAlikeAnimal()); - Area area = areaQuery.findByName(request.getActivityArea()); - - // 사용자 생성 및 저장 - User user = new User( - gender.genderBoolean, - request.getName(), - request.getNickname(), - request.getBirth(), - age, - request.getHeight(), - request.getEmail(), - encryptedPassword, - salt, - request.getPhoneNumber(), - JOIN_REWARD_ARROWS, - request.getPhotoSyncRate(), - request.getBodyType(), - request.getJob(), - request.getMbti(), - vocalRange, - animal, - area); - - return userCommand.save(user); - } - - @Transactional - public void saveProfilePhotos(User user, UserJoinRequest request) { - - List photoFiles = request.getProfilePhotos(); - - List profilePhotoUrls = s3Service.uploadProfilePhotos(photoFiles, user); - List profilePhotos = profilePhotoUrls.stream() - .map(profilePhotoUrl -> new ProfilePhoto(user, profilePhotoUrl)) - .toList(); - - profilePhotoCommand.save(profilePhotos); - user.updateProfilePhotos(profilePhotos); - } - - @Transactional - public void saveUserPreference(User user, UserJoinRequest request) { - - // 선호 음역대, 동물상, 지역 조회 - Animal preferredAnimal = animalQuery.findByName(request.getPreferredAnimal()); - VocalRange preferredVocalRange = vocalRangeQuery.find(request.getPreferredVocalRange()); - Area preferredArea = areaQuery.findByName(request.getPreferredArea()); - - UserPreference userPreference = new UserPreference( - request.getPreferredAgeLowerBound(), - request.getPreferredAgeUpperBound(), - request.getPreferredHeightLowerBound(), - request.getPreferredHeightUpperBound(), - request.getMbti(), - request.getBodyType(), - user, - preferredVocalRange, - preferredArea, - preferredAnimal); - userPreferenceCommand.save(userPreference); - } + private final int JOIN_REWARD_ARROWS = 30; + + private final ProfilePhotoCommand profilePhotoCommand; + private final UserCommand userCommand; + private final UserPreferenceCommand userPreferenceCommand; + + private final AnimalQuery animalQuery; + private final AreaQuery areaQuery; + private final ArrowQuery arrowQuery; + private final UserQuery userQuery; + private final VocalRangeQuery vocalRangeQuery; + + private final PasswordEncryptor passwordEncryptor; + + private final S3Service s3Service; + + + @Transactional(readOnly = true) + public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { + + User user = userQuery.findById(userId); + User targetUser = userQuery.findById(targetUserId); + + boolean isAlreadySentArrow = arrowQuery.isArrowTransactionExist(user, targetUser); + + return new UserProfileResponse( + targetUser.getProfilePhotoUrls(), + targetUser.getGender(), + targetUser.getNickname(), + targetUser.getArrow(), + targetUser.getAge(), + targetUser.getHeight(), + targetUser.getArea().getName(), + targetUser.getPhotoSyncRate(), + targetUser.getVocalRange().getClassification(), + targetUser.getAnimal().getName(), + isAlreadySentArrow); + } + + @Transactional + public User saveUser(UserJoinRequest request) { + + // 이미 사용 중인 이메일인지 확인 + if (userQuery.isAlreadyUsedEmail(request.getEmail())) { + throw new GeneralException(JoinException.ALREADY_USED_EMAIL); + } + + // 이미 사용 중인 닉네임인지 확인 + if (userQuery.isAlreadyUsedNickname(request.getNickname())) { + throw new GeneralException(JoinException.ALREADY_USED_NICKNAME); + } + + // 비밀빈호, 비밀번호 확인 값 일치 확인 + String password = request.getPassword(); + String passwordConfirmation = request.getPasswordConfirmation(); + if (!StringUtils.equals(password, passwordConfirmation)) { + throw new GeneralException(JoinException.PASSWORD_CONFIRMATION_NOT_MATCH); + } + + // 성별 판별 + Gender gender = Gender.from(request.getGender()); + + // 나이 계산 + LocalDate birth = request.getBirth(); + int age = Period.between(birth, LocalDate.now()).getYears(); + + // salt 생성 및 비밀번호 암호화 + String salt = SaltGenerator.generateRandomSalt(); + String encryptedPassword = passwordEncryptor.encrypt(password, salt); + + // 음역대, 동물상, 지역 조회 + VocalRange vocalRange = vocalRangeQuery.findBy(request.getVocalRange()); + Animal animal = animalQuery.findByName(request.getLookAlikeAnimal()); + Area area = areaQuery.findByName(request.getActivityArea()); + + // 사용자 생성 및 저장 + User user = new User( + gender.genderBoolean, + request.getName(), + request.getNickname(), + request.getBirth(), + age, + request.getHeight(), + request.getEmail(), + encryptedPassword, + salt, + request.getPhoneNumber(), + JOIN_REWARD_ARROWS, + request.getPhotoSyncRate(), + request.getBodyType(), + request.getJob(), + request.getMbti(), + vocalRange, + animal, + area); + + return userCommand.save(user); + } + + @Transactional + public void saveProfilePhotos(User user, UserJoinRequest request) { + + List photoFiles = request.getProfilePhotos(); + + List profilePhotoUrls = s3Service.uploadProfilePhotos(photoFiles, user); + List profilePhotos = profilePhotoUrls.stream() + .map(profilePhotoUrl -> new ProfilePhoto(user, profilePhotoUrl)) + .toList(); + + profilePhotoCommand.save(profilePhotos); + user.updateProfilePhotos(profilePhotos); + } + + @Transactional + public void saveUserPreference(User user, UserJoinRequest request) { + + // 선호 음역대, 동물상, 지역 조회 + Animal preferredAnimal = animalQuery.findByName(request.getPreferredAnimal()); + VocalRange preferredVocalRange = vocalRangeQuery.findBy(request.getPreferredVocalRange()); + Area preferredArea = areaQuery.findByName(request.getPreferredArea()); + + UserPreference userPreference = new UserPreference( + request.getPreferredAgeLowerBound(), + request.getPreferredAgeUpperBound(), + request.getPreferredHeightLowerBound(), + request.getPreferredHeightUpperBound(), + request.getMbti(), + request.getBodyType(), + user, + preferredVocalRange, + preferredArea, + preferredAnimal); + userPreferenceCommand.save(userPreference); + } } diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index b3e6636a..eaaca1a1 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -15,59 +15,59 @@ @Component public class JwtUtil { - private final Duration ACCESS_TOKEN_DURATION = Duration.of(8, ChronoUnit.HOURS); - private final Duration REFRESH_TOKEN_DURATION = Duration.of(10, ChronoUnit.DAYS); + private final Duration ACCESS_TOKEN_DURATION = Duration.of(8, ChronoUnit.HOURS); + private final Duration REFRESH_TOKEN_DURATION = Duration.of(10, ChronoUnit.DAYS); - @Value("${JWT_SECRET}") - private String jwtSecret; + @Value("${JWT_SECRET}") + private String jwtSecret; - public String generateAccessToken(User user, Date issuedAt) { + public String generateAccessToken(User user, Date issuedAt) { - Date expiredAt = getExpiredAt(issuedAt, ACCESS_TOKEN_DURATION); + Date expiredAt = getExpiredAt(issuedAt, ACCESS_TOKEN_DURATION); - return generateUserJwt(user, issuedAt, expiredAt); - } + return generateUserJwt(user, issuedAt, expiredAt); + } - public String generateRefreshToken(User user, Date issuedAt) { + public String generateRefreshToken(User user, Date issuedAt) { - Date expiredAt = getExpiredAt(issuedAt, REFRESH_TOKEN_DURATION); + Date expiredAt = getExpiredAt(issuedAt, REFRESH_TOKEN_DURATION); - return generateUserJwt(user, issuedAt, expiredAt); - } + return generateUserJwt(user, issuedAt, expiredAt); + } - private Date getExpiredAt(Date issuedAt, Duration duration) { + private Date getExpiredAt(Date issuedAt, Duration duration) { - Instant instant = issuedAt.toInstant() - .plusMillis(duration.toMillis()); + Instant instant = issuedAt.toInstant() + .plusMillis(duration.toMillis()); - return Date.from(instant); - } + return Date.from(instant); + } - private String generateUserJwt( - User user, - Date issuedAt, - Date expiredAt) { + private String generateUserJwt( + User user, + Date issuedAt, + Date expiredAt) { - return Jwts.builder() - .setSubject(user.getEmail()) - .setIssuedAt(issuedAt) - .setExpiration(expiredAt) - .claim("userId", user.getId()) - .signWith(SignatureAlgorithm.HS256, jwtSecret) - .compact(); - } + return Jwts.builder() + .setSubject(user.getEmail()) + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .claim("userId", user.getId()) + .signWith(SignatureAlgorithm.HS256, jwtSecret) + .compact(); + } - public void validateJwt(String jwt) { + public void validateJwt(String jwt) { - try { - Jwts.parser() - .setSigningKey(jwtSecret) - .parseClaimsJws(jwt); + try { + Jwts.parser() + .setSigningKey(jwtSecret) + .parseClaimsJws(jwt); - } catch (SignatureException e) { - throw new IllegalStateException("유효하지 않은 JWT 시그니처입니다.", e); - } catch (ExpiredJwtException e) { - throw new IllegalStateException("만료된 JWT입니다.", e); - } - } + } catch (SignatureException e) { + throw new IllegalStateException("유효하지 않은 JWT 시그니처입니다.", e); + } catch (ExpiredJwtException e) { + throw new IllegalStateException("만료된 JWT입니다.", e); + } + } } diff --git a/be/src/main/java/yeonba/be/util/S3Service.java b/be/src/main/java/yeonba/be/util/S3Service.java index be4ad246..b7094a6b 100644 --- a/be/src/main/java/yeonba/be/util/S3Service.java +++ b/be/src/main/java/yeonba/be/util/S3Service.java @@ -17,78 +17,78 @@ @RequiredArgsConstructor public class S3Service { - private final String ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX = "^(.+)\\.(jpg|jpeg|png)$"; - - private final S3Client s3Client; - - @Value("${S3_BUCKET_NAME}") - private String bucketName; - - /* - 프로필 사진 업로드는 다음 과정을 거쳐 이뤄진다. - 1. 사진 파일들 확장자 검증 - 2. 파일 식별을 위한 키 생성, profilephoto/{userId}-{photo idx} 형식 - 3. 사진 파일 업로드 요청 생성 및 업로드 수행 - */ - public List uploadProfilePhotos(List profilePhotos, User user) { - - if (!validateProfilePhotosExtensions(profilePhotos)) { - throw new IllegalArgumentException("jpg, jpeg, png 확장자 형식의 파일만 허용됩니다."); - } - - List uploadedProfilePhotosUrls = new ArrayList<>(); - long userId = user.getId(); - - for (int profilePhotoIdx = 0; profilePhotoIdx < profilePhotos.size(); profilePhotoIdx++) { - - MultipartFile profilePhoto = profilePhotos.get(profilePhotoIdx); - String key = String.format("profilephoto/%d-%d", userId, profilePhotoIdx); - - PutObjectRequest putObjectRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .contentDisposition("inline") - .contentType(profilePhoto.getContentType()) - .build(); - - try { - s3Client.putObject(putObjectRequest, - RequestBody.fromInputStream(profilePhoto.getInputStream(), - profilePhoto.getSize())); - uploadedProfilePhotosUrls.add(key); - } catch (Exception e) { - throw new IllegalStateException( - String.format("Failed to upload file : %s", profilePhoto.getOriginalFilename()) - , e); - } - } - - return uploadedProfilePhotosUrls; - } - - public void deleteProfilePhotos(List profilePhotos) { - - for (ProfilePhoto profilePhoto : profilePhotos) { - String photoUrl = profilePhoto.getPhotoUrl(); - - DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() - .bucket(bucketName) - .key(photoUrl) - .build(); - - try { - s3Client.deleteObject(deleteObjectRequest); - } catch (Exception e) { - throw new IllegalStateException( - String.format("Failed to delete file, key : %s", photoUrl), e); - } - } - } - - private boolean validateProfilePhotosExtensions(List profilePhotos) { - - return profilePhotos.stream() - .allMatch(profilePhoto -> profilePhoto.getOriginalFilename() - .matches(ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX)); - } + private final String ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX = "^(.+)\\.(jpg|jpeg|png)$"; + + private final S3Client s3Client; + + @Value("${S3_BUCKET_NAME}") + private String bucketName; + + /* + 프로필 사진 업로드는 다음 과정을 거쳐 이뤄진다. + 1. 사진 파일들 확장자 검증 + 2. 파일 식별을 위한 키 생성, profilephoto/{userId}-{photo idx} 형식 + 3. 사진 파일 업로드 요청 생성 및 업로드 수행 + */ + public List uploadProfilePhotos(List profilePhotos, User user) { + + if (!validateProfilePhotosExtensions(profilePhotos)) { + throw new IllegalArgumentException("jpg, jpeg, png 확장자 형식의 파일만 허용됩니다."); + } + + List uploadedProfilePhotosUrls = new ArrayList<>(); + long userId = user.getId(); + + for (int profilePhotoIdx = 0; profilePhotoIdx < profilePhotos.size(); profilePhotoIdx++) { + + MultipartFile profilePhoto = profilePhotos.get(profilePhotoIdx); + String key = String.format("profilephoto/%d-%d", userId, profilePhotoIdx); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentDisposition("inline") + .contentType(profilePhoto.getContentType()) + .build(); + + try { + s3Client.putObject(putObjectRequest, + RequestBody.fromInputStream(profilePhoto.getInputStream(), + profilePhoto.getSize())); + uploadedProfilePhotosUrls.add(key); + } catch (Exception e) { + throw new IllegalStateException( + String.format("Failed to upload file : %s", profilePhoto.getOriginalFilename()) + , e); + } + } + + return uploadedProfilePhotosUrls; + } + + public void deleteProfilePhotos(List profilePhotos) { + + for (ProfilePhoto profilePhoto : profilePhotos) { + String photoUrl = profilePhoto.getPhotoUrl(); + + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(photoUrl) + .build(); + + try { + s3Client.deleteObject(deleteObjectRequest); + } catch (Exception e) { + throw new IllegalStateException( + String.format("Failed to delete file, key : %s", photoUrl), e); + } + } + } + + private boolean validateProfilePhotosExtensions(List profilePhotos) { + + return profilePhotos.stream() + .allMatch(profilePhoto -> profilePhoto.getOriginalFilename() + .matches(ALLOWED_PROFILE_PHOTO_EXTENSION_REGEX)); + } } diff --git a/be/src/main/java/yeonba/be/util/SaltGenerator.java b/be/src/main/java/yeonba/be/util/SaltGenerator.java index 56ee849c..12c4bf81 100644 --- a/be/src/main/java/yeonba/be/util/SaltGenerator.java +++ b/be/src/main/java/yeonba/be/util/SaltGenerator.java @@ -6,22 +6,22 @@ import org.apache.commons.codec.binary.Base64; /* - - 32바이트 길이의 salt만으로 대부분의 보안적 위협은 커버 가능 - - 이하 로직을 통해 생성되는 salt 문자열의 길이는 약 44자 +- 32바이트 길이의 salt만으로 대부분의 보안적 위협은 커버 가능 +- 이하 로직을 통해 생성되는 salt 문자열의 길이는 약 44자 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class SaltGenerator { - private static final int SALT_BYTE_LENGTH = 32; + private static final int SALT_BYTE_LENGTH = 32; - public static String generateRandomSalt() { + public static String generateRandomSalt() { - SecureRandom random = new SecureRandom(); + SecureRandom random = new SecureRandom(); - byte[] salt = new byte[SALT_BYTE_LENGTH]; - random.nextBytes(salt); + byte[] salt = new byte[SALT_BYTE_LENGTH]; + random.nextBytes(salt); - return Base64.encodeBase64String(salt); - } + return Base64.encodeBase64String(salt); + } } From 7be7bf495c6ce74cc030f32169ba02c5bb2f4a22 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Sun, 31 Mar 2024 16:23:48 +0900 Subject: [PATCH 120/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 브랜치 내역 병합으로 인한 코드 수정 --- be/build.gradle | 3 + .../java/yeonba/be/config/RedisConfig.java | 35 ---- .../yeonba/be/exception/LoginException.java | 2 +- .../be/login/controller/LoginController.java | 184 +++++++++--------- .../yeonba/be/login/service/LoginService.java | 24 +++ .../be/mypage/service/MyPageService.java | 2 +- .../main/java/yeonba/be/user/entity/User.java | 10 +- .../yeonba/be/user/repository/UserQuery.java | 2 + .../be/user/repository/UserRepository.java | 2 + .../main/java/yeonba/be/util/RedisUtil.java | 29 --- 10 files changed, 133 insertions(+), 160 deletions(-) delete mode 100644 be/src/main/java/yeonba/be/config/RedisConfig.java delete mode 100644 be/src/main/java/yeonba/be/util/RedisUtil.java diff --git a/be/build.gradle b/be/build.gradle index 2ffa52ec..74405f5a 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -41,6 +41,9 @@ dependencies { // spring mail implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' + // coolsms java sdk + implementation 'net.nurigo:sdk:4.3.0' + //Querydsl 추가 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" diff --git a/be/src/main/java/yeonba/be/config/RedisConfig.java b/be/src/main/java/yeonba/be/config/RedisConfig.java deleted file mode 100644 index 7003b41f..00000000 --- a/be/src/main/java/yeonba/be/config/RedisConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package yeonba.be.config; - -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.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - - @Value("${spring.data.redis.host}") - private String host; - - @Value("${spring.data.redis.port}") - private int port; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - - return new LettuceConnectionFactory(host, port); - } - - @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - - redisTemplate.setDefaultSerializer(new StringRedisSerializer()); - - return redisTemplate; - } -} diff --git a/be/src/main/java/yeonba/be/exception/LoginException.java b/be/src/main/java/yeonba/be/exception/LoginException.java index 87111dce..280c2357 100644 --- a/be/src/main/java/yeonba/be/exception/LoginException.java +++ b/be/src/main/java/yeonba/be/exception/LoginException.java @@ -10,7 +10,7 @@ public enum LoginException implements BaseException { VERIFICATION_CODE_NOT_MATCH( HttpStatus.BAD_REQUEST, - "인증 코드가 일치하지 않습니다."); + "인증 코드가 일치하지 않습니다."), EXPIRED_VERIFICATION_CODE( HttpStatus.BAD_REQUEST, diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index 5e749806..e16ec8da 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -6,20 +6,22 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import yeonba.be.login.dto.request.UserIdInquiryRequest; +import yeonba.be.login.dto.request.UserEmailInquiryRequest; import yeonba.be.login.dto.request.UserJoinRequest; import yeonba.be.login.dto.request.UserLoginRequest; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; -import yeonba.be.login.dto.request.UserPhoneNumberVerifyRequest; import yeonba.be.login.dto.request.UserRefreshTokenRequest; -import yeonba.be.login.dto.response.UserIdInquiryResponse; +import yeonba.be.login.dto.request.UserVerificationCodeRequest; +import yeonba.be.login.dto.response.UserEmailInquiryResponse; import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.login.dto.response.UserLoginResponse; import yeonba.be.login.dto.response.UserRefreshTokenResponse; import yeonba.be.login.service.LoginService; +import yeonba.be.user.service.JoinService; import yeonba.be.util.CustomResponse; @Tag(name = "Login", description = "로그인 관련 API") @@ -27,92 +29,92 @@ @RequiredArgsConstructor public class LoginController { - private final LoginService loginService; - private final JoinService joinService; - - @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") - @PostMapping(path = "/users/join", consumes = "multipart/form-data") - public ResponseEntity> join( - @Valid @ModelAttribute UserJoinRequest request) { - - UserJoinResponse response = joinService.join(request); - - return ResponseEntity - .ok() - .body(new CustomResponse<>(response)); - } - - @Operation(summary = "이메일 찾기 인증 코드 sms 전송", description = "이메일 찾기를 위한 인증번호 sms 전송을 요청합니다.") - @ApiResponse(responseCode = "202", description = "전화번호 인증 코드 전송 성공") - @PostMapping("/users/email-inquiry/verification-code") - public ResponseEntity> verifyPhoneNumber( - @Valid @RequestBody UserVerificationCodeRequest request) { - - loginService.sendVerificationCodeMessage(request); - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "이메일 찾기", description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다.") - @ApiResponse(responseCode = "200", description = "아이디 찾기 정상 처리") - @PostMapping("/users/email-inquiry") - public ResponseEntity> emailInquiry( - @Valid @RequestBody UserEmailInquiryRequest request) { - - UserEmailInquiryResponse response = loginService.findEmail(request); - - return ResponseEntity - .ok() - .body(new CustomResponse<>(response)); - } - - @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") - @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") - @PostMapping("/users/help/pw-inquiry") - public ResponseEntity> passwordInquiry( - @Valid @RequestBody UserPasswordInquiryRequest request) { - - loginService.sendTemporaryPasswordMail(request); - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") - @PostMapping("/users/login") - public ResponseEntity> login( - @RequestBody UserLoginRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>( - new UserLoginResponse( - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" - ))); - } - - @Operation( - summary = "access token 재발급", - description = "refresh token을 통해 access token을 재발급받을 수 있습니다." - ) - @PostMapping("/users/refresh") - public ResponseEntity> refresh( - @RequestBody UserRefreshTokenRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); - } + private final LoginService loginService; + private final JoinService joinService; + + @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") + @PostMapping(path = "/users/join", consumes = "multipart/form-data") + public ResponseEntity> join( + @Valid @ModelAttribute UserJoinRequest request) { + + UserJoinResponse response = joinService.join(request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + + @Operation(summary = "이메일 찾기 인증 코드 sms 전송", description = "이메일 찾기를 위한 인증번호 sms 전송을 요청합니다.") + @ApiResponse(responseCode = "202", description = "전화번호 인증 코드 전송 성공") + @PostMapping("/users/email-inquiry/verification-code") + public ResponseEntity> verifyPhoneNumber( + @Valid @RequestBody UserVerificationCodeRequest request) { + + loginService.sendVerificationCodeMessage(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "이메일 찾기", description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다.") + @ApiResponse(responseCode = "200", description = "아이디 찾기 정상 처리") + @PostMapping("/users/email-inquiry") + public ResponseEntity> emailInquiry( + @Valid @RequestBody UserEmailInquiryRequest request) { + + UserEmailInquiryResponse response = loginService.findEmail(request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + + @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") + @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") + @PostMapping("/users/help/pw-inquiry") + public ResponseEntity> passwordInquiry( + @Valid @RequestBody UserPasswordInquiryRequest request) { + + loginService.sendTemporaryPasswordMail(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") + @PostMapping("/users/login") + public ResponseEntity> login( + @RequestBody UserLoginRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>( + new UserLoginResponse( + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" + ))); + } + + @Operation( + summary = "access token 재발급", + description = "refresh token을 통해 access token을 재발급받을 수 있습니다." + ) + @PostMapping("/users/refresh") + public ResponseEntity> refresh( + @RequestBody UserRefreshTokenRequest request) { + + String createdJwt = "created"; + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); + } } diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index e0416a5b..b0873fd8 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -1,22 +1,46 @@ package yeonba.be.login.service; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.LoginException; +import yeonba.be.exception.UserException; +import yeonba.be.login.dto.request.UserEmailInquiryRequest; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; +import yeonba.be.login.dto.request.UserVerificationCodeRequest; +import yeonba.be.login.dto.response.UserEmailInquiryResponse; +import yeonba.be.login.entity.VerificationCode; +import yeonba.be.login.repository.VerificationCodeCommand; +import yeonba.be.login.repository.VerificationCodeQuery; import yeonba.be.user.entity.User; import yeonba.be.user.repository.UserQuery; import yeonba.be.util.EmailService; +import yeonba.be.util.PasswordEncryptor; +import yeonba.be.util.SmsService; import yeonba.be.util.TemporaryPasswordGenerator; +import yeonba.be.util.VerificationCodeGenerator; @Service @RequiredArgsConstructor public class LoginService { + private final long VERIFICATION_CODE_TTL = 5; + private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; + private final String VERIFICATION_CODE_MESSAGE = "연바(연애는 바로 지금) 인증 코드 : %s"; + private final UserQuery userQuery; + private final VerificationCodeCommand verificationCodeCommand; + private final VerificationCodeQuery verificationCodeQuery; + private final EmailService emailService; + private final SmsService smsService; + + private final PasswordEncryptor passwordEncryptor; /* 임시 비밀번호는 다음 과정을 거친다. diff --git a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java index 3b379d60..4eea8e14 100644 --- a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java +++ b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java @@ -19,12 +19,12 @@ import yeonba.be.mypage.dto.response.BlockedUsersResponse; import yeonba.be.mypage.dto.response.UserProfileDetailResponse; import yeonba.be.mypage.dto.response.UserSimpleProfileResponse; -import yeonba.be.mypage.util.PasswordEncryptor; import yeonba.be.user.entity.Block; import yeonba.be.user.entity.User; import yeonba.be.user.repository.BlockCommand; import yeonba.be.user.repository.BlockQuery; import yeonba.be.user.repository.UserQuery; +import yeonba.be.util.PasswordEncryptor; @Service @RequiredArgsConstructor diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index f56161fe..de4f5744 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -71,6 +71,7 @@ public class User { @Column(nullable = false) private String mbti; + private String refreshToken; @ManyToOne @JoinColumn(name = "vocal_range_id") @@ -121,8 +122,7 @@ public User( String mbti, VocalRange vocalRange, Animal animal, - Area area, - List profilePhotos) { + Area area) { this.gender = gender; this.name = name; @@ -143,7 +143,6 @@ public User( this.vocalRange = vocalRange; this.animal = animal; this.area = area; - this.profilePhotos = profilePhotos; } public void validateSameUser(User user) { @@ -165,6 +164,11 @@ public void changePassword(String encryptedNewPassword) { this.encryptedPassword = encryptedNewPassword; } + public void delete(LocalDateTime willDeleteTime) { + + this.deletedAt = willDeleteTime; + } + /** * 삭제된 사용자인지 검증 */ diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 0dc43eb4..99e37a2c 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -1,5 +1,7 @@ package yeonba.be.user.repository; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import yeonba.be.exception.GeneralException; diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index 4c2240de..a4b9e370 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -1,5 +1,7 @@ package yeonba.be.user.repository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/be/src/main/java/yeonba/be/util/RedisUtil.java b/be/src/main/java/yeonba/be/util/RedisUtil.java deleted file mode 100644 index cdea4feb..00000000 --- a/be/src/main/java/yeonba/be/util/RedisUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package yeonba.be.util; - -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RedisUtil { - - private final RedisTemplate redisTemplate; - - public void putData(String key, String value, long expiredTimeMinutes) { - redisTemplate.opsForValue() - .set(key, value, expiredTimeMinutes, TimeUnit.MINUTES); - } - - public Optional getData(String key) { - Object value = redisTemplate.opsForValue().get(key); - - return Optional.ofNullable(value); - } - - public void deleteData(String key) { - redisTemplate.delete(key); - } -} From fbd3cb8d5e2da553d55fad080107d415eac83b47 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 16:59:43 +0900 Subject: [PATCH 121/138] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20DTO=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사진 싱크로율 필드 타입 int로 수정 - 채형 필드 명세 오타 수정 --- .../be/mypage/dto/response/UserProfileDetailResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java index 5feee1a1..cf7f13ab 100644 --- a/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java +++ b/be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java @@ -64,12 +64,12 @@ public class UserProfileDetailResponse { type = "number", description = "사진 싱크로율", example = "80") - private double photoSyncRate; + private int photoSyncRate; @Schema( type = "string", description = "체형", - example = "메른 체형") + example = "마른 체형") private String bodyType; @Schema( From 52c3e5bcbb19bb35ca58095445af18f059b671d3 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 17:02:25 +0900 Subject: [PATCH 122/138] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=97=85=EB=A1=9C=EB=93=9C=EB=90=9C=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동일한 URL로 사진을 업로드하기에 이미 업로드된 사진 URL을 업데이트하는 방식으로 데이터 무결성 보장 가능 - 이에 따라 불필요한 로직들 제거 --- .../user/event/ProfilePhotoDeletionEvent.java | 13 ------------ .../event/ProfilePhotoDeletionListener.java | 20 ------------------ .../yeonba/be/user/service/JoinService.java | 10 +-------- .../main/java/yeonba/be/util/S3Service.java | 21 ------------------- 4 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java delete mode 100644 be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java deleted file mode 100644 index 50a4609a..00000000 --- a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package yeonba.be.user.event; - -import java.util.List; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import yeonba.be.user.entity.ProfilePhoto; - -@Getter -@RequiredArgsConstructor -public class ProfilePhotoDeletionEvent { - - private final List profilePhotos; -} \ No newline at end of file diff --git a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java b/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java deleted file mode 100644 index 54fe9404..00000000 --- a/be/src/main/java/yeonba/be/user/event/ProfilePhotoDeletionListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package yeonba.be.user.event; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; -import yeonba.be.util.S3Service; - -@Component -@RequiredArgsConstructor -public class ProfilePhotoDeletionListener { - - private final S3Service s3Service; - - @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) - public void onProfilePhotoDeletion(ProfilePhotoDeletionEvent event) { - - s3Service.deleteProfilePhotos(event.getProfilePhotos()); - } -} diff --git a/be/src/main/java/yeonba/be/user/service/JoinService.java b/be/src/main/java/yeonba/be/user/service/JoinService.java index 11a43faa..88cb7e51 100644 --- a/be/src/main/java/yeonba/be/user/service/JoinService.java +++ b/be/src/main/java/yeonba/be/user/service/JoinService.java @@ -2,20 +2,17 @@ import java.util.Date; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import yeonba.be.login.dto.request.UserJoinRequest; import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.user.entity.User; -import yeonba.be.user.event.ProfilePhotoDeletionEvent; import yeonba.be.util.JwtUtil; @Service @RequiredArgsConstructor public class JoinService { - private final ApplicationEventPublisher eventPublisher; private final UserService userService; private final JwtUtil jwtUtil; @@ -31,14 +28,9 @@ public class JoinService { @Transactional public UserJoinResponse join(UserJoinRequest request) { + // 사용자, 프로필 사진, 선호 조건 엔티티 생성 및 저장 User user = userService.saveUser(request); userService.saveProfilePhotos(user, request); - - // 트랜잭션 롤백시 업로드된 프로필 사진 삭제를 위해 이벤트 발행 - ProfilePhotoDeletionEvent event = new ProfilePhotoDeletionEvent( - user.getProfilePhotos()); - eventPublisher.publishEvent(event); - userService.saveUserPreference(user, request); // access token, refresh token 발급 diff --git a/be/src/main/java/yeonba/be/util/S3Service.java b/be/src/main/java/yeonba/be/util/S3Service.java index b7094a6b..6bbf08fc 100644 --- a/be/src/main/java/yeonba/be/util/S3Service.java +++ b/be/src/main/java/yeonba/be/util/S3Service.java @@ -8,9 +8,7 @@ import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import yeonba.be.user.entity.ProfilePhoto; import yeonba.be.user.entity.User; @Service @@ -66,25 +64,6 @@ public List uploadProfilePhotos(List profilePhotos, User return uploadedProfilePhotosUrls; } - public void deleteProfilePhotos(List profilePhotos) { - - for (ProfilePhoto profilePhoto : profilePhotos) { - String photoUrl = profilePhoto.getPhotoUrl(); - - DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() - .bucket(bucketName) - .key(photoUrl) - .build(); - - try { - s3Client.deleteObject(deleteObjectRequest); - } catch (Exception e) { - throw new IllegalStateException( - String.format("Failed to delete file, key : %s", photoUrl), e); - } - } - } - private boolean validateProfilePhotosExtensions(List profilePhotos) { return profilePhotos.stream() From 85522be894cd82ba613cf34adc4fd1503b57530d Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 17:05:07 +0900 Subject: [PATCH 123/138] =?UTF-8?q?refactor:=20jwt=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - issuedAt, 기존 서비스 명명 방식에 부합하고 더 친숙한 generatedAt으로 수정 - 불필요한 사용자 이메일 sub 설정 제거 --- be/src/main/java/yeonba/be/util/JwtUtil.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index eaaca1a1..f1631f97 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -35,23 +35,19 @@ public String generateRefreshToken(User user, Date issuedAt) { return generateUserJwt(user, issuedAt, expiredAt); } - private Date getExpiredAt(Date issuedAt, Duration duration) { + private Date getExpiredAt(Date generatedAt, Duration duration) { - Instant instant = issuedAt.toInstant() + Instant instant = generatedAt.toInstant() .plusMillis(duration.toMillis()); return Date.from(instant); } - private String generateUserJwt( - User user, - Date issuedAt, - Date expiredAt) { + private String generateUserJwt(User user, Date issuedAt, Date generatedAt) { return Jwts.builder() - .setSubject(user.getEmail()) .setIssuedAt(issuedAt) - .setExpiration(expiredAt) + .setExpiration(generatedAt) .claim("userId", user.getId()) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); From 7e494e5a16675bce12f82d5fa03030dd00b41c74 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 17:07:03 +0900 Subject: [PATCH 124/138] =?UTF-8?q?refactor:=20JWT=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=98=88=EC=99=B8,=20=EC=9D=BC=EA=B4=84=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/util/JwtUtil.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index f1631f97..b39a68c0 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -1,9 +1,7 @@ package yeonba.be.util; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -60,10 +58,9 @@ public void validateJwt(String jwt) { .setSigningKey(jwtSecret) .parseClaimsJws(jwt); - } catch (SignatureException e) { - throw new IllegalStateException("유효하지 않은 JWT 시그니처입니다.", e); - } catch (ExpiredJwtException e) { - throw new IllegalStateException("만료된 JWT입니다.", e); + } catch (Exception e) { + + throw new IllegalStateException("유효하지 않은 JWT입니다. 다시 로그인 해주세요", e); } } } From d37ff5804f5908a2e57a163aa67080d064074ddc Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 18:22:43 +0900 Subject: [PATCH 125/138] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20=EC=A0=9C=EA=B1=B0(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/service/UserService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 930d250d..ab54d0a7 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -74,7 +74,6 @@ public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) isAlreadySentArrow); } - @Transactional public User saveUser(UserJoinRequest request) { // 이미 사용 중인 이메일인지 확인 @@ -134,7 +133,6 @@ public User saveUser(UserJoinRequest request) { return userCommand.save(user); } - @Transactional public void saveProfilePhotos(User user, UserJoinRequest request) { List photoFiles = request.getProfilePhotos(); @@ -148,7 +146,6 @@ public void saveProfilePhotos(User user, UserJoinRequest request) { user.updateProfilePhotos(profilePhotos); } - @Transactional public void saveUserPreference(User user, UserJoinRequest request) { // 선호 음역대, 동물상, 지역 조회 From ecebd2cd1b2e851f4ed6bf8a16a8c78bc3cac6a2 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 18:23:42 +0900 Subject: [PATCH 126/138] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8B=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=B0=EA=B4=80=20=EC=98=88=EC=99=B8=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유효하지 않은 JWT - 허용되지 않은 이미지 파일 확장자 --- .../yeonba/be/exception/UtilException.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 be/src/main/java/yeonba/be/exception/UtilException.java diff --git a/be/src/main/java/yeonba/be/exception/UtilException.java b/be/src/main/java/yeonba/be/exception/UtilException.java new file mode 100644 index 00000000..b859903d --- /dev/null +++ b/be/src/main/java/yeonba/be/exception/UtilException.java @@ -0,0 +1,34 @@ +package yeonba.be.exception; + +import org.springframework.http.HttpStatus; + +public enum UtilException implements BaseException { + INVALID_JWT( + HttpStatus.BAD_REQUEST, + "유효하지 않은 JWT입니다. 다시 로그인 해주세요."), + + NOT_ALLOWED_IMAGE_FILE_EXTENSION( + HttpStatus.BAD_REQUEST, + "jpg, jpeg, png 확장자 형식의 파일만 허용됩니다."); + + private final HttpStatus httpStatus; + private final String reason; + + UtilException(HttpStatus httpStatus, String reason) { + + this.httpStatus = httpStatus; + this.reason = reason; + } + + @Override + public HttpStatus getHttpStatus() { + + return httpStatus; + } + + @Override + public String getReason() { + + return reason; + } +} \ No newline at end of file From 9fa2ecb8ba0e052ab62e5c07279862df4d408d7d Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 18:24:23 +0900 Subject: [PATCH 127/138] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/util/JwtUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index b39a68c0..8aa65e40 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -8,6 +8,8 @@ import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.UtilException; import yeonba.be.user.entity.User; @Component @@ -60,7 +62,7 @@ public void validateJwt(String jwt) { } catch (Exception e) { - throw new IllegalStateException("유효하지 않은 JWT입니다. 다시 로그인 해주세요", e); + throw new GeneralException(UtilException.INVALID_JWT); } } } From 87b8599d33f6e85c9a97f4e10d0f12c0e4e9d166 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Mon, 1 Apr 2024 18:26:09 +0900 Subject: [PATCH 128/138] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=82=AC=EC=9A=A9=ED=86=A0=EB=A1=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/util/S3Service.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/S3Service.java b/be/src/main/java/yeonba/be/util/S3Service.java index 6bbf08fc..97a09e24 100644 --- a/be/src/main/java/yeonba/be/util/S3Service.java +++ b/be/src/main/java/yeonba/be/util/S3Service.java @@ -9,6 +9,8 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.UtilException; import yeonba.be.user.entity.User; @Service @@ -31,7 +33,8 @@ public class S3Service { public List uploadProfilePhotos(List profilePhotos, User user) { if (!validateProfilePhotosExtensions(profilePhotos)) { - throw new IllegalArgumentException("jpg, jpeg, png 확장자 형식의 파일만 허용됩니다."); + + throw new GeneralException(UtilException.NOT_ALLOWED_IMAGE_FILE_EXTENSION); } List uploadedProfilePhotosUrls = new ArrayList<>(); @@ -55,9 +58,11 @@ public List uploadProfilePhotos(List profilePhotos, User profilePhoto.getSize())); uploadedProfilePhotosUrls.add(key); } catch (Exception e) { - throw new IllegalStateException( - String.format("Failed to upload file : %s", profilePhoto.getOriginalFilename()) - , e); + + String message = String.format("Failed to upload file : %s", + profilePhoto.getOriginalFilename()); + + throw new IllegalStateException(message, e); } } From dc212e9738cb97924e1063cfd249b2990a3ccf3a Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 3 Apr 2024 15:00:40 +0900 Subject: [PATCH 129/138] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B0=80=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=95=20=EC=A0=9C=EC=99=B8=20URL=EB=93=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 인가 과정이 필요 없는 리소스 경로들 설정 --- be/src/main/java/yeonba/be/config/WebConfig.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/config/WebConfig.java b/be/src/main/java/yeonba/be/config/WebConfig.java index 630081d9..2d996c0a 100644 --- a/be/src/main/java/yeonba/be/config/WebConfig.java +++ b/be/src/main/java/yeonba/be/config/WebConfig.java @@ -19,7 +19,12 @@ public void addInterceptors(InterceptorRegistry registry) { "/swagger-resources/**", "/v2/api-docs", "/webjars/**", - "/error"); + "/error") + .excludePathPatterns( + "/users/join/**", + "/users/email-inquiry/**", + "/users/pw-inquiry", + "/users/login", + "/users/refresh"); } - } From a2c43a4690be268db4e9190b0aae766cf0f34c95 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 3 Apr 2024 15:10:42 +0900 Subject: [PATCH 130/138] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=A0=84=ED=99=94?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미 사용하는 전화번호인지 확인하는 작업을 수행한다는 것을 잘 표현하기 위해 수정 --- be/src/main/java/yeonba/be/user/repository/UserQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 99e37a2c..220eb910 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -26,7 +26,7 @@ public User findByEmail(String email) { .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); } - public boolean existByPhoneNumber(String phoneNumber) { + public boolean isAlreadyUsedPhoneNumber(String phoneNumber) { return userRepository.existsByPhoneNumber(phoneNumber); } From bcde03efb62cb3dd03327408d9dbb614da6003a0 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 3 Apr 2024 15:11:39 +0900 Subject: [PATCH 131/138] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/yeonba/be/user/service/UserService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index ab54d0a7..dc3b27a6 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -78,18 +78,27 @@ public User saveUser(UserJoinRequest request) { // 이미 사용 중인 이메일인지 확인 if (userQuery.isAlreadyUsedEmail(request.getEmail())) { + throw new GeneralException(JoinException.ALREADY_USED_EMAIL); } // 이미 사용 중인 닉네임인지 확인 if (userQuery.isAlreadyUsedNickname(request.getNickname())) { + throw new GeneralException(JoinException.ALREADY_USED_NICKNAME); } + // 이미 사용 중인 핸드폰 번호인지 확인 + if (userQuery.isAlreadyUsedPhoneNumber(request.getPhoneNumber())) { + + throw new GeneralException(JoinException.ALREADY_USED_PHONE_NUMBER); + } + // 비밀빈호, 비밀번호 확인 값 일치 확인 String password = request.getPassword(); String passwordConfirmation = request.getPasswordConfirmation(); if (!StringUtils.equals(password, passwordConfirmation)) { + throw new GeneralException(JoinException.PASSWORD_CONFIRMATION_NOT_MATCH); } @@ -166,4 +175,4 @@ public void saveUserPreference(User user, UserJoinRequest request) { preferredAnimal); userPreferenceCommand.save(userPreference); } -} +} \ No newline at end of file From 6bb7168335d3bd8d53d728c6c18c28dce79974a8 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 3 Apr 2024 15:13:59 +0900 Subject: [PATCH 132/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EA=B3=BC?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 사용 중인 전화번호 검증 메서드명 변경에 따라 코드 수정 - dev 브랜치 작업 내역 병합에 따른 수정 --- .../yeonba/be/login/service/LoginService.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index be5c4811..0d43b39e 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -12,6 +12,7 @@ import yeonba.be.login.dto.request.UserEmailInquiryRequest; import yeonba.be.login.dto.request.UserPasswordInquiryRequest; import yeonba.be.login.dto.request.UserVerificationCodeRequest; +import yeonba.be.login.dto.request.UserVerifyPhoneNumberRequest; import yeonba.be.login.dto.response.UserEmailInquiryResponse; import yeonba.be.login.entity.VerificationCode; import yeonba.be.login.repository.VerificationCodeCommand; @@ -44,11 +45,11 @@ public class LoginService { private final PasswordEncryptor passwordEncryptor; /* - 임시 비밀번호는 다음 과정을 거친다. - 1. 요청 이메일 기반 사용자 조회 - 2. 임시 비밀번호 생성 - 3. 사용자 비밀번호, 임시 비밀번호로 변경 - 4. 임시 비밀번호 발급 메일 전송 + 임시 비밀번호는 다음 과정을 거친다. + 1. 요청 이메일 기반 사용자 조회 + 2. 임시 비밀번호 생성 + 3. 사용자 비밀번호, 임시 비밀번호로 변경 + 4. 임시 비밀번호 발급 메일 전송 */ @Transactional public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { @@ -68,9 +69,10 @@ public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { @Transactional public void sendVerificationCodeMessage(UserVerificationCodeRequest request) { - // 전화 번호로 사용자 조회 + // 해당 번호를 가진 사용자가 존재하는 지 확인 String phoneNumber = request.getPhoneNumber(); - if (!userQuery.existByPhoneNumber(phoneNumber)) { + if (!userQuery.isAlreadyUsedPhoneNumber(phoneNumber)) { + throw new GeneralException(UserException.USER_NOT_FOUND); } @@ -108,7 +110,7 @@ public UserEmailInquiryResponse findEmail(UserEmailInquiryRequest request) { public void sendJoinVerificationCodeMessage(UserVerificationCodeRequest request) { // 이미 사용 중인 번호인 지 검증 - if (userQuery.existByPhoneNumber(request.getPhoneNumber())) { + if (userQuery.isAlreadyUsedPhoneNumber(request.getPhoneNumber())) { throw new GeneralException(JoinException.ALREADY_USED_PHONE_NUMBER); } From f08b17fecbad949781e14d5062149a2f82cca71b Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Wed, 3 Apr 2024 15:14:49 +0900 Subject: [PATCH 133/138] =?UTF-8?q?refactor:=20=EB=B3=91=ED=95=A9=EC=97=90?= =?UTF-8?q?=20=EB=94=B0=EB=A5=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 브랜치 작업내역 병합에 따른 코드 수정 --- be/src/main/java/yeonba/be/login/controller/LoginController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index cb4b3cf1..6b87caa5 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -16,6 +16,7 @@ import yeonba.be.login.dto.request.UserPasswordInquiryRequest; import yeonba.be.login.dto.request.UserRefreshTokenRequest; import yeonba.be.login.dto.request.UserVerificationCodeRequest; +import yeonba.be.login.dto.request.UserVerifyPhoneNumberRequest; import yeonba.be.login.dto.response.UserEmailInquiryResponse; import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.login.dto.response.UserLoginResponse; From b8511ca424ef8661461c145d4dee58cf6c69c787 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 4 Apr 2024 18:40:00 +0900 Subject: [PATCH 134/138] =?UTF-8?q?refactor:=20jwt=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일단 사용하지 않는 jwt 검증 로직 삭제 - issuedAt 변수명, generatedAt으로 일괄 수정 --- be/src/main/java/yeonba/be/util/JwtUtil.java | 27 +++++--------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index 8aa65e40..9ce61c2f 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -8,8 +8,6 @@ import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import yeonba.be.exception.GeneralException; -import yeonba.be.exception.UtilException; import yeonba.be.user.entity.User; @Component @@ -21,18 +19,18 @@ public class JwtUtil { @Value("${JWT_SECRET}") private String jwtSecret; - public String generateAccessToken(User user, Date issuedAt) { + public String generateAccessToken(User user, Date generatedAt) { - Date expiredAt = getExpiredAt(issuedAt, ACCESS_TOKEN_DURATION); + Date expiredAt = getExpiredAt(generatedAt, ACCESS_TOKEN_DURATION); - return generateUserJwt(user, issuedAt, expiredAt); + return generateUserJwt(user, generatedAt, expiredAt); } - public String generateRefreshToken(User user, Date issuedAt) { + public String generateRefreshToken(User user, Date generatedAt) { - Date expiredAt = getExpiredAt(issuedAt, REFRESH_TOKEN_DURATION); + Date expiredAt = getExpiredAt(generatedAt, REFRESH_TOKEN_DURATION); - return generateUserJwt(user, issuedAt, expiredAt); + return generateUserJwt(user, generatedAt, expiredAt); } private Date getExpiredAt(Date generatedAt, Duration duration) { @@ -52,17 +50,4 @@ private String generateUserJwt(User user, Date issuedAt, Date generatedAt) { .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); } - - public void validateJwt(String jwt) { - - try { - Jwts.parser() - .setSigningKey(jwtSecret) - .parseClaimsJws(jwt); - - } catch (Exception e) { - - throw new GeneralException(UtilException.INVALID_JWT); - } - } } From 6b485c496c8d9ed85cbbd3fca5f64aa4bc44c315 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 4 Apr 2024 18:40:32 +0900 Subject: [PATCH 135/138] =?UTF-8?q?refactor:=20EOL=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/exception/UtilException.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/be/src/main/java/yeonba/be/exception/UtilException.java b/be/src/main/java/yeonba/be/exception/UtilException.java index b859903d..52c16baa 100644 --- a/be/src/main/java/yeonba/be/exception/UtilException.java +++ b/be/src/main/java/yeonba/be/exception/UtilException.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpStatus; public enum UtilException implements BaseException { + INVALID_JWT( HttpStatus.BAD_REQUEST, "유효하지 않은 JWT입니다. 다시 로그인 해주세요."), @@ -31,4 +32,4 @@ public String getReason() { return reason; } -} \ No newline at end of file +} From 88d7032118184f6e6fa2ff01c912ef3c5581c2d6 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 4 Apr 2024 18:41:33 +0900 Subject: [PATCH 136/138] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용 중인 데이터(이메일, 폰번호, 별명) 검증 로직 이름 일괄 수정 --- .../yeonba/be/user/repository/UserQuery.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 220eb910..9dd1ff00 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -25,12 +25,7 @@ public User findByEmail(String email) { return userRepository.findByEmail(email) .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); } - - public boolean isAlreadyUsedPhoneNumber(String phoneNumber) { - - return userRepository.existsByPhoneNumber(phoneNumber); - } - + public User findByPhoneNumber(String phoneNumber) { return userRepository.findByPhoneNumber(phoneNumber) @@ -44,13 +39,18 @@ public List findWillDeleteUsers() { return userRepository.findAllByDeletedAtIsBeforeAndDeletedIsFalse(now); } - public boolean isAlreadyUsedNickname(String nickname) { + public boolean validateUsedEmail(String email) { + + return userRepository.existsByEmail(email); + } + + public boolean validateUsedNickname(String nickname) { return userRepository.existsByNickname(nickname); } - public boolean isAlreadyUsedEmail(String email) { + public boolean validateUsedPhoneNumber(String phoneNumber) { - return userRepository.existsByEmail(email); + return userRepository.existsByPhoneNumber(phoneNumber); } } From 0f7a1b89059554591507a5717ec1e1335c0c18bf Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 4 Apr 2024 18:42:05 +0900 Subject: [PATCH 137/138] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/user/service/UserService.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index dc3b27a6..9dd4f912 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -51,7 +51,6 @@ public class UserService { private final S3Service s3Service; - @Transactional(readOnly = true) public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) { @@ -77,19 +76,19 @@ public UserProfileResponse getTargetUserProfile(long userId, long targetUserId) public User saveUser(UserJoinRequest request) { // 이미 사용 중인 이메일인지 확인 - if (userQuery.isAlreadyUsedEmail(request.getEmail())) { + if (userQuery.validateUsedEmail(request.getEmail())) { throw new GeneralException(JoinException.ALREADY_USED_EMAIL); } // 이미 사용 중인 닉네임인지 확인 - if (userQuery.isAlreadyUsedNickname(request.getNickname())) { + if (userQuery.validateUsedNickname(request.getNickname())) { throw new GeneralException(JoinException.ALREADY_USED_NICKNAME); } // 이미 사용 중인 핸드폰 번호인지 확인 - if (userQuery.isAlreadyUsedPhoneNumber(request.getPhoneNumber())) { + if (userQuery.validateUsedPhoneNumber(request.getPhoneNumber())) { throw new GeneralException(JoinException.ALREADY_USED_PHONE_NUMBER); } @@ -175,4 +174,4 @@ public void saveUserPreference(User user, UserJoinRequest request) { preferredAnimal); userPreferenceCommand.save(userPreference); } -} \ No newline at end of file +} From 0153fc0cc5ec074b344deee8f76b0529eff50bc3 Mon Sep 17 00:00:00 2001 From: Minjae-An Date: Thu, 4 Apr 2024 18:42:44 +0900 Subject: [PATCH 138/138] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95(#8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/src/main/java/yeonba/be/login/service/LoginService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java index 0d43b39e..d2ea17cb 100644 --- a/be/src/main/java/yeonba/be/login/service/LoginService.java +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -71,7 +71,7 @@ public void sendVerificationCodeMessage(UserVerificationCodeRequest request) { // 해당 번호를 가진 사용자가 존재하는 지 확인 String phoneNumber = request.getPhoneNumber(); - if (!userQuery.isAlreadyUsedPhoneNumber(phoneNumber)) { + if (!userQuery.validateUsedPhoneNumber(phoneNumber)) { throw new GeneralException(UserException.USER_NOT_FOUND); } @@ -110,7 +110,7 @@ public UserEmailInquiryResponse findEmail(UserEmailInquiryRequest request) { public void sendJoinVerificationCodeMessage(UserVerificationCodeRequest request) { // 이미 사용 중인 번호인 지 검증 - if (userQuery.isAlreadyUsedPhoneNumber(request.getPhoneNumber())) { + if (userQuery.validateUsedPhoneNumber(request.getPhoneNumber())) { throw new GeneralException(JoinException.ALREADY_USED_PHONE_NUMBER); }