Skip to content

Commit

Permalink
feat : 선착순 API 기능 일부 구현 (CC-141)
Browse files Browse the repository at this point in the history
  • Loading branch information
putdata committed Aug 13, 2024
1 parent 01971ab commit c70cda2
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ai.softeer.caecae.findinggame.api;

import ai.softeer.caecae.findinggame.domain.dto.request.AnswerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.request.RegistWinnerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.response.AnswerResponseDto;
import ai.softeer.caecae.findinggame.domain.dto.response.RegistWinnerResponseDto;
import ai.softeer.caecae.findinggame.service.FindingGamePlayService;
import ai.softeer.caecae.global.dto.response.SuccessResponse;
import ai.softeer.caecae.global.enums.SuccessCode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/finding")
public class FindingGamePlayController {
private final FindingGamePlayService findingGamePlayService;

/**
*
* @param req
* @return
*/
@PostMapping("/answer")
public ResponseEntity<SuccessResponse<AnswerResponseDto>> checkAnswer(@RequestBody AnswerRequestDto req) {
return SuccessResponse.of(SuccessCode.OK, findingGamePlayService.checkAnswer(req));
}

@PostMapping("/regist")
public ResponseEntity<SuccessResponse<RegistWinnerResponseDto>> registWinner(@RequestBody RegistWinnerRequestDto req) {
return SuccessResponse.of(SuccessCode.OK, findingGamePlayService.registWinner(req));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ai.softeer.caecae.findinggame.domain.dto.request;

import java.util.List;

public record AnswerRequestDto(
List<CoordDto> answerList
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ai.softeer.caecae.findinggame.domain.dto.request;

public record CoordDto(
int coordX,
int coordY
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ai.softeer.caecae.findinggame.domain.dto.request;

public record RegistWinnerRequestDto(
String ticketId,
String phone
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ai.softeer.caecae.findinggame.domain.dto.response;

import ai.softeer.caecae.findinggame.domain.dto.request.CoordDto;
import lombok.Builder;

import java.util.List;

@Builder
public record AnswerResponseDto(
List<CoordDto> correctAnswerList,
String ticketId,
Long startTime
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ai.softeer.caecae.findinggame.domain.dto.response;

import lombok.Builder;

@Builder
public record RegistWinnerResponseDto(
boolean success
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
import ai.softeer.caecae.global.entity.BaseEntity;
import ai.softeer.caecae.user.domain.entity.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@IdClass(FindingGameWinnerId.class)
public class FindingGameWinner extends BaseEntity {
@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package ai.softeer.caecae.findinggame.domain.entity;

import ai.softeer.caecae.user.domain.entity.User;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@NoArgsConstructor
public class FindingGameWinnerId implements Serializable {
private User user;
private FindingGame findingGame;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ai.softeer.caecae.findinggame.repository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
@RequiredArgsConstructor
public class FindingGameRedisRepository {
private final static String TICKET_KEY = "ticket";
private final static String COUNT_KEY = "count";
private final static String WINNER_KEY = "winner";
private final RedisTemplate<String, Object> redisTemplate;

/**
* 당첨자 수 증가
*
* @return 현재 당첨자 수 값
*/
public Long increaseCount() {
return redisTemplate.opsForValue().increment(COUNT_KEY);
}

/**
* 당첨자 수 감소
*
* @return 현재 당첨자 수 값
*/
public Long decreaseCount() {
return redisTemplate.opsForValue().decrement(COUNT_KEY);
}


/**
* 선착순에 들은 참여자를 위너 목록에 추가한다.
*
* @param ticketId
* @return 시작 시간
*/
public Long addWinner(String ticketId) {
Long startTime = System.currentTimeMillis();
redisTemplate.opsForHash().put(WINNER_KEY, ticketId, startTime);
return startTime;
}

/**
* 위너 목록에서 참여자를 삭제한다.
*
* @param ticketId
*/
public void deleteWinner(String ticketId) {
redisTemplate.opsForHash().delete(WINNER_KEY, ticketId);
}

/**
* TicketID를 가진 참여자의 입력 시작 시간을 가져온다.
*
* @param ticketId
* @return
*/
public Long getWinnerStartTime(String ticketId) {
return (Long) redisTemplate.opsForHash().get(WINNER_KEY, ticketId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ai.softeer.caecae.findinggame.repository;

import ai.softeer.caecae.findinggame.domain.entity.FindingGameWinner;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FindingGameWinnerRepository extends JpaRepository<FindingGameWinner, Integer> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package ai.softeer.caecae.findinggame.service;

import ai.softeer.caecae.findinggame.domain.dto.request.AnswerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.request.CoordDto;
import ai.softeer.caecae.findinggame.domain.dto.request.RegistWinnerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.response.AnswerResponseDto;
import ai.softeer.caecae.findinggame.domain.dto.response.RegistWinnerResponseDto;
import ai.softeer.caecae.findinggame.domain.entity.FindingGameWinner;
import ai.softeer.caecae.findinggame.repository.FindingGameDbRepository;
import ai.softeer.caecae.findinggame.repository.FindingGameRedisRepository;
import ai.softeer.caecae.findinggame.repository.FindingGameWinnerRepository;
import ai.softeer.caecae.user.domain.entity.User;
import ai.softeer.caecae.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class FindingGamePlayService {
private final FindingGameRedisRepository findingGameRedisRepository;
private final FindingGameDbRepository findingGameDbRepository;
private final FindingGameWinnerRepository findingGameWinnerRepository;
private final UserRepository userRepository;

/**
* 사용자가 보낸 정답을 채점하고 모두 맞으면 선착순 인원에 들었는지 확인하는 서비스 로직
*
* @param req
* @return
*/
public AnswerResponseDto checkAnswer(AnswerRequestDto req) {
// 정답 판단
List<CoordDto> answerList = req.answerList(); // 사용자가 보낸 리스트
List<CoordDto> correctList = new ArrayList<>(); // 정답 리스트
correctList.add(new CoordDto(10, 10)); // 임시 정답 데이터
correctList.add(new CoordDto(30, 30)); // TODO: 정답 정보 가져와야함
List<CoordDto> correctAnswerList = new ArrayList<>(); // 사용자에게 보낼 채점한 리스트
int count = 0;
for (CoordDto answer : answerList) {
int x = answer.coordX(), y = answer.coordY();
log.info("X: {}, Y: {}", x, y);
for (CoordDto correct : correctList) {
int cx = correct.coordX(), cy = correct.coordY();
int diff = (x - cx) * (x - cx) + (y - cy) * (y - cy); // 점과 점 사이의 거리 제곱 공식
if (diff <= 900) { // TODO: 정답 범위 _ 수정 요망
correctAnswerList.add(correct);
count++;
correctList.remove(correct);
break;
}
}
}

// 정답 개수가 0이거나 1 || 남은 선착순 자리 체크
if (count != 2 || findingGameRedisRepository.increaseCount() > 315L) { // TODO: 오늘 선착순 인원 정보 가져와야함
if (count == 2) findingGameRedisRepository.decreaseCount();
return AnswerResponseDto.builder()
.correctAnswerList(correctAnswerList)
.ticketId("")
.startTime(-1L)
.build();
}

// TicketId 생성
String ticketId = UUID.randomUUID().toString();
Long startTime = findingGameRedisRepository.addWinner(ticketId);
return AnswerResponseDto.builder()
.correctAnswerList(correctAnswerList)
.ticketId(ticketId)
.startTime(startTime)
.build();
}

/**
* 선착순에 든 사용자가 전화번호를 입력했을 때 처리하는 서비스 로직
*
* @param req
* @return 정답 리스트와 선착순 참가자 등록 정보
*/
@Transactional
public RegistWinnerResponseDto registWinner(RegistWinnerRequestDto req) {
String ticketId = req.ticketId();
Long startTime = findingGameRedisRepository.getWinnerStartTime(ticketId);
if (startTime == null) return RegistWinnerResponseDto.builder().success(false).build(); // 목록에 없으면
log.info("사용자 시작시간: {}", startTime);
Long endTime = System.currentTimeMillis() - 1000L; // 1초 지연 허용
if (endTime - startTime > 1000L * 60 * 3) { // 3분 초과 - 실패 및 당첨자 제외
findingGameRedisRepository.decreaseCount();
findingGameRedisRepository.deleteWinner(ticketId);
return RegistWinnerResponseDto.builder().success(false).build();
}
Integer gameId = 1;
String phone = req.phone();
findingGameDbRepository.findById(gameId);
FindingGameWinner winner = FindingGameWinner.builder()
.user(userRepository.findByPhone(phone).orElseGet(() -> userRepository.save(
User.builder()
.phone(phone)
.build()
)))
.findingGame(findingGameDbRepository.findById(gameId).get())
.build();
findingGameWinnerRepository.save(winner);
findingGameRedisRepository.deleteWinner(ticketId);
return RegistWinnerResponseDto.builder().success(true).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class User extends BaseEntity {

@Id
Expand Down

0 comments on commit c70cda2

Please sign in to comment.