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 : 선착순 API 기능 일부 구현 (CC-141) #30

Merged
merged 4 commits into from
Aug 14, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package ai.softeer.caecae.findinggame.api;

import ai.softeer.caecae.findinggame.domain.dto.request.AnswerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.request.RegisterWinnerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.response.AnswerResponseDto;
import ai.softeer.caecae.findinggame.domain.dto.response.RegisterWinnerResponseDto;
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));
}

/**
* 선착순에 든 참여자가 전화번호를 입력
*
* @param req
* @return
*/
@PostMapping("/register")
public ResponseEntity<SuccessResponse<RegisterWinnerResponseDto>> registWinner(@RequestBody RegisterWinnerRequestDto 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 RegisterWinnerRequestDto(
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 RegisterWinnerResponseDto(
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,122 @@
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.RegisterWinnerRequestDto;
import ai.softeer.caecae.findinggame.domain.dto.response.AnswerResponseDto;
import ai.softeer.caecae.findinggame.domain.dto.response.RegisterWinnerResponseDto;
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;

private final int MAX_ANSWER_COUNT = 2;
private final int ANSWER_RANGE = 900;
private final long CONSTRAINT_TIME = 1000L * 60 * 3;

/**
* 사용자가 보낸 정답을 채점하고 모두 맞으면 선착순 인원에 들었는지 확인하는 서비스 로직
*
* @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 <= ANSWER_RANGE) { // TODO: 정답 범위 _ 수정 요망
correctAnswerList.add(correct);
count++;
correctList.remove(correct);
break;
}
}
}

// 정답 개수가 0이거나 1 || 남은 선착순 자리 체크
if (count != MAX_ANSWER_COUNT || findingGameRedisRepository.increaseCount() > 315L) { // TODO: 오늘 선착순 인원 정보 가져와야함
if (count == MAX_ANSWER_COUNT) 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 RegisterWinnerResponseDto registWinner(RegisterWinnerRequestDto req) {
String ticketId = req.ticketId();
Long startTime = findingGameRedisRepository.getWinnerStartTime(ticketId);
if (startTime == null) return RegisterWinnerResponseDto.builder().success(false).build(); // 목록에 없으면
log.info("사용자 시작시간: {}", startTime);
Long endTime = System.currentTimeMillis() - 1000L; // 1초 지연 허용
if (endTime - startTime > CONSTRAINT_TIME) { // 3분 초과 - 실패 및 당첨자 제외
findingGameRedisRepository.decreaseCount();
findingGameRedisRepository.deleteWinner(ticketId);
return RegisterWinnerResponseDto.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 RegisterWinnerResponseDto.builder()
.success(true)
.build();
}
}
Loading
Loading