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 : 숨캐찾 정답 처리 로직 캐싱 적용 및 정답 관련 정보를 coord -> position으로 수정 (CC-159) #34

Merged
merged 7 commits into from
Aug 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// 숨은캐스퍼찾기 정답 정보
@Builder
public record FindingGameAnswerDto(
int coordX,
int coordY,
double positionX,
double positionY,
String descriptionImageUrl,
String title,
String content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ public FindingGameDailyAnswerResponseDto saveFindingGameDailyAnswer(
FindingGameAnswer findingGameAnswer = findingGameAnswerList.get(idx);

findingGameAnswer.updateFindingGame(
findingGameAnswerDto.coordX(),
findingGameAnswerDto.coordY(),
findingGameAnswerDto.positionX(),
findingGameAnswerDto.positionY(),
findingGameAnswerDto.descriptionImageUrl(),
findingGameAnswerDto.title(),
findingGameAnswerDto.content()
Expand All @@ -102,8 +102,8 @@ private List<FindingGameAnswer> initFindingGameAnswer(FindingGame findingGame) {
List<FindingGameAnswer> findingGameAnswerList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
findingGameAnswerList.add(FindingGameAnswer.builder()
.coordX(-1)
.coordY(-1)
.positionX(-1)
.positionY(-1)
.descriptionImageUrl("no-image")
.title("no-title")
.content("no-content")
Expand Down Expand Up @@ -152,6 +152,7 @@ private List<FindingGame> initFindingGames() {
for (int day = 0; day < 7; day++) {
findingGames.add(
FindingGame.builder()
.id(day + 1) // PK를 1~7로 고정
.questionImageUrl("no-image")
.numberOfWinners(315)
.answerType(AnswerType.UNSELECTED)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ai.softeer.caecae.findinggame.domain.dto;

public record PositionDto(
double positionX,
double positionY
) {
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package ai.softeer.caecae.findinggame.domain.dto.request;

import ai.softeer.caecae.findinggame.domain.dto.PositionDto;

import java.util.List;

public record AnswerRequestDto(
List<CoordDto> answerList
List<PositionDto> answerList
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
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,
List<CorrectAnswerDto> correctAnswerList,
String ticketId,
Long startTime
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ai.softeer.caecae.findinggame.domain.dto.response;

import lombok.Builder;

@Builder
public record CorrectAnswerDto(
double positionX,
double positionY,
String descriptionImageUrl,
String title,
String content
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ public class FindingGameAnswer extends BaseEntity {
private Integer id;

@Column(nullable = false)
private int coordX;
private double positionX;

@Column(nullable = false)
private int coordY;
private double positionY;

@Column(nullable = false)
private String descriptionImageUrl;
Expand All @@ -33,9 +33,9 @@ public class FindingGameAnswer extends BaseEntity {
@JoinColumn(name = "finding_game_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private FindingGame findingGame;

public void updateFindingGame(int coordX, int coordY, String descriptionImageUrl, String title, String content) {
this.coordX = coordX;
this.coordY = coordY;
public void updateFindingGame(double positionX, double positionY, String descriptionImageUrl, String title, String content) {
this.positionX = positionX;
this.positionY = positionY;
this.descriptionImageUrl = descriptionImageUrl;
this.title = title;
this.content = content;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ai.softeer.caecae.findinggame.domain.exception;

import ai.softeer.caecae.global.enums.ErrorCode;
import lombok.Getter;

@Getter
public class FindingGameException extends RuntimeException {
private final ErrorCode errorCode;

public FindingGameException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ai.softeer.caecae.findinggame.domain.exception;

import ai.softeer.caecae.global.dto.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

/**
* FindingGame 도메인에서 에러를 핸들링하여 HttpResponse 를 반환하는 핸들러
*/
@Slf4j
@ControllerAdvice
public class FindingGameExceptionHandler {
// FindingGameException 에 대한 에러 핸들링
@ExceptionHandler(value = FindingGameException.class)
public ResponseEntity<ErrorResponse> handleFindingGameException(FindingGameException findingGameException) {
log.error(findingGameException.getMessage(), findingGameException);
return ErrorResponse.of(findingGameException.getErrorCode());
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package ai.softeer.caecae.findinggame.repository;

import ai.softeer.caecae.findinggame.domain.entity.FindingGameAnswer;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface FindingGameAnswerDbRepository extends JpaRepository<FindingGameAnswer, Integer> {
@Cacheable(value = "AllFindingGameAnswersByGameId", key = "#findingGameId")
public List<FindingGameAnswer> findAllByFindingGame_Id(Integer findingGameId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
// Redis Repository 도 만들어야 하므로 네이밍에 Db를 붙임
@Repository
public interface FindingGameDbRepository extends JpaRepository<FindingGame, Integer> {
List<FindingGame> findAllByOrderByStartTime();
List<FindingGame> findAllByOrderByStartTime(); // TODO : 조회하는 데이터의 결과가 같아서 합쳐야 함

@Cacheable(cacheNames = "RecentFindingGame")
@Query(value = "SELECT f FROM FindingGame f WHERE f.id = :id")
FindingGame findByIdCacheable(@Param("id") Integer id);

@Cacheable(cacheNames = "AllFindingGame")
@Query("SELECT f FROM FindingGame f ORDER BY f.startTime")
List<FindingGame> findAllOrderByStartTimeCacheable(); // TODO : 조회하는 데이터의 결과가 같아서 합쳐야 함
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
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.PositionDto;
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.CorrectAnswerDto;
import ai.softeer.caecae.findinggame.domain.dto.response.RegisterWinnerResponseDto;
import ai.softeer.caecae.findinggame.domain.entity.FindingGame;
import ai.softeer.caecae.findinggame.domain.entity.FindingGameAnswer;
import ai.softeer.caecae.findinggame.domain.entity.FindingGameWinner;
import ai.softeer.caecae.findinggame.domain.exception.FindingGameException;
import ai.softeer.caecae.findinggame.repository.FindingGameAnswerDbRepository;
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.global.enums.ErrorCode;
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.time.Clock;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Slf4j
Expand All @@ -27,44 +36,78 @@ public class FindingGamePlayService {
private final FindingGameRedisRepository findingGameRedisRepository;
private final FindingGameDbRepository findingGameDbRepository;
private final FindingGameWinnerRepository findingGameWinnerRepository;
private final FindingGameAnswerDbRepository findingGameAnswerDbRepository;
private final UserRepository userRepository;
private final Clock clock;

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


/**
* 사용자가 보낸 정답을 채점하고 모두 맞으면 선착순 인원에 들었는지 확인하는 서비스 로직
*
* @param req
* @return
*/
public AnswerResponseDto checkAnswer(AnswerRequestDto req) {
// TODO : req의 position 0~1 validation

// 현재 진행중인 숨은캐스퍼찾기 게임 반환
List<FindingGame> findingGames = findingGameDbRepository.findAllOrderByStartTimeCacheable();
FindingGame currentFindingGame = getCachedCurrentFindingGame(findingGames).orElseThrow(
() -> new FindingGameException(ErrorCode.CURRENT_FINDING_GAME_NOT_FOUND)
);

// 숨은캐스퍼찾기 정답 리스트 deep copy
List<FindingGameAnswer> correctList = new ArrayList<>(getCachedCurrentFindingGameAnswer(currentFindingGame.getId()));
if (correctList.size() != MAX_ANSWER_COUNT) {
throw new FindingGameException(ErrorCode.INVALID_FINDING_GAME_ANSWER);
}

// 정답 판단
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++;
List<PositionDto> answerList = req.answerList(); // 사용자가 보낸 리스트
List<CorrectAnswerDto> correctAnswerList = new ArrayList<>(); // 사용자에게 보낼 채점한 리스트
int correctAnswerCount = 0; // 사용자가 맞춘 정답의 개수
for (PositionDto answer : answerList) {
double positionX = answer.positionX();
double positionY = answer.positionY();
log.info("user answer : posX: {}, posY: {}", positionX, positionY);
for (FindingGameAnswer correct : correctList) {
double correctX = correct.getPositionX();
double correctY = correct.getPositionY();

// 점과 점 사이의 거리
double diff = Math.sqrt((positionX - correctX) * (positionX - correctX) + (positionY - correctY) * (positionY - correctY));

if (diff <= ANSWER_RADIUS) { // TODO: 정답 범위 0.1에서 조정 필요
correctAnswerCount++;
// 정답을 1개만 맞추고 1개를 지운 경우, 아래의 코드 때문에, getCachedCurrentFindingGameAnswer 에서 가져오는 정답이 2개에서 1개로 바뀌는 버그
correctList.remove(correct);
correctAnswerList.add(
CorrectAnswerDto.builder()
.positionX(positionX)
.positionY(positionY)
.descriptionImageUrl(correct.getDescriptionImageUrl())
.title(correct.getTitle())
.content(correct.getContent())
.build()
);
break;
}
}
}

// 정답 개수가 0이거나 1 || 남은 선착순 자리 체크
if (count != MAX_ANSWER_COUNT || findingGameRedisRepository.increaseCount() > 315L) { // TODO: 오늘 선착순 인원 정보 가져와야함
if (count == MAX_ANSWER_COUNT) findingGameRedisRepository.decreaseCount();
// 정답을 2개 모두 맞추지 못했거나, 선착순 315명 안에 들지 못한 경우
if (correctAnswerCount != MAX_ANSWER_COUNT ||
findingGameRedisRepository.increaseCount() > currentFindingGame.getNumberOfWinners()) {
// 정답을 모두 맞추었다면 315명안에 들지 못했으므로 카운트를 감소시켜 315로 유지
if (correctAnswerCount == MAX_ANSWER_COUNT) {
findingGameRedisRepository.decreaseCount();
log.info("정답을 모두 맞추었지만 선착순 인원 내에 들지 못하였음.");
}

return AnswerResponseDto.builder()
.correctAnswerList(correctAnswerList)
.ticketId("")
Expand Down Expand Up @@ -119,4 +162,30 @@ public RegisterWinnerResponseDto registWinner(RegisterWinnerRequestDto req) {
.success(true)
.build();
}

/**
* 캐싱된 전체 숨은그림찾기 게임을 이용하여 현재 진행중인 게임 찾기
*
* @param findingGames 전체 숨은그림찾기 게임
* @return 현재 진행중인 게임 엔티티
*/
private Optional<FindingGame> getCachedCurrentFindingGame(List<FindingGame> findingGames) {
LocalDateTime now = LocalDateTime.now(clock);
for (FindingGame findingGame : findingGames) {
if (!findingGame.getStartTime().isAfter(now) && !findingGame.getEndTime().isBefore(now)) {
return Optional.of(findingGame);
}
}
return Optional.empty();
}

/**
* 숨은캐스퍼찾기 gameId를 이용하여 캐싱된 정답 정보 가져오기
*
* @param findingGameId 숨은캐스퍼찾기 gameId
* @return 정답 정보
*/
private List<FindingGameAnswer> getCachedCurrentFindingGameAnswer(int findingGameId) {
return findingGameAnswerDbRepository.findAllByFindingGame_Id(findingGameId);
}
}
5 changes: 3 additions & 2 deletions src/main/java/ai/softeer/caecae/global/enums/CacheType.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
@RequiredArgsConstructor
public enum CacheType {
FINDING_GAME_KEY("FindingGameInfo", 10, 1000),
RECENT_FINDING_GAME("RecentFindingGame", 20, 1000);

RECENT_FINDING_GAME("RecentFindingGame", 20, 1000),
ALL_FINDING_GAME_ANSWERS_BY_GAME_ID("AllFindingGameAnswersByGameId", 20, 1000),
ALL_FINDING_GAME("AllFindingGame", 20, 1000);
/**
* cacheName : 캐시 이름
* expiredAfterWrite : 캐시 만료 시간(TTL, Second)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/softeer/caecae/global/enums/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public enum ErrorCode implements BaseCode {
*/

FINDING_GAME_OF_DAY_NOT_FOUND(-4000, "해당 날짜에 등록된 틀린그림찾기 게임이 존재하지 않습니다.", HttpStatus.NOT_FOUND),
CURRENT_FINDING_GAME_NOT_FOUND(-4000, "현재 진행중인 숨은캐스퍼찾기 게임이 없습니다", HttpStatus.NOT_FOUND),
INVALID_FINDING_GAME_ANSWER(-4000, "숨은캐스퍼찾기 게임의 정답이 유효하지 않습니다", HttpStatus.INTERNAL_SERVER_ERROR),
S3_IMAGE_UPLOAD_FAIL(-4001, "S3 이미지 업로드에 실패하였습니다.", HttpStatus.UNPROCESSABLE_ENTITY),
S3_INVALID_DIRECTORY_NAME(-4001, "S3 버킷 디렉토리명이 잘못되었습니다.", HttpStatus.BAD_REQUEST),

Expand Down
Loading