Skip to content

Commit

Permalink
Merge pull request #136 from softeerbootcamp4th/feature/133-refactor-sms
Browse files Browse the repository at this point in the history
[refactor] 인증번호 전송 기능 리팩토링 + DrawEvent 복수개 조회하지 않도록 수정 (#133)
  • Loading branch information
win-luck authored Aug 22, 2024
2 parents 7dd36ee + aef6c08 commit 81e5ebd
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface EventFrameRepository extends JpaRepository<EventFrame, Long> {

Optional<EventFrame> findByFrameId(String frameId);

boolean existsByFrameId(String frameId);

@Query("SELECT ef.frameId FROM EventFrame ef WHERE ef.frameId LIKE %:search%")
List<String> findAllFrameIdsWithLike(@Param("search") String search);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ public interface DrawEventRepository extends JpaRepository<DrawEvent, Long> {
@Query(value = "SELECT d FROM DrawEvent d WHERE d.eventMetadata.eventId = :eventId")
Optional<DrawEvent> findByEventId(@Param("eventId") String eventId);

// eventframeId로 eventmetadata의 drawevent를 찾는 fetch join 쿼리 (N+1 문제 방지)
@Query(value = "SELECT d FROM DrawEvent d JOIN FETCH d.eventMetadata em JOIN FETCH em.eventFrame ef WHERE ef.frameId = :eventFrameId")
@Query(value = "SELECT d.* FROM draw_event d " +
"JOIN event_metadata em ON d.event_metadata_id = em.id " +
"JOIN event_frame ef ON em.event_frame_id = ef.id " +
"WHERE ef.frame_id = :eventFrameId " +
"LIMIT 1", nativeQuery = true)
Optional<DrawEvent> findByEventFrameId(@Param("eventFrameId") String eventFrameId);

// @Modifying // 수정 쿼리는 Modifying 필요
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import hyundai.softeer.orange.eventuser.dto.RequestAuthCodeDto;
import hyundai.softeer.orange.eventuser.dto.RequestUserDto;
import hyundai.softeer.orange.eventuser.service.EventUserService;
import hyundai.softeer.orange.eventuser.service.SmsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -23,7 +22,6 @@
public class EventUserController {

private final EventUserService eventUserService;
private final SmsService smsService;

// 로그인
@Tag(name = "EventUser")
Expand All @@ -42,14 +40,18 @@ public ResponseEntity<TokenDto> login(@RequestBody @Valid RequestUserDto dto) {

// 인증번호 전송
@Tag(name = "EventUser")
@PostMapping("/send-auth")
@PostMapping("/send-auth/{eventFrameId}")
@Operation(summary = "인증번호 전송", description = "유저의 전화번호에 인증번호를 전송한다.", responses = {
@ApiResponse(responseCode = "200", description = "인증번호 전송 성공"),
@ApiResponse(responseCode = "400", description = "입력받은 정보의 유효성 검사가 실패했을 때",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "해당 이벤트 프레임이 존재하지 않을 때",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "해당 이벤트의 유저로 이미 가입되어 있을 때",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<Void> sendAuthCode(@RequestBody @Valid RequestUserDto dto) {
smsService.sendSms(dto);
public ResponseEntity<Void> sendAuthCode(@RequestBody @Valid RequestUserDto dto, @PathVariable String eventFrameId) {
eventUserService.sendAuthCode(dto, eventFrameId);
return ResponseEntity.ok().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface EventUserRepository extends JpaRepository<EventUser, Long>, Cus

Optional<EventUser> findByUserNameAndPhoneNumber(String userName, String phoneNumber);

boolean existsByPhoneNumberAndEventFrameFrameId(String phoneNumber, String frameId);

Optional<EventUser> findByUserId(String userId);

@Query("SELECT eu FROM EventUser eu WHERE eu.userId IN :userIds")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package hyundai.softeer.orange.eventuser.service;

import hyundai.softeer.orange.common.ErrorCode;
import hyundai.softeer.orange.common.util.ConstantUtil;
import hyundai.softeer.orange.eventuser.config.CoolSmsApiConfig;
import hyundai.softeer.orange.eventuser.dto.RequestUserDto;
import hyundai.softeer.orange.eventuser.exception.EventUserException;
import hyundai.softeer.orange.eventuser.repository.EventUserRepository;
import lombok.extern.slf4j.Slf4j;
import net.nurigo.sdk.NurigoApp;
import net.nurigo.sdk.message.model.Message;
import net.nurigo.sdk.message.request.SingleMessageSendingRequest;
import net.nurigo.sdk.message.response.SingleMessageSentResponse;
import net.nurigo.sdk.message.service.DefaultMessageService;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -20,28 +18,23 @@
import java.util.concurrent.TimeUnit;

@Slf4j
@Primary
@Service
public class CoolSmsService implements SmsService {

private final DefaultMessageService defaultMessageService;
private final CoolSmsApiConfig coolSmsApiConfig;
private final StringRedisTemplate stringRedisTemplate;
private final EventUserRepository eventUserRepository;

public CoolSmsService(CoolSmsApiConfig coolSmsApiConfig, StringRedisTemplate stringRedisTemplate, EventUserRepository eventUserRepository) {
public CoolSmsService(CoolSmsApiConfig coolSmsApiConfig, StringRedisTemplate stringRedisTemplate) {
this.defaultMessageService = NurigoApp.INSTANCE.initialize(coolSmsApiConfig.getApiKey(), coolSmsApiConfig.getApiSecret(), coolSmsApiConfig.getUrl());
this.coolSmsApiConfig = coolSmsApiConfig;
this.stringRedisTemplate = stringRedisTemplate;
this.eventUserRepository = eventUserRepository;
}

@Override
@Transactional(readOnly = true)
public void sendSms(RequestUserDto dto) {
if(eventUserRepository.existsByPhoneNumber(dto.getPhoneNumber())) {
throw new EventUserException(ErrorCode.PHONE_NUMBER_ALREADY_EXISTS);
}

String authCode = generateAuthCode();
Message message = new Message();
message.setFrom(coolSmsApiConfig.getFrom());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import hyundai.softeer.orange.core.jwt.JWTManager;
import hyundai.softeer.orange.event.common.entity.EventFrame;
import hyundai.softeer.orange.event.common.repository.EventFrameRepository;
import hyundai.softeer.orange.eventuser.dto.EventUserOnAdminDto;
import hyundai.softeer.orange.eventuser.dto.EventUserPageDto;
import hyundai.softeer.orange.eventuser.dto.RequestAuthCodeDto;
import hyundai.softeer.orange.eventuser.dto.RequestUserDto;
Expand All @@ -20,7 +19,6 @@
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -33,6 +31,7 @@
public class EventUserService {

private static final Logger log = LoggerFactory.getLogger(EventUserService.class);
private final SmsService smsService;
private final EventUserRepository eventUserRepository;
private final EventFrameRepository eventFrameRepository;
private final StringRedisTemplate stringRedisTemplate;
Expand Down Expand Up @@ -60,6 +59,26 @@ public EventUserPageDto getUserBySearch(String search, int page, int size) {
return EventUserPageDto.from(userPage);
}

/**
* 1. 유저의 전화번호 및 이벤트 프레임 아이디로 이미 해당 이벤트에 가입된 유저인지 검증
* 2. 없다면 인증번호 발송
* @param dto
* @param eventFrameId
*/
@Transactional(readOnly = true)
public void sendAuthCode(RequestUserDto dto, String eventFrameId){
// 이벤트 프레임이 존재하지 않는 경우
if(!eventFrameRepository.existsByFrameId(eventFrameId)){
throw new EventUserException(ErrorCode.EVENT_FRAME_NOT_FOUND);
}

// 이미 해당 이벤트에 가입된 유저인 경우
if(eventUserRepository.existsByPhoneNumberAndEventFrameFrameId(dto.getPhoneNumber(), eventFrameId)){
throw new EventUserException(ErrorCode.USER_ALREADY_EXISTS);
}
smsService.sendSms(dto);
}

/**
* 1. 유저가 입력한 인증번호와 Redis에 저장된 인증번호 비교
* 2. 일치하면 신규 유저 저장하고 JWT 토큰 발급
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
import java.util.Map;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand Down Expand Up @@ -118,15 +117,47 @@ void login404Test() throws Exception {
void sendAuthCodeTest() throws Exception {
// given
String requestBody = mapper.writeValueAsString(requestUserDto);
doNothing().when(smsService).sendSms(any(RequestUserDto.class));
doNothing().when(eventUserService).sendAuthCode(any(RequestUserDto.class), anyString());

// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event-user/send-auth")
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event-user/send-auth/" + eventFrameId)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk());
}

@DisplayName("sendAuthCode: 인증번호 전송 API를 호출할 때 이벤트 프레임이 존재하지 않아 예외가 발생한다.")
@Test
void sendAuthCode404Test() throws Exception {
// given
String requestBody = mapper.writeValueAsString(requestUserDto);
String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_FRAME_NOT_FOUND));
doThrow(new EventUserException(ErrorCode.EVENT_FRAME_NOT_FOUND)).when(eventUserService).sendAuthCode(any(RequestUserDto.class), anyString());

// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event-user/send-auth/" + eventFrameId)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isNotFound())
.andExpect(content().json(responseBody));
}

@DisplayName("sendAuthCode: 인증번호 전송 API를 호출할 때 이미 가입된 유저로 간주해 예외가 발생한다.")
@Test
void sendAuthCode409Test() throws Exception {
// given
String requestBody = mapper.writeValueAsString(requestUserDto);
String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.USER_ALREADY_EXISTS));
doThrow(new EventUserException(ErrorCode.USER_ALREADY_EXISTS)).when(eventUserService).sendAuthCode(any(RequestUserDto.class), anyString());

// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event-user/send-auth/" + eventFrameId)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isConflict())
.andExpect(content().json(responseBody));
}

@DisplayName("checkAuthCode: 인증번호 검증 API를 호출한다.")
@Test
void checkAuthCodeTest() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import hyundai.softeer.orange.eventuser.exception.EventUserException;
import hyundai.softeer.orange.eventuser.repository.EventUserRepository;
import hyundai.softeer.orange.eventuser.service.EventUserService;
import hyundai.softeer.orange.eventuser.service.SmsService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -30,12 +31,17 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;

class EventUserServiceTest {

@InjectMocks
private EventUserService eventUserService;

@Mock
private SmsService smsService;

@Mock
private EventUserRepository eventUserRepository;

Expand Down Expand Up @@ -94,6 +100,53 @@ void loginNotFoundTest() {
.hasMessage(ErrorCode.EVENT_USER_NOT_FOUND.getMessage());
}

@DisplayName("sendAuthCode: 유저가 인증번호를 전송한다.")
@Test
void sendAuthCodeTest() {
// given
given(eventFrameRepository.existsByFrameId(eventFrameId))
.willReturn(true);
given(eventUserRepository.existsByPhoneNumberAndEventFrameFrameId(requestUserDto.getPhoneNumber(), eventFrameId))
.willReturn(false);
doNothing().when(smsService).sendSms(requestUserDto);

// when
eventUserService.sendAuthCode(requestUserDto, eventFrameId);

// then
verify(eventFrameRepository).existsByFrameId(eventFrameId);
verify(eventUserRepository).existsByPhoneNumberAndEventFrameFrameId(requestUserDto.getPhoneNumber(), eventFrameId);
verify(smsService).sendSms(requestUserDto);
}

@DisplayName("sendAuthCode: 유저가 인증번호를 전송하려 할 때 이벤트 프레임을 찾을 수 없으면 예외가 발생한다.")
@Test
void sendAuthCodeNotFoundTest() {
// given
given(eventFrameRepository.existsByFrameId(eventFrameId))
.willReturn(false);

// when & then
assertThatThrownBy(() -> eventUserService.sendAuthCode(requestUserDto, eventFrameId))
.isInstanceOf(EventUserException.class)
.hasMessage(ErrorCode.EVENT_FRAME_NOT_FOUND.getMessage());
}

@DisplayName("sendAuthCode: 유저가 인증번호를 전송하려 할 때 이미 가입되었음이 확인될 경우 예외가 발생한다.")
@Test
void sendAuthCodeConflictTest() {
// given
given(eventFrameRepository.existsByFrameId(eventFrameId))
.willReturn(true);
given(eventUserRepository.existsByPhoneNumberAndEventFrameFrameId(requestUserDto.getPhoneNumber(), eventFrameId)) // 이미 가입된 유저
.willReturn(true);

// when & then
assertThatThrownBy(() -> eventUserService.sendAuthCode(requestUserDto, eventFrameId))
.isInstanceOf(EventUserException.class)
.hasMessage(ErrorCode.USER_ALREADY_EXISTS.getMessage());
}

@DisplayName("checkAuthCode: 유저가 전송한 인증번호를 Redis 상에서 확인하고 성공한다.")
@ParameterizedTest
@ValueSource(strings = {"123456", "654321", "111111", "921345"})
Expand Down

0 comments on commit 81e5ebd

Please sign in to comment.