diff --git a/src/main/java/ai/softeer/caecae/findinggame/api/FindingGamePlayController.java b/src/main/java/ai/softeer/caecae/findinggame/api/FindingGamePlayController.java new file mode 100644 index 0000000..4674ee8 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/api/FindingGamePlayController.java @@ -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> checkAnswer(@RequestBody AnswerRequestDto req) { + return SuccessResponse.of(SuccessCode.OK, findingGamePlayService.checkAnswer(req)); + } + + /** + * 선착순에 든 참여자가 전화번호를 입력 + * + * @param req + * @return + */ + @PostMapping("/register") + public ResponseEntity> registWinner(@RequestBody RegisterWinnerRequestDto req) { + return SuccessResponse.of(SuccessCode.OK, findingGamePlayService.registWinner(req)); + } +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/AnswerRequestDto.java b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/AnswerRequestDto.java new file mode 100644 index 0000000..6b2d4e5 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/AnswerRequestDto.java @@ -0,0 +1,8 @@ +package ai.softeer.caecae.findinggame.domain.dto.request; + +import java.util.List; + +public record AnswerRequestDto( + List answerList +) { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/CoordDto.java b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/CoordDto.java new file mode 100644 index 0000000..a59008e --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/CoordDto.java @@ -0,0 +1,7 @@ +package ai.softeer.caecae.findinggame.domain.dto.request; + +public record CoordDto( + int coordX, + int coordY +) { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/RegisterWinnerRequestDto.java b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/RegisterWinnerRequestDto.java new file mode 100644 index 0000000..da205e8 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/request/RegisterWinnerRequestDto.java @@ -0,0 +1,7 @@ +package ai.softeer.caecae.findinggame.domain.dto.request; + +public record RegisterWinnerRequestDto( + String ticketId, + String phone +) { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/AnswerResponseDto.java b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/AnswerResponseDto.java new file mode 100644 index 0000000..c817a6b --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/AnswerResponseDto.java @@ -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 correctAnswerList, + String ticketId, + Long startTime +) { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/RegisterWinnerResponseDto.java b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/RegisterWinnerResponseDto.java new file mode 100644 index 0000000..685124d --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/dto/response/RegisterWinnerResponseDto.java @@ -0,0 +1,9 @@ +package ai.softeer.caecae.findinggame.domain.dto.response; + +import lombok.Builder; + +@Builder +public record RegisterWinnerResponseDto( + boolean success +) { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinner.java b/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinner.java index 959d15c..e587316 100644 --- a/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinner.java +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinner.java @@ -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 diff --git a/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinnerId.java b/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinnerId.java index 94fbdf8..9cbd162 100644 --- a/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinnerId.java +++ b/src/main/java/ai/softeer/caecae/findinggame/domain/entity/FindingGameWinnerId.java @@ -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; diff --git a/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameRedisRepository.java b/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameRedisRepository.java new file mode 100644 index 0000000..49418be --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameRedisRepository.java @@ -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 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); + } +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameWinnerRepository.java b/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameWinnerRepository.java new file mode 100644 index 0000000..4d23181 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/repository/FindingGameWinnerRepository.java @@ -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 { +} diff --git a/src/main/java/ai/softeer/caecae/findinggame/service/FindingGamePlayService.java b/src/main/java/ai/softeer/caecae/findinggame/service/FindingGamePlayService.java new file mode 100644 index 0000000..d96a039 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/findinggame/service/FindingGamePlayService.java @@ -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 answerList = req.answerList(); // 사용자가 보낸 리스트 + List correctList = new ArrayList<>(); // 정답 리스트 + correctList.add(new CoordDto(10, 10)); // 임시 정답 데이터 + correctList.add(new CoordDto(30, 30)); // TODO: 정답 정보 가져와야함 + List 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(); + } +} diff --git a/src/main/java/ai/softeer/caecae/user/domain/entity/User.java b/src/main/java/ai/softeer/caecae/user/domain/entity/User.java index 77164f1..1c3c459 100644 --- a/src/main/java/ai/softeer/caecae/user/domain/entity/User.java +++ b/src/main/java/ai/softeer/caecae/user/domain/entity/User.java @@ -9,7 +9,7 @@ @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor public class User extends BaseEntity { @Id