diff --git a/backend/src/main/java/ddangkong/config/ClockConfig.java b/backend/src/main/java/ddangkong/config/ClockConfig.java new file mode 100644 index 000000000..d4fa7b9f4 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/ClockConfig.java @@ -0,0 +1,15 @@ +package ddangkong.config; + +import java.time.Clock; +import java.time.ZoneId; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ClockConfig { + + @Bean + public Clock clock() { + return Clock.system(ZoneId.of("Asia/Seoul")); + } +} diff --git a/backend/src/main/java/ddangkong/domain/balance/room/RoomContent.java b/backend/src/main/java/ddangkong/domain/balance/room/RoomContent.java index 22ab7f889..78f3d712f 100644 --- a/backend/src/main/java/ddangkong/domain/balance/room/RoomContent.java +++ b/backend/src/main/java/ddangkong/domain/balance/room/RoomContent.java @@ -3,6 +3,7 @@ import ddangkong.domain.BaseEntity; import ddangkong.domain.balance.content.BalanceContent; import ddangkong.domain.balance.content.Category; +import ddangkong.exception.BadRequestException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -11,6 +12,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -35,6 +38,19 @@ public class RoomContent extends BaseEntity { @Column(nullable = false) private int round; + private LocalDateTime roundEndedAt; + + @Column(nullable = false) + private boolean isUsed; + + public boolean isRoundOver(LocalDateTime currentTime) { + return currentTime.isAfter(getRoundEndedAt()); + } + + public boolean isNotSameContentId(Long contentId) { + return !Objects.equals(getContentId(), contentId); + } + public Long getContentId() { return balanceContent.getId(); } @@ -50,4 +66,11 @@ public String getContentName() { public int getTotalRound() { return room.getTotalRound(); } + + public LocalDateTime getRoundEndedAt() { + if (roundEndedAt == null) { + throw new BadRequestException("라운드 종료 시간이 설정되지 않습니다."); + } + return roundEndedAt; + } } diff --git a/backend/src/main/java/ddangkong/service/balance/vote/BalanceVoteService.java b/backend/src/main/java/ddangkong/service/balance/vote/BalanceVoteService.java index 3650707ab..dbef7bb64 100644 --- a/backend/src/main/java/ddangkong/service/balance/vote/BalanceVoteService.java +++ b/backend/src/main/java/ddangkong/service/balance/vote/BalanceVoteService.java @@ -19,6 +19,8 @@ import ddangkong.domain.member.Member; import ddangkong.domain.member.MemberRepository; import ddangkong.exception.BadRequestException; +import java.time.Clock; +import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -41,8 +43,11 @@ public class BalanceVoteService { private final RoomRepository roomRepository; + private final Clock clock; + @Transactional public BalanceVoteResponse createBalanceVote(BalanceVoteRequest request, Long roomId, Long contentId) { + validateRoundEnded(roomId, contentId); BalanceOption balanceOption = findValidOption(request.optionId(), contentId); Member member = findValidMember(request.memberId(), roomId); @@ -51,6 +56,19 @@ public BalanceVoteResponse createBalanceVote(BalanceVoteRequest request, Long ro return new BalanceVoteResponse(savedBalanceVote); } + private void validateRoundEnded(Long roomId, Long contentId) { + RoomContent roomContent = findValidRoomContent(roomId); + if (roomContent.isNotSameContentId(contentId) || roomContent.isRoundOver(LocalDateTime.now(clock))) { + throw new BadRequestException("유효하지 않은 라운드에는 투표할 수 없습니다."); + } + } + + private RoomContent findValidRoomContent(Long roomId) { + Room room = roomRepository.getById(roomId); + return roomContentRepository.findByRoomAndRound(room, room.getCurrentRound()) + .orElseThrow(() -> new BadRequestException("해당 방의 현재 진행중인 질문이 존재하지 않습니다.")); + } + private BalanceOption findValidOption(Long optionId, Long contentId) { return balanceOptionRepository.findByIdAndBalanceContentId(optionId, contentId) .orElseThrow(() -> new BadRequestException("해당 질문의 선택지가 존재하지 않습니다.")); diff --git a/backend/src/test/java/ddangkong/controller/balance/vote/BalanceVoteControllerTest.java b/backend/src/test/java/ddangkong/controller/balance/vote/BalanceVoteControllerTest.java index c342465aa..5a69fbdc0 100644 --- a/backend/src/test/java/ddangkong/controller/balance/vote/BalanceVoteControllerTest.java +++ b/backend/src/test/java/ddangkong/controller/balance/vote/BalanceVoteControllerTest.java @@ -5,11 +5,14 @@ import ddangkong.controller.BaseControllerTest; import ddangkong.controller.balance.vote.dto.BalanceVoteRequest; import ddangkong.controller.balance.vote.dto.BalanceVoteResponse; +import ddangkong.support.config.TestClockConfig; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +@Import(TestClockConfig.class) class BalanceVoteControllerTest extends BaseControllerTest { @Nested @@ -21,7 +24,7 @@ class 투표_생성 { private static final BalanceVoteResponse EXPECTED_RESPONSE = new BalanceVoteResponse(1L); @Test - void 현재_방의_질문을_조회할_수_있다() { + void 현재_방에서_투표할_수_있다() { // given & when BalanceVoteResponse actual = RestAssured.given().log().all() .body(NORMAL_REQUEST).contentType(ContentType.JSON) diff --git a/backend/src/test/java/ddangkong/service/balance/vote/BalanceVoteServiceTest.java b/backend/src/test/java/ddangkong/service/balance/vote/BalanceVoteServiceTest.java index af1f1d13c..06c60e96c 100644 --- a/backend/src/test/java/ddangkong/service/balance/vote/BalanceVoteServiceTest.java +++ b/backend/src/test/java/ddangkong/service/balance/vote/BalanceVoteServiceTest.java @@ -12,11 +12,14 @@ import ddangkong.controller.balance.vote.dto.BalanceVoteResultResponse; import ddangkong.exception.BadRequestException; import ddangkong.service.BaseServiceTest; +import ddangkong.support.config.TestClockConfig; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +@Import(TestClockConfig.class) class BalanceVoteServiceTest extends BaseServiceTest { @Autowired @@ -45,8 +48,8 @@ class 투표_생성 { @Test void 질문에_해당하는_선택지가_아닌_경우_예외를_던진다() { // given - Long optionId = 1L; - Long contentId = 2L; + Long optionId = 3L; + Long contentId = 1L; Long memberId = 1L; Long roomId = 1L; @@ -63,7 +66,7 @@ class 투표_생성 { Long optionId = 1L; Long contentId = 1L; Long memberId = 1L; - Long roomId = 2L; + Long roomId = 3L; // when & then assertThatThrownBy(() -> balanceVoteService.createBalanceVote( @@ -71,6 +74,36 @@ class 투표_생성 { .isInstanceOf(BadRequestException.class) .hasMessage("해당 방의 멤버가 존재하지 않습니다."); } + + @Test + void 투표_시간이_지난_이후_투표_시_예외를_던진다() { + // given + Long optionId = 3L; + Long contentId = 2L; + Long memberId = 1L; + Long roomId = 1L; + + // when & then + assertThatThrownBy(() -> balanceVoteService.createBalanceVote( + new BalanceVoteRequest(memberId, optionId), roomId, contentId)) + .isInstanceOf(BadRequestException.class) + .hasMessage("유효하지 않은 라운드에는 투표할 수 없습니다."); + } + + @Test + void 아직_진행하지_않은_컨텐츠에_투표_시_예외를_던진다() { + // given + Long optionId = 5L; + Long contentId = 3L; + Long memberId = 1L; + Long roomId = 1L; + + // when & then + assertThatThrownBy(() -> balanceVoteService.createBalanceVote( + new BalanceVoteRequest(memberId, optionId), roomId, contentId)) + .isInstanceOf(BadRequestException.class) + .hasMessage("유효하지 않은 라운드에는 투표할 수 없습니다."); + } } @Nested diff --git a/backend/src/test/java/ddangkong/support/config/TestClockConfig.java b/backend/src/test/java/ddangkong/support/config/TestClockConfig.java new file mode 100644 index 000000000..ec1cb1dd3 --- /dev/null +++ b/backend/src/test/java/ddangkong/support/config/TestClockConfig.java @@ -0,0 +1,26 @@ +package ddangkong.support.config; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class TestClockConfig { + + @Primary + @Bean + public Clock testClock() { + String dateTimeString = "2024-07-18 20:00:02.000"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + LocalDateTime customDateTime = LocalDateTime.parse(dateTimeString, formatter); + ZonedDateTime customZonedDateTime = customDateTime.atZone(ZoneId.of("Asia/Seoul")); + Instant customInstant = customZonedDateTime.toInstant(); + return Clock.fixed(customInstant, ZoneId.of("Asia/Seoul")); + } +} diff --git a/backend/src/test/resources/init-test.sql b/backend/src/test/resources/init-test.sql index 39b7dfe26..92b6caed0 100644 --- a/backend/src/test/resources/init-test.sql +++ b/backend/src/test/resources/init-test.sql @@ -16,11 +16,11 @@ VALUES ('EXAMPLE', '민초 vs 반민초'), ('EXAMPLE', '월 200 백수 vs 월 500 직장인'), ('EXAMPLE', '다음 중 여행가고 싶은 곳은?'); -INSERT INTO room_content (room_id, balance_content_id, round, created_at) -VALUES (1, 2, 1, '2024-07-18 19:50:00.000'), - (1, 1, 2, '2024-07-18 20:00:00.000'), - (1, 3, 3, '2024-07-18 20:00:00.000'), - (3, 1, 1, '2024-07-18 19:51:00.000'); +INSERT INTO room_content (room_id, balance_content_id, round, created_at, round_ended_at, is_used) +VALUES (1, 2, 1, '2024-07-18 19:50:00.000', '2024-07-18 19:50:32.000', false), + (1, 1, 2, '2024-07-18 19:50:00.000', '2024-07-18 20:00:32.000', false), + (1, 3, 3, '2024-07-18 19:50:00.000', null, false), + (3, 1, 1, '2024-07-18 20:00:00.000', '2024-07-18 20:00:32.000', false); INSERT INTO balance_option (name, balance_content_id) VALUES ('민초', 1),