Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Feat: 전화번호 기반 sms 인증 요청, 검증 API 구현 #100

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ dependencies {
// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'


// feign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.3'
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
implementation platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")

runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/example/tree/TreeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@SpringBootApplication
@EnableJpaAuditing
@EnableFeignClients
@EnableRedisRepositories
public class TreeApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package org.example.tree.domain.member.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.example.tree.domain.member.dto.MemberRequestDTO;
import org.example.tree.domain.member.dto.MemberResponseDTO;
import org.example.tree.domain.member.service.MemberService;
import org.example.tree.global.common.ApiResponse;
import org.example.tree.global.feign.service.NcpService;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClientException;

import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class MemberController {
private final MemberService memberService;
private final NcpService ncpService;


@PostMapping("/checkId")
Expand Down Expand Up @@ -44,4 +54,32 @@ public ApiResponse<MemberResponseDTO.registerMember> loginTemp(
){
return ApiResponse.onSuccess((memberService.login(request)));
}


@Operation(summary = "인증번호 요청 API ✔️️", description = "인증번호 요청 API입니다. 대시(-) 제외 전화번호 입력하시면 됩니다. ex) 01012345678 ")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK 성공 , 인증번호 전송 완료"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "NCP200_1", description = "OK 성공 , 이미 회원가입된 전화번호입니다."),

})
@PostMapping("/phone/sms")
public ApiResponse<MemberResponseDTO.checkSentSms> sendSms(@RequestBody MemberRequestDTO.SmsRequestDto request) throws JsonProcessingException, RestClientException, URISyntaxException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
memberService.existsByPhoneNum(request.getTargetPhoneNum());
MemberResponseDTO.checkSentSms authNumResultDto = ncpService.sendSms(request.getTargetPhoneNum());
return ApiResponse.onSuccess(authNumResultDto);
}

//인증번호 검증
@Operation(summary = "인증번호 검증 API ✔️️", description = "인증번호 검증 API입니다. 대시(-) 제외 전화번호와 인증번호 입력하시면 됩니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK 성공 , 인증 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "NCP400_1", description = "BAD_REQUEST, 전화번호를 잘못 전달했거나, 인증요청을 하지않은 상태로 확인버튼을 누른 경우"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "NCP400_2", description = "BAD_REQUEST, 인증 번호가 옳지 않습니다."),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "NCP400_3", description = "BAD_REQUEST, 인증 시간(5분)이 지났습니다."),
})
@PostMapping("/phone/auth")
public ApiResponse<MemberResponseDTO.checkPhoneAuth> authPhoneNum(@RequestBody MemberRequestDTO.PhoneNumAuthDto request) {
MemberResponseDTO.checkPhoneAuth authNumResultDto = ncpService.authNumber(request.getAuthNum(), request.getPhoneNum());
return ApiResponse.onSuccess(authNumResultDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,30 @@ public static class reissue {
@NotBlank
private String refreshToken;
}

@Getter
public static class SmsRequestDto {
@Override
public String toString() {
return "SmsRequestDto{" +
"targetPhoneNum='" + targetPhoneNum + '\'' +
'}';
}

private String targetPhoneNum;
}

@Getter
public static class PhoneNumAuthDto {
@Override
public String toString() {
return "PhoneNumAuthDto{" +
"phoneNum='" + phoneNum + '\'' +
", authNum=" + authNum +
'}';
}

private String phoneNum;
private Integer authNum;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,20 @@ public static class reissue {
private String accessToken;
private String refreshToken;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class checkPhoneAuth{
boolean authenticated;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class checkSentSms{
boolean messageSent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.example.tree.domain.member.entity.redis;

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import java.time.LocalDateTime;

@RedisHash(value = "phoneAuthTreeHouse", timeToLive = 1830)
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PhoneAuth {
@Id
private String phoneNum;
private LocalDateTime authNumTime;
private Integer authNum;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByUserName(String userName);

boolean existsByPhone(String phone);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.example.tree.domain.member.repository.MemberRepository;
import org.example.tree.global.exception.GeneralException;
import org.example.tree.global.exception.GlobalErrorCode;
import org.example.tree.global.exception.MemberException;
import org.example.tree.global.security.provider.TokenProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -32,4 +33,10 @@ public Boolean existById(Long id){
public Optional<Member> findByPhoneNumber(String phone) {
return memberRepository.findByPhone(phone);
}

public void existByPhoneNumber(String phoneNum) {
if (memberRepository.existsByPhone(phoneNum)) {
throw new MemberException(GlobalErrorCode.PHONE_NUMBER_EXIST);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ public MemberResponseDTO.reissue reissue(MemberRequestDTO.reissue request) {
return memberConverter.toReissue(token.getAccessToken(), token.getRefreshToken());
}

public void existsByPhoneNum(String phoneNum) {
memberQueryService.existByPhoneNumber(phoneNum);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.tree.global.exception;

public class CustomFeignException extends GeneralException {
public CustomFeignException(BaseErrorCode errorCode){
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ public enum GlobalErrorCode implements BaseErrorCode{
//Notification
//404 Not Found - 찾을 수 없음
NOTIFICATION_NOT_FOUND(NOT_FOUND, "NOTIFICATION", "존재하지 않는 알림입니다."),

//FeignClient
FEIGN_CLIENT_ERROR_400(BAD_REQUEST, "FEIGN400", "feignClient 에서 400번대 에러가 발생했습니다."),
FEIGN_CLIENT_ERROR_500(INTERNAL_SERVER_ERROR, "FEIGN500", "feignClient 에서 500번대 에러가 발생했습니다."),

// NCP Phone Auth
PHONE_NUMBER_EXIST(OK, "NCP200_1", "이미 인증된 전화번호입니다."),
PHONE_AUTH_NOT_FOUND(BAD_REQUEST, "NCP400_1", "인증 번호 요청이 필요합니다."),
PHONE_AUTH_WRONG(BAD_REQUEST, "NCP400_2", "잘못된 인증 번호 입니다."),
PHONE_AUTH_TIMEOUT(BAD_REQUEST, "NCP400_3", "인증 시간이 초과되었습니다."),

;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.tree.global.exception;

public class MemberException extends GeneralException {
public MemberException(BaseErrorCode errorCode){
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.tree.global.exception;

public class PhoneAuthException extends GeneralException {
public PhoneAuthException(BaseErrorCode errorCode){
super(errorCode);
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/example/tree/global/feign/NcpFeignClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example.tree.global.feign;

import org.example.tree.global.feign.config.NcpFeignConfiguration;
import org.example.tree.global.feign.dto.NcpDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "feign", url = "https://sens.apigw.ntruss.com/sms/v2/services", configuration = NcpFeignConfiguration.class)
public interface NcpFeignClient {
@PostMapping(value = "/{serviceId}/messages")
void sendSms(@PathVariable(value = "serviceId") String serviceId,
@RequestHeader HttpHeaders headers,
@RequestBody NcpDto.SmsRequestDto request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.example.tree.global.feign.config;

import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.codec.ErrorDecoder;
import lombok.RequiredArgsConstructor;
import org.example.tree.global.feign.exception.FeignExceptionErrorDecoder;
import org.springframework.context.annotation.Bean;

@RequiredArgsConstructor
public class NcpFeignConfiguration {
@Bean
public RequestInterceptor basicAuthRequestInterceptor() {
return new ColonInterceptor();
}
public static class ColonInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.uri(template.path().replaceAll("%3A", ":"));
}
}

@Bean
public ErrorDecoder errorDecoder() {
return new FeignExceptionErrorDecoder();
}

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
31 changes: 31 additions & 0 deletions src/main/java/org/example/tree/global/feign/dto/NcpDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.example.tree.global.feign.dto;

import lombok.*;

import java.util.List;

public class NcpDto {

@Builder
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class MessageDto {
String to;
String content;
}

@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class SmsRequestDto {
String type;
String contentType;
String countryCode;
String from;
String content;
List<MessageDto> messages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example.tree.global.feign.exception;

import feign.Response;
import feign.codec.ErrorDecoder;
import org.example.tree.global.exception.CustomFeignException;
import org.example.tree.global.exception.GlobalErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FeignExceptionErrorDecoder implements ErrorDecoder {
Logger logger = LoggerFactory.getLogger(FeignExceptionErrorDecoder.class);

@Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
logger.error("{}번 에러 발생 : {}", response.status(), response.reason());
return new CustomFeignException(GlobalErrorCode.FEIGN_CLIENT_ERROR_400);
} else {
logger.error("500번대 에러 발생 : {}", response.reason());
return new CustomFeignException(GlobalErrorCode.FEIGN_CLIENT_ERROR_500);
}
}

}
Loading
Loading