From ef1f7e686dc657a6e4f8a4a20faccd7079dbe4a0 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:48:42 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat=20:=20=EB=AC=B8=EC=9E=90=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문자 발송 엔드포인트에 대한 컨트롤러 추가 --- .../core/domain/auth/api/AuthApi.java | 7 +++++- .../domain/auth/api/AuthApiController.java | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApi.java index af7085c4d..93ede3161 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApi.java @@ -9,9 +9,11 @@ import org.springframework.web.bind.annotation.PostMapping; import site.timecapsulearchive.core.domain.auth.dto.request.SignUpRequest; import site.timecapsulearchive.core.domain.auth.dto.request.TokenReIssueRequest; +import site.timecapsulearchive.core.domain.auth.dto.request.VerificationMessageSendRequest; import site.timecapsulearchive.core.domain.auth.dto.response.OAuthUrlResponse; import site.timecapsulearchive.core.domain.auth.dto.response.TemporaryTokenResponse; import site.timecapsulearchive.core.domain.auth.dto.response.TokenResponse; +import site.timecapsulearchive.core.domain.auth.dto.response.VerificationMessageSendResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; public interface AuthApi { @@ -138,7 +140,10 @@ public interface AuthApi { value = "/verification/send-message", consumes = {"application/json"} ) - ResponseEntity sendVerificationMessage(); + ResponseEntity> sendVerificationMessage( + Long memberId, + VerificationMessageSendRequest request + ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java index c1b2c1ba9..d77d53589 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java @@ -3,14 +3,18 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import site.timecapsulearchive.core.domain.auth.dto.request.SignUpRequest; import site.timecapsulearchive.core.domain.auth.dto.request.TokenReIssueRequest; +import site.timecapsulearchive.core.domain.auth.dto.request.VerificationMessageSendRequest; import site.timecapsulearchive.core.domain.auth.dto.response.OAuthUrlResponse; import site.timecapsulearchive.core.domain.auth.dto.response.TemporaryTokenResponse; import site.timecapsulearchive.core.domain.auth.dto.response.TokenResponse; +import site.timecapsulearchive.core.domain.auth.dto.response.VerificationMessageSendResponse; +import site.timecapsulearchive.core.domain.auth.service.MessageVerificationService; import site.timecapsulearchive.core.domain.auth.service.TokenService; import site.timecapsulearchive.core.domain.member.dto.mapper.MemberMapper; import site.timecapsulearchive.core.domain.member.service.MemberService; @@ -23,6 +27,7 @@ public class AuthApiController implements AuthApi { private final TokenService tokenService; + private final MessageVerificationService messageVerificationService; private final MemberService memberService; private final MemberMapper memberMapper; @@ -73,8 +78,23 @@ public ResponseEntity> signUpWithSocialProvider( } @Override - public ResponseEntity sendVerificationMessage() { - return null; + public ResponseEntity> sendVerificationMessage( + @AuthenticationPrincipal final Long memberId, + @Valid @RequestBody final VerificationMessageSendRequest request + ) { + final VerificationMessageSendResponse response = messageVerificationService.sendVerificationMessage( + memberId, + request.receiver(), + request.appHashKey() + ); + + return ResponseEntity.accepted() + .body( + ApiSpec.success( + SuccessCode.ACCEPTED, + response + ) + ); } @Override From b773ffba08ba1a26d50dfe63abeb6ee3f0d8dbd8 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:48:54 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat=20:=20submodule=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/core/src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/src/main/resources/config b/backend/core/src/main/resources/config index fc33c5efb..5587e3965 160000 --- a/backend/core/src/main/resources/config +++ b/backend/core/src/main/resources/config @@ -1 +1 @@ -Subproject commit fc33c5efb1f9109b3c04969a4f039f0ae7d8cabb +Subproject commit 5587e39657c2d35f8b07767e3ddc86dea7409512 From bbf3caf4f53994199d3b0a32d892a17f268e6577 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:49:36 +0900 Subject: [PATCH 03/17] =?UTF-8?q?build=20:=20rest=20template=20http=20clie?= =?UTF-8?q?nt=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Socket 커넥션 풀을 이용하기 위해 Apache 의존성 추가 --- backend/core/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/core/build.gradle b/backend/core/build.gradle index 5bfdede8d..bac4c7dd8 100644 --- a/backend/core/build.gradle +++ b/backend/core/build.gradle @@ -25,6 +25,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + //rest template + implementation 'org.apache.httpcomponents.client5:httpclient5:5.3' + //flyway implementation 'org.flywaydb:flyway-core:9.5.1' implementation 'org.flywaydb:flyway-mysql:9.5.1' From 7b142252fe016a1a3a810c90023b6b4f5844b555 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:51:12 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat=20:=20api=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 외부 API 예외 추가 - API 요청 제한 예외 추가 --- .../exception/TooManyRequestException.java | 11 ++++++ .../core/global/error/ErrorCode.java | 8 +++- .../global/error/GlobalExceptionHandler.java | 38 ++++++++++++++----- .../sms/exception/ExternalApiException.java | 14 +++++++ 4 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/exception/TooManyRequestException.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/exception/ExternalApiException.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/exception/TooManyRequestException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/exception/TooManyRequestException.java new file mode 100644 index 000000000..31b72dc87 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/exception/TooManyRequestException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.auth.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class TooManyRequestException extends BusinessException { + + public TooManyRequestException() { + super(ErrorCode.TOO_MANY_REQUEST_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java index 63d6b339f..0db4a6b1a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java @@ -19,8 +19,14 @@ public enum ErrorCode { //auth AUTHENTICATION_ERROR(401, "A001", "인증에 실패했습니다. 인증 수단이 유효한지 확인하세요."), + //message + TOO_MANY_REQUEST_ERROR(429, "M001", "너무 많은 인증 메시지를 요청했습니다. 24시간 후 요청해주세요."), + //ouath - OAUTH2_NOT_AUTHENTICATED_ERROR(401, "O001", "OAuth2 인증에 실패하였습니다."); + OAUTH2_NOT_AUTHENTICATED_ERROR(401, "O001", "OAuth2 인증에 실패하였습니다."), + + //외부 API + EXTERNAL_API_ERROR(500, "A001", "외부 api 호출에 실패했습니다. 잠시 후 요청해주세요."); private final int status; private final String code; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java index bcfc8f136..7eb3dc7ba 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java @@ -2,15 +2,16 @@ import static site.timecapsulearchive.core.global.error.ErrorCode.INPUT_INVALID_TYPE_ERROR; import static site.timecapsulearchive.core.global.error.ErrorCode.INPUT_INVALID_VALUE_ERROR; +import static site.timecapsulearchive.core.global.error.ErrorCode.INTERNAL_SERVER_ERROR; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import site.timecapsulearchive.core.global.error.exception.BusinessException; +import site.timecapsulearchive.core.infra.sms.exception.ExternalApiException; @RestControllerAdvice @Slf4j @@ -19,13 +20,16 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) protected ResponseEntity handleGlobalException(final Exception e) { log.error(e.getMessage(), e); + ErrorResponse errorResponse = ErrorResponse.create(ErrorCode.INTERNAL_SERVER_ERROR); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + return ResponseEntity.status(INTERNAL_SERVER_ERROR.getStatus()) + .body(errorResponse); } @ExceptionHandler(BusinessException.class) protected ResponseEntity handleBusinessException(final BusinessException e) { log.error(e.getMessage(), e); + ErrorCode errorCode = e.getErrorCode(); ErrorResponse errorResponse = ErrorResponse.create(errorCode); @@ -33,21 +37,37 @@ protected ResponseEntity handleBusinessException(final BusinessEx .body(errorResponse); } - @ExceptionHandler + @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handleRequestArgumentNotValidException( - MethodArgumentNotValidException e) { - log.warn(e.getMessage()); - ErrorResponse response = ErrorResponse.create(INPUT_INVALID_VALUE_ERROR, e.getBindingResult()); + MethodArgumentNotValidException e + ) { + log.warn(e.getMessage(), e); + + ErrorResponse response = ErrorResponse.create(INPUT_INVALID_VALUE_ERROR, + e.getBindingResult()); return ResponseEntity.status(INPUT_INVALID_VALUE_ERROR.getStatus()) .body(response); } - @ExceptionHandler + @ExceptionHandler(HttpMessageNotReadableException.class) protected ResponseEntity handleRequestTypeNotValidException( - HttpMessageNotReadableException e) { - log.warn(e.getMessage()); + HttpMessageNotReadableException e + ) { + log.warn(e.getMessage(), e); + ErrorResponse response = ErrorResponse.create(INPUT_INVALID_TYPE_ERROR); return ResponseEntity.status(INPUT_INVALID_VALUE_ERROR.getStatus()) .body(response); } + + @ExceptionHandler + protected ResponseEntity handleExternalApiException(ExternalApiException e) { + log.warn(e.getMessage(), e); + + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse response = ErrorResponse.create(errorCode); + + return ResponseEntity.status(errorCode.getStatus()) + .body(response); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/exception/ExternalApiException.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/exception/ExternalApiException.java new file mode 100644 index 000000000..3b39170d8 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/exception/ExternalApiException.java @@ -0,0 +1,14 @@ +package site.timecapsulearchive.core.infra.sms.exception; + +import lombok.Getter; +import site.timecapsulearchive.core.global.error.ErrorCode; + +@Getter +public class ExternalApiException extends RuntimeException { + + private final ErrorCode errorCode; + + public ExternalApiException(ErrorCode errorCode) { + this.errorCode = errorCode; + } +} From 7f76518718a5311259ab811d84779cf56145a94d Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:53:12 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat=20:=20request,=20response=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문자 인증 request 추가 - 문자 인증 response 추가 Open #29 --- .../VerificationMessageSendRequest.java | 11 ++++++++--- .../VerificationMessageSendResponse.java | 19 +++++++++++++++++++ .../global/common/response/SuccessCode.java | 3 ++- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/response/VerificationMessageSendResponse.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java index 08c22041f..cf29ef831 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java @@ -1,14 +1,19 @@ package site.timecapsulearchive.core.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @Schema(description = "인증 문자 요청") public record VerificationMessageSendRequest( - @Schema(description = "핸드폰 번호") - @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") - String phone + @Schema(description = "수신자 핸드폰 번호 ex)01012341234") + @Pattern(regexp = "^01(?:0|1|[6-9])?(\\d{3}|\\d{4})?(\\d{4})$", message = "숫자로만 구성된 전화번호 형식이여만합니다.") + String receiver, + + @Schema(description = "앱의 해시 키") + @NotBlank(message = "앱의 해시 키는 필수입니다.") + String appHashKey ) { } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/response/VerificationMessageSendResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/response/VerificationMessageSendResponse.java new file mode 100644 index 000000000..d65befb07 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/response/VerificationMessageSendResponse.java @@ -0,0 +1,19 @@ +package site.timecapsulearchive.core.domain.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "인증 문자 발송 응답") +public record VerificationMessageSendResponse( + + @Schema(name = "전송 상태") + Integer status, + + @Schema(name = "상태 메시지") + String message +) { + + public static VerificationMessageSendResponse success(final Integer status, + final String message) { + return new VerificationMessageSendResponse(status, message); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/SuccessCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/SuccessCode.java index 7b7020c79..ffafac669 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/SuccessCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/SuccessCode.java @@ -7,7 +7,8 @@ @RequiredArgsConstructor public enum SuccessCode { //success handle - SUCCESS("00", "요청 처리에 성공했습니다."); + SUCCESS("00", "요청 처리에 성공했습니다."), + ACCEPTED("01", "요청이 수락되었습니다."); private final String code; private final String message; From c24be185951affbb125f79c4ed91d19a5725a64a Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:55:15 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4자리 인증 번호 전송 서비스 추가 Open #29 --- .../service/MessageVerificationService.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java new file mode 100644 index 000000000..3c0d93591 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java @@ -0,0 +1,59 @@ +package site.timecapsulearchive.core.domain.auth.service; + +import java.util.concurrent.ThreadLocalRandom; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import site.timecapsulearchive.core.domain.auth.dto.response.VerificationMessageSendResponse; +import site.timecapsulearchive.core.domain.auth.repository.MessageAuthenticationCacheRepository; +import site.timecapsulearchive.core.infra.sms.SmsApiService; +import site.timecapsulearchive.core.infra.sms.dto.SmsApiResponse; + +@Service +@RequiredArgsConstructor +public class MessageVerificationService { + + private static final int MIN = 1000; + private static final int MAX = 10000; + + private final MessageAuthenticationCacheRepository messageAuthenticationCacheRepository; + private final SmsApiService smsApiService; + + /** + * 사용자 아이디와 수신자 핸드폰을 받아서 인증번호를 발송한다. + * + * @param memberId 사용자 아이디 + * @param receiver 수신자 핸드폰 번호 + * @param appHashKey 앱의 해시 키(메시지 자동 파싱) + */ + public VerificationMessageSendResponse sendVerificationMessage( + final Long memberId, + final String receiver, + final String appHashKey + ) { + final String code = generateRandomCode(); + + messageAuthenticationCacheRepository.save(memberId, code); + + final String message = generateMessage(code, appHashKey); + + final SmsApiResponse apiResponse = smsApiService.sendMessage(receiver, message); + + return VerificationMessageSendResponse.success(apiResponse.resultCode(), + apiResponse.message()); + } + + private String generateMessage(final String code, final String appHashKey) { + return "<*>[ARchive]" + + "[ARchive 인증번호: " + + code + + "]" + + appHashKey; + } + + private String generateRandomCode() { + return String.valueOf( + ThreadLocalRandom.current() + .nextInt(MIN, MAX) + ); + } +} From daaf3a6f23f938f153f421c30f695ebebe29ca60 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:56:02 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat=20:=20=EB=AC=B8=EC=9E=90=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EC=A0=95=EB=B3=B4=20=EC=BA=90=EC=8B=9C=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 문자 전송 내역 캐시 레포지토리 추가 Open #29 --- .../MessageAuthenticationCacheRepository.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MessageAuthenticationCacheRepository.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MessageAuthenticationCacheRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MessageAuthenticationCacheRepository.java new file mode 100644 index 000000000..fdaec99f5 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MessageAuthenticationCacheRepository.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.auth.repository; + +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MessageAuthenticationCacheRepository { + + private static final int MINUTE = 5; + private static final String PREFIX = "messageAuthentication:"; + + private final StringRedisTemplate redisTemplate; + + public void save(final Long memberId, final String code) { + redisTemplate.opsForValue().set(PREFIX + memberId, code, MINUTE, TimeUnit.MINUTES); + } +} From 57bf918bc9a6c071bd4a0fe676dcfbbe9209d1ca Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 11:58:48 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=20=EC=A0=84=EC=86=A1=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알리고 문자 인증 전송 서비스 추가 - 관련 프로퍼티 추가 - 응답 추가 Open #29 --- .../core/infra/sms/SmsApiService.java | 16 ++++ .../infra/sms/config/AligoSmsProperties.java | 13 ++++ .../core/infra/sms/dto/SmsApiResponse.java | 12 +++ .../infra/sms/service/AligoSmsApiService.java | 75 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/SmsApiService.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/config/AligoSmsProperties.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/dto/SmsApiResponse.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/service/AligoSmsApiService.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/SmsApiService.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/SmsApiService.java new file mode 100644 index 000000000..f9e7ad778 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/SmsApiService.java @@ -0,0 +1,16 @@ +package site.timecapsulearchive.core.infra.sms; + +import site.timecapsulearchive.core.infra.sms.dto.SmsApiResponse; +import site.timecapsulearchive.core.infra.sms.exception.ExternalApiException; + +public interface SmsApiService { + + /** + * 수신자와 메시지를 받아서 Sms Api에 요청을 보낸다. 실제 문자 도착에는 지연이 발생할 수 있다. + * + * @param receiver 수신자 + * @param message 보낼 메시지 + * @return Sms Api 응답 + */ + SmsApiResponse sendMessage(String receiver, String message) throws ExternalApiException; +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/config/AligoSmsProperties.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/config/AligoSmsProperties.java new file mode 100644 index 000000000..39ef66b2b --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/config/AligoSmsProperties.java @@ -0,0 +1,13 @@ +package site.timecapsulearchive.core.infra.sms.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "aligo") +public record AligoSmsProperties( + String userId, + String apiKey, + String sender, + String testmodeYn +) { + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/dto/SmsApiResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/dto/SmsApiResponse.java new file mode 100644 index 000000000..149f648bc --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/dto/SmsApiResponse.java @@ -0,0 +1,12 @@ +package site.timecapsulearchive.core.infra.sms.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record SmsApiResponse( + Integer resultCode, + String message +) { + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/service/AligoSmsApiService.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/service/AligoSmsApiService.java new file mode 100644 index 000000000..90699d173 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/sms/service/AligoSmsApiService.java @@ -0,0 +1,75 @@ +package site.timecapsulearchive.core.infra.sms.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.infra.sms.SmsApiService; +import site.timecapsulearchive.core.infra.sms.config.AligoSmsProperties; +import site.timecapsulearchive.core.infra.sms.dto.SmsApiResponse; +import site.timecapsulearchive.core.infra.sms.exception.ExternalApiException; + +@Service +@RequiredArgsConstructor +public class AligoSmsApiService implements SmsApiService { + + private static final String SEND_URL = "/send/"; + private static final Integer SUCCESS_STATUS = 1; + private static final String KEY = "key"; + private static final String USER_ID = "user_id"; + private static final String SENDER = "sender"; + private static final String TESTMODE_YN = "testmode_yn"; + private static final String MSG = "msg"; + private static final String RECEIVER = "receiver"; + + private final RestTemplate aligoRestTemplate; + private final AligoSmsProperties aligoSmsProperties; + + public SmsApiResponse sendMessage( + final String receiver, + final String message + ) throws ExternalApiException { + final HttpEntity> request = createMessageRequest(receiver, + message); + + final SmsApiResponse response = aligoRestTemplate.postForObject( + SEND_URL, + request, + SmsApiResponse.class + ); + + if (isError(response)) { + throw new ExternalApiException(ErrorCode.EXTERNAL_API_ERROR); + } + + return response; + } + + private HttpEntity> createMessageRequest( + final String receiver, + final String message + ) { + MultiValueMap map = new LinkedMultiValueMap<>(); + + map.add(KEY, aligoSmsProperties.apiKey()); + map.add(USER_ID, aligoSmsProperties.userId()); + map.add(SENDER, aligoSmsProperties.sender()); + map.add(TESTMODE_YN, aligoSmsProperties.testmodeYn()); + map.add(MSG, message); + map.add(RECEIVER, receiver); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + return new HttpEntity<>(map, headers); + } + + private boolean isError(final SmsApiResponse body) { + return body == null || !body.resultCode().equals(SUCCESS_STATUS); + } +} \ No newline at end of file From 75b969554bbc4ebdf1834bff4131a850c1f36679 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 12:03:11 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat=20:=20=EB=AC=B8=EC=9E=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=A0=9C=ED=95=9C=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문자 인증 횟수 제한 인터셉터 추가 - Api 사용 횟수 캐시 레포지토리 추가 - 인터셉터 path config 추가 --- .../global/api/ApiLimitCheckInterceptor.java | 60 +++++++++++++++++++ .../global/api/ApiUsageCacheRepository.java | 41 +++++++++++++ .../core/global/config/WebMvcConfig.java | 20 +++++++ 3 files changed, 121 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiUsageCacheRepository.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/config/WebMvcConfig.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java new file mode 100644 index 000000000..2dc44add0 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java @@ -0,0 +1,60 @@ +package site.timecapsulearchive.core.global.api; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import site.timecapsulearchive.core.domain.auth.exception.TooManyRequestException; + +@Component +@RequiredArgsConstructor +public class ApiLimitCheckInterceptor implements HandlerInterceptor { + + private static final int MAX_API_CALL_LIMIT = 5; + private static final int FIRST_REQUEST = 0; + + private final ApiUsageCacheRepository apiUsageCacheRepository; + + /** + * 엔드포인트에 Api 요청 횟수를 검사하는 인터셉터이다. + * WebMvcConfig에 path로 등록된 경로는 여기를 거치게 된다. + * 아직 문자 인증에 대한 요청만 걸려있다. + * @param request 요청 + * @param response 응답 + * @param handler 해당 요청을 처리할 메서드 + * @return 클라이언트가 보낸 요청이 Api 횟수 제한에 걸리는지 여부 {@code True, False} + * @throws TooManyRequestException 횟수 제한이 발생하면 예외 발생 + */ + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws TooManyRequestException { + Long memberId = (Long) SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + + Integer apiUsageCount = apiUsageCacheRepository.getSmsApiUsage(memberId) + .orElse(FIRST_REQUEST); + + if (apiUsageCount > MAX_API_CALL_LIMIT) { + throw new TooManyRequestException(); + } + + if (isFirstRequest(apiUsageCount)) { + apiUsageCacheRepository.saveAsFirstRequest(memberId); + return true; + } + + apiUsageCacheRepository.increaseSmsApiUsage(memberId); + + return true; + } + + private boolean isFirstRequest(Integer apiUsageCount) { + return apiUsageCount.equals(FIRST_REQUEST); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiUsageCacheRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiUsageCacheRepository.java new file mode 100644 index 000000000..edee48cce --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiUsageCacheRepository.java @@ -0,0 +1,41 @@ +package site.timecapsulearchive.core.global.api; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ApiUsageCacheRepository { + + private static final String PREFIX = "apiUsage:"; + private static final String SMS_API_USAGE = "smsApi"; + private static final String FIRST_REQUEST = "1"; + private static final int EXPIRATION_DAYS = 1; + + private final StringRedisTemplate redisTemplate; + + public Optional getSmsApiUsage(Long memberId) { + String result = (String) redisTemplate.opsForHash().get(PREFIX + memberId, SMS_API_USAGE); + + if (result == null) { + return Optional.empty(); + } + + return Optional.of(Integer.parseInt(result)); + } + + public void increaseSmsApiUsage(Long memberId) { + redisTemplate.opsForHash().increment(PREFIX + memberId, SMS_API_USAGE, 1); + } + + public void saveAsFirstRequest(Long memberId) { + String key = PREFIX + memberId; + + redisTemplate.opsForHash().put(key, SMS_API_USAGE, FIRST_REQUEST); + + redisTemplate.expire(key, EXPIRATION_DAYS, TimeUnit.DAYS); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/WebMvcConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/WebMvcConfig.java new file mode 100644 index 000000000..024c65c68 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/WebMvcConfig.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import site.timecapsulearchive.core.global.api.ApiLimitCheckInterceptor; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final ApiLimitCheckInterceptor apiLimitCheckInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(apiLimitCheckInterceptor) + .addPathPatterns("/auth/verification/send-message"); + } +} From 89dfa323f7002d55645139a1042c9d163a152393 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 12:04:10 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix=20:=20MemberInfo=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth/dto 아래로 변경 --- .../core/domain/auth/{service => dto}/MemberInfo.java | 2 +- .../core/domain/auth/repository/MemberInfoCacheRepository.java | 2 +- .../core/domain/auth/service/TokenService.java | 1 + .../site/timecapsulearchive/core/global/config/RedisConfig.java | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/{service => dto}/MemberInfo.java (71%) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MemberInfo.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/MemberInfo.java similarity index 71% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MemberInfo.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/MemberInfo.java index ad8c6a251..410a36c31 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MemberInfo.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/MemberInfo.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.auth.service; +package site.timecapsulearchive.core.domain.auth.dto; public record MemberInfo(Long memberId) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MemberInfoCacheRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MemberInfoCacheRepository.java index 0dfd356d7..ddfe1d88f 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MemberInfoCacheRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/repository/MemberInfoCacheRepository.java @@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import site.timecapsulearchive.core.domain.auth.service.MemberInfo; +import site.timecapsulearchive.core.domain.auth.dto.MemberInfo; @Repository @RequiredArgsConstructor diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/TokenService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/TokenService.java index 4f71b3408..062f52fd2 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/TokenService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/TokenService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.domain.auth.dto.MemberInfo; import site.timecapsulearchive.core.domain.auth.dto.response.TemporaryTokenResponse; import site.timecapsulearchive.core.domain.auth.dto.response.TokenResponse; import site.timecapsulearchive.core.domain.auth.exception.AlreadyReIssuedTokenException; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/RedisConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/RedisConfig.java index 12825fab8..923a6d493 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/RedisConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/RedisConfig.java @@ -11,7 +11,7 @@ import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import site.timecapsulearchive.core.domain.auth.service.MemberInfo; +import site.timecapsulearchive.core.domain.auth.dto.MemberInfo; @Configuration @EnableTransactionManagement From 01c70d14b8252e9e95d70f9dc01f16ba7fe7b101 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 12:50:46 +0900 Subject: [PATCH 11/17] feat : rest template config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rest template 관련 설정 추가 Open #29 --- .../resttemplate/AligoRestTemplateConfig.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java new file mode 100644 index 000000000..f53c3a414 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java @@ -0,0 +1,77 @@ +package site.timecapsulearchive.core.global.config.resttemplate; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import lombok.RequiredArgsConstructor; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.util.TimeValue; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +@Configuration +@RequiredArgsConstructor +public class AligoRestTemplateConfig { + + private static final String ALIGO_API_URL = "https://apis.aligo.in"; + + @Bean + public RestTemplate aligoRestTemplate(RestTemplateBuilder builder) { + return builder + .messageConverters(formHttpConverter(), mappingJackson2HttpMessageConverter()) + .requestFactory(this::clientHttpRequestFactory) + .rootUri(ALIGO_API_URL) + .build(); + } + + private FormHttpMessageConverter formHttpConverter() { + FormHttpMessageConverter formConverter = new FormHttpMessageConverter(); + + formConverter.addPartConverter(new ByteArrayHttpMessageConverter()); + formConverter.setMultipartCharset(StandardCharsets.UTF_8); + formConverter.setSupportedMediaTypes( + Collections.singletonList(MediaType.MULTIPART_FORM_DATA)); + return formConverter; + } + + private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { + MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); + + jsonConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); + return jsonConverter; + } + + private ClientHttpRequestFactory clientHttpRequestFactory() { + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setConnectionRequestTimeout(5000); + factory.setHttpClient(httpClient()); + + return factory; + } + + private HttpClient httpClient() { + return HttpClientBuilder.create() + .setConnectionManager(connectionManager()) + .evictIdleConnections(TimeValue.ofMinutes(3)) + .evictExpiredConnections() + .build(); + } + + private HttpClientConnectionManager connectionManager() { + return PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnTotal(100) + .setMaxConnPerRoute(10) + .build(); + } +} From 8889eec4bb53fd84d07f9d4c85aabb5eca2111f2 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 12:53:41 +0900 Subject: [PATCH 12/17] =?UTF-8?q?build=20:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core 하위에 필요 없는 .github 삭제 --- .../.github/workflows/google-java-format.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 backend/core/.github/workflows/google-java-format.yml diff --git a/backend/core/.github/workflows/google-java-format.yml b/backend/core/.github/workflows/google-java-format.yml deleted file mode 100644 index e8a1129cf..000000000 --- a/backend/core/.github/workflows/google-java-format.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Format - -on: [ push, pull_request ] - -jobs: - - formatting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: axel-op/googlejavaformat-action@v3 - with: - args: "--replace" - skip-commit: true - - name: Print diffs - run: git --no-pager diff --exit-code \ No newline at end of file From cd08865c0a514385009cf8c2bdcf3aa3107e8606 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 14:41:41 +0900 Subject: [PATCH 13/17] feat : submodule update --- backend/core/src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/src/main/resources/config b/backend/core/src/main/resources/config index 5587e3965..ca2e8f5ba 160000 --- a/backend/core/src/main/resources/config +++ b/backend/core/src/main/resources/config @@ -1 +1 @@ -Subproject commit 5587e39657c2d35f8b07767e3ddc86dea7409512 +Subproject commit ca2e8f5ba737bc1ad8d81078210aff9f834f3748 From 77b438f8f2184e9e14bf5ea9ccf8ad255e6a65cc Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 16:34:59 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix=20:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=8A=A4=ED=8E=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/MessageVerificationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java index 3c0d93591..04889ee04 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java @@ -43,10 +43,10 @@ public VerificationMessageSendResponse sendVerificationMessage( } private String generateMessage(final String code, final String appHashKey) { - return "<*>[ARchive]" - + "[ARchive 인증번호: " + return "<#>[ARchive]" + + "본인확인 인증번호는 [" + code - + "]" + + "]입니다." + appHashKey; } From 717dcec997d967fe289a394ddbda034f1358b303 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 18:32:18 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix=20:=20=EC=9D=B8=EC=A6=9D=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 메시지 전송 후 인증 객체 저장하도록 수정 --- .../core/domain/auth/service/MessageVerificationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java index 04889ee04..ad83257a2 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java @@ -32,12 +32,12 @@ public VerificationMessageSendResponse sendVerificationMessage( ) { final String code = generateRandomCode(); - messageAuthenticationCacheRepository.save(memberId, code); - final String message = generateMessage(code, appHashKey); final SmsApiResponse apiResponse = smsApiService.sendMessage(receiver, message); + messageAuthenticationCacheRepository.save(memberId, code); + return VerificationMessageSendResponse.success(apiResponse.resultCode(), apiResponse.message()); } From 8f175075e098dfdf0713d6156a666496d6453052 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 18:33:52 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat=20:=20=ED=95=B8=EB=93=9C=ED=8F=B0=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=EA=B8=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Custom Validator & Validation 추가 --- .../VerificationMessageSendRequest.java | 4 ++-- .../global/common/valid/PhoneValidator.java | 16 +++++++++++++ .../global/common/valid/annotation/Phone.java | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java index cf29ef831..92a285350 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/dto/request/VerificationMessageSendRequest.java @@ -2,13 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; +import site.timecapsulearchive.core.global.common.valid.annotation.Phone; @Schema(description = "인증 문자 요청") public record VerificationMessageSendRequest( @Schema(description = "수신자 핸드폰 번호 ex)01012341234") - @Pattern(regexp = "^01(?:0|1|[6-9])?(\\d{3}|\\d{4})?(\\d{4})$", message = "숫자로만 구성된 전화번호 형식이여만합니다.") + @Phone String receiver, @Schema(description = "앱의 해시 키") diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java new file mode 100644 index 000000000..934a19980 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java @@ -0,0 +1,16 @@ +package site.timecapsulearchive.core.global.common.valid; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; +import site.timecapsulearchive.core.global.common.valid.annotation.Phone; + +public class PhoneValidator implements ConstraintValidator { + + private static final String PHONE_REGEX = "^01(?:0|1|[6-9])?(\\d{3}|\\d{4})?(\\d{4})$"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return Pattern.matches(value, PHONE_REGEX); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java new file mode 100644 index 000000000..6c5400571 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java @@ -0,0 +1,24 @@ +package site.timecapsulearchive.core.global.common.valid.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import site.timecapsulearchive.core.global.common.valid.PhoneValidator; + +@Target({FIELD}) +@Retention(RUNTIME) +@Constraint(validatedBy = PhoneValidator.class) +@Documented +public @interface Phone { + + String message() default "Invalid Phone"; + + Class[] groups() default { }; + + Class[] payload() default { }; +} From 9f61d21618b137f0de1455e843542db6e1a13dee Mon Sep 17 00:00:00 2001 From: hong seokho Date: Fri, 19 Jan 2024 20:04:38 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix=20:=20rest=20template=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=B3=80=EC=88=98=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RestTemplate 설정 변경 - 변수 -> 상수화 --- .../core/global/api/ApiLimitCheckInterceptor.java | 6 +++--- .../core/global/common/valid/PhoneValidator.java | 4 ++-- .../global/common/valid/annotation/Phone.java | 9 ++++----- .../resttemplate/AligoRestTemplateConfig.java | 15 ++++++++++----- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java index 2dc44add0..d3056f5c7 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/api/ApiLimitCheckInterceptor.java @@ -13,7 +13,7 @@ public class ApiLimitCheckInterceptor implements HandlerInterceptor { private static final int MAX_API_CALL_LIMIT = 5; - private static final int FIRST_REQUEST = 0; + private static final int NO_USAGE = 0; private final ApiUsageCacheRepository apiUsageCacheRepository; @@ -38,7 +38,7 @@ public boolean preHandle( .getPrincipal(); Integer apiUsageCount = apiUsageCacheRepository.getSmsApiUsage(memberId) - .orElse(FIRST_REQUEST); + .orElse(NO_USAGE); if (apiUsageCount > MAX_API_CALL_LIMIT) { throw new TooManyRequestException(); @@ -55,6 +55,6 @@ public boolean preHandle( } private boolean isFirstRequest(Integer apiUsageCount) { - return apiUsageCount.equals(FIRST_REQUEST); + return apiUsageCount.equals(NO_USAGE); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java index 934a19980..3bcf1b9e1 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/PhoneValidator.java @@ -7,10 +7,10 @@ public class PhoneValidator implements ConstraintValidator { - private static final String PHONE_REGEX = "^01(?:0|1|[6-9])?(\\d{3}|\\d{4})?(\\d{4})$"; + private static final String PHONE_REGEX = "^\\d{11}$"; @Override public boolean isValid(String value, ConstraintValidatorContext context) { - return Pattern.matches(value, PHONE_REGEX); + return Pattern.matches(PHONE_REGEX, value); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java index 6c5400571..338396a0c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/valid/annotation/Phone.java @@ -1,17 +1,16 @@ package site.timecapsulearchive.core.global.common.valid.annotation; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import site.timecapsulearchive.core.global.common.valid.PhoneValidator; -@Target({FIELD}) -@Retention(RUNTIME) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) @Documented public @interface Phone { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java index f53c3a414..914973c6b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/resttemplate/AligoRestTemplateConfig.java @@ -23,6 +23,11 @@ @RequiredArgsConstructor public class AligoRestTemplateConfig { + private static final int MAX_CONNECTION_TOTAL = 10; + private static final int MAX_CONNECTION_PER_ROUTE = 10; + private static final int CONNECTION_TIMEOUT = 5000; + private static final int CONNECTION_REQUEST_TIMEOUT = 5000; + private static final int KEEP_ALIVE_SECONDS = 30; private static final String ALIGO_API_URL = "https://apis.aligo.in"; @Bean @@ -53,8 +58,8 @@ private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( private ClientHttpRequestFactory clientHttpRequestFactory() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); - factory.setConnectTimeout(5000); - factory.setConnectionRequestTimeout(5000); + factory.setConnectTimeout(CONNECTION_TIMEOUT); + factory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT); factory.setHttpClient(httpClient()); return factory; @@ -63,15 +68,15 @@ private ClientHttpRequestFactory clientHttpRequestFactory() { private HttpClient httpClient() { return HttpClientBuilder.create() .setConnectionManager(connectionManager()) - .evictIdleConnections(TimeValue.ofMinutes(3)) + .evictIdleConnections(TimeValue.ofSeconds(KEEP_ALIVE_SECONDS)) .evictExpiredConnections() .build(); } private HttpClientConnectionManager connectionManager() { return PoolingHttpClientConnectionManagerBuilder.create() - .setMaxConnTotal(100) - .setMaxConnPerRoute(10) + .setMaxConnTotal(MAX_CONNECTION_TOTAL) + .setMaxConnPerRoute(MAX_CONNECTION_PER_ROUTE) .build(); } }