Skip to content

Commit

Permalink
Merge pull request #117 from modu-menu/feat/add-save-choice
Browse files Browse the repository at this point in the history
투표 생성 API 수정, 투표, 재투표 API 구현
  • Loading branch information
eelseungmin authored Jun 10, 2024
2 parents 65b63bd + 1a51934 commit f9f1aee
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 5 deletions.
4 changes: 4 additions & 0 deletions src/main/java/modu/menu/choice/domain/Choice.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class Choice extends BaseTime {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

public void updateVoteItem(VoteItem voteItem) {
this.voteItem = voteItem;
}
}
10 changes: 10 additions & 0 deletions src/main/java/modu/menu/choice/repository/ChoiceRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface ChoiceRepository extends JpaRepository<Choice, Long> {

@Query("""
Expand All @@ -13,4 +16,11 @@ select count(*)
where c.voteItem.id = :voteItemId
""")
int countByVoteItemId(@Param("voteItemId") Long id);

@Query("""
select c
from Choice c
where c.user.id = :userId and c.voteItem.id in :voteItemIds
""")
Optional<Choice> findByUserIdAndVoteItemIds(@Param("userId") Long userId, @Param("voteItemIds") List<Long> voteItemIds);
}
1 change: 1 addition & 0 deletions src/main/java/modu/menu/core/response/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum ErrorMessage {
NOT_EXIST_VOTE("해당 ID와 일치하는 투표가 존재하지 않습니다."),
CANT_INVITE_TO_END_VOTE("종료된 투표에는 초대할 수 없습니다."),
NOT_EXIST_PLACE("해당 ID와 일치하는 음식점이 존재하지 않습니다."),
NOT_EXIST_PLACE_IN_VOTE("해당 ID와 일치하는 음식점이 투표에 존재하지 않습니다."),
CANT_FINISH_ALREADY_END_VOTE("이미 종료된 투표입니다."),
NOT_ALLOWED_USER("투표에 초대된 회원이 아닙니다."),
CANT_FINISH_BY_PARTICIPANT("주최자만 투표를 종료할 수 있습니다.");
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/modu/menu/vote/api/VoteController.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import modu.menu.core.response.ApiSuccessResponse;
import modu.menu.core.response.ErrorMessage;
import modu.menu.vote.api.request.SaveVoteRequest;
import modu.menu.vote.api.request.VoteRequest;
import modu.menu.vote.api.request.VoteResultRequest;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.service.VoteService;
Expand All @@ -30,7 +31,7 @@ public class VoteController {

private final VoteService voteService;

@Operation(summary = "투표 생성", description = "투표를 생성합니다.")
@Operation(summary = "투표 생성", description = "투표를 생성합니다. 투표를 생성한 회원이 주최자가 됩니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "투표 생성이 성공한 경우"),
Expand Down Expand Up @@ -75,7 +76,7 @@ public ResponseEntity<ApiSuccessResponse> invite(
}

// 투표 종료
@Operation(summary = "투표 종료", description = "투표를 종료합니다.")
@Operation(summary = "투표 종료", description = "투표를 종료합니다. 투표 주최자만 가능합니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "투표 종료가 성공한 경우"),
Expand All @@ -95,6 +96,22 @@ public ResponseEntity<ApiSuccessResponse> finishVote(
.body(new ApiSuccessResponse<>());
}

@Operation(summary = "투표, 재투표", description = """
투표하거나 재투표합니다. 초대받은 사람만 투표할 수 있습니다.
placeId가 null이라면 기존 투표 기록을 삭제합니다.
""")
@SecurityRequirement(name = "Authorization")
@PostMapping("/api/vote/{voteId}")
public ResponseEntity<ApiSuccessResponse> vote(
@Positive(message = "voteId는 양수여야 합니다.") @PathVariable("voteId") Long voteId,
@Valid @RequestBody VoteRequest voteRequest
) {
voteService.vote(voteId, voteRequest);

return ResponseEntity.ok()
.body(new ApiSuccessResponse<>());
}

/**
* 회원의 위도와 경도를 외부에 노출해선 안 된다고 판단하여
* HTTPS로 통신하는 점을 이용해 RequestBody에 위치 데이터를 담아서 요청하도록 설계
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
public class SaveVoteRequest {

@Schema(description = "투표에 포함시킬 음식점 ID 목록")
@Size(min = 2, message = "투표 항목으로 최소 2개의 음식점이 포함되어야 합니다.")
@Size(min = 2, max = 3, message = "투표 항목으로 최소 2개, 최대 3개의 음식점이 포함되어야 합니다.")
private List<@Positive(message = "ID는 양수여야 합니다.") Long> placeIds;
}
20 changes: 20 additions & 0 deletions src/main/java/modu/menu/vote/api/request/VoteRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package modu.menu.vote.api.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Schema(description = "투표 요청 DTO")
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class VoteRequest {

@Schema(description = "투표하려는 음식점 ID")
@Positive(message = "placeId는 양수여야 합니다.")
private Long placeId;
}
59 changes: 59 additions & 0 deletions src/main/java/modu/menu/vote/service/VoteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import modu.menu.choice.domain.Choice;
import modu.menu.choice.repository.ChoiceRepository;
import modu.menu.core.exception.Exception400;
import modu.menu.core.exception.Exception403;
Expand All @@ -18,6 +19,7 @@
import modu.menu.user.repository.UserRepository;
import modu.menu.vibe.repository.VibeRepository;
import modu.menu.vote.api.request.SaveVoteRequest;
import modu.menu.vote.api.request.VoteRequest;
import modu.menu.vote.api.request.VoteResultRequest;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.domain.Vote;
Expand Down Expand Up @@ -72,6 +74,11 @@ public void saveVote(SaveVoteRequest saveVoteRequest) {

vote.addVoteItem(voteItem);
});
participantRepository.save(Participant.builder()
.vote(vote)
.user(userRepository.findById((Long) request.getAttribute("userId")).get())
.voteRole(VoteRole.ORGANIZER)
.build());
}

// 투표 초대
Expand Down Expand Up @@ -124,6 +131,58 @@ public void finishVote(Long voteId) {
vote.updateVoteStatus(VoteStatus.END);
}

// 투표, 재투표
@Transactional
public void vote(Long voteId, VoteRequest voteRequest) {

boolean isExsistsVote = voteRepository.existsById(voteId);
if (!isExsistsVote) {
throw new Exception404(ErrorMessage.NOT_EXIST_VOTE);
}

Long userId = (Long) request.getAttribute("userId");
participantRepository.findByUserIdAndVoteId(userId, voteId).orElseThrow(
() -> new Exception404(ErrorMessage.NOT_ALLOWED_USER)
);

List<VoteItem> voteItems = voteItemRepository.findByVoteId(voteId);
Optional<Choice> choiceOptional = choiceRepository.findByUserIdAndVoteItemIds(userId, voteItems.stream()
.map(VoteItem::getId)
.toList());

// case 1. 기존 투표 기록이 존재할 경우(재투표)
if (choiceOptional.isPresent()) {
Choice choice = choiceOptional.get();

// case 1-1. 기존 투표 기록 삭제
if (voteRequest.getPlaceId() == null) {
choiceRepository.delete(choice);
return;
}

// case 1-2. 기존 투표 기록 수정
VoteItem satisfiedVoteItem = findSatisfiedVoteItem(voteRequest, voteItems);

choice.updateVoteItem(satisfiedVoteItem);
return;
}

// case 2. 기존 투표 기록이 존재하지 않을 경우(투표)
choiceRepository.save(Choice.builder()
.voteItem(findSatisfiedVoteItem(voteRequest, voteItems))
.user(userRepository.findById(userId).get())
.build());
}

// VoteItem 목록에서 요청 DTO 내의 placeId와 일치하는 항목을 찾는 메서드
private VoteItem findSatisfiedVoteItem(VoteRequest voteRequest, List<VoteItem> voteItems) {
return voteItems.stream()
.filter(voteItem -> voteItem.getPlace().getId().equals(voteRequest.getPlaceId()))
.findFirst()
.orElseThrow(() -> new Exception400(String.valueOf(voteRequest.getPlaceId()), ErrorMessage.NOT_EXIST_PLACE_IN_VOTE.getValue()));
}

// TODO 투표 조회 API에서 해당 유저의 투표 여부를 변수로 같이 보내줘야 한다.
// 투표 결과 조회
public VoteResultResponse getResult(Long voteId, VoteResultRequest voteResultRequest) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

import modu.menu.voteItem.domain.VoteItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface VoteItemRepository extends JpaRepository<VoteItem, Long> {

@Query("""
select vi
from VoteItem vi
where vi.vote.id = :voteId
""")
List<VoteItem> findByVoteId(@Param("voteId") Long voteId);
}
96 changes: 94 additions & 2 deletions src/test/java/modu/menu/vote/api/VoteControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import modu.menu.food.domain.FoodType;
import modu.menu.vibe.domain.VibeType;
import modu.menu.vote.api.request.SaveVoteRequest;
import modu.menu.vote.api.request.VoteRequest;
import modu.menu.vote.api.request.VoteResultRequest;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.service.dto.VoteResultServiceResponse;
Expand Down Expand Up @@ -62,7 +63,7 @@ void saveVoteWithEmptyIdList() throws Exception {
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.message").value("투표 항목으로 최소 2개의 음식점이 포함되어야 합니다."));
.andExpect(jsonPath("$.message").value("투표 항목으로 최소 2개, 최대 3개의 음식점이 포함되어야 합니다."));
}

@DisplayName("투표를 생성할 때 ID가 하나만 있으면 실패한다.")
Expand All @@ -82,7 +83,27 @@ void saveVoteWithOneId() throws Exception {
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.message").value("투표 항목으로 최소 2개의 음식점이 포함되어야 합니다."));
.andExpect(jsonPath("$.message").value("투표 항목으로 최소 2개, 최대 3개의 음식점이 포함되어야 합니다."));
}

@DisplayName("투표를 생성할 때 ID가 3개를 초과하면 실패한다.")
@Test
void saveVoteWithExceedMax() throws Exception {
// given
SaveVoteRequest saveVoteRequest = SaveVoteRequest.builder()
.placeIds(List.of(1L, 2L, 3L, 4L))
.build();

// when
doNothing().when(voteService).saveVote(saveVoteRequest);

// then
mockMvc.perform(post("/api/vote")
.content(objectMapper.writeValueAsBytes(saveVoteRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.message").value("투표 항목으로 최소 2개, 최대 3개의 음식점이 포함되어야 합니다."));
}

@DisplayName("투표를 생성할 때 ID 목록 중에 0이 포함되어 있으면 실패한다.")
Expand Down Expand Up @@ -222,6 +243,77 @@ void finishVoteWithZeroVoteId() throws Exception {
.andExpect(jsonPath("$.message").value("voteId는 양수여야 합니다."));
}

@DisplayName("투표하면 성공한다.")
@Test
void vote() throws Exception {
// given
Long voteId = 1L;
Long placeId = 2L;
VoteRequest voteRequest = VoteRequest.builder()
.placeId(placeId)
.build();

// when
doNothing().when(voteService).vote(anyLong(), any(VoteRequest.class));

// then
mockMvc.perform(post("/api/vote/{voteId}", voteId)
.content(objectMapper.writeValueAsString(voteRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(200))
.andExpect(jsonPath("$.reason").value("OK"))
.andExpect(jsonPath("$.data").isEmpty());
}

@DisplayName("투표할 때 voteId가 0이면 실패한다.")
@Test
void voteWithZeroVoteId() throws Exception {
// given
Long voteId = 0L;
Long placeId = 2L;
VoteRequest voteRequest = VoteRequest.builder()
.placeId(placeId)
.build();

// when
doNothing().when(voteService).vote(anyLong(), any(VoteRequest.class));

// then
mockMvc.perform(post("/api/vote/{voteId}", voteId)
.content(objectMapper.writeValueAsString(voteRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.cause").exists())
.andExpect(jsonPath("$.message").value("voteId는 양수여야 합니다."));
}

@DisplayName("투표할 때 placeId가 0이면 실패한다.")
@Test
void voteWithZeroPlaceId() throws Exception {
// given
Long voteId = 1L;
Long placeId = 0L;
VoteRequest voteRequest = VoteRequest.builder()
.placeId(placeId)
.build();

// when
doNothing().when(voteService).vote(anyLong(), any(VoteRequest.class));

// then
mockMvc.perform(post("/api/vote/{voteId}", voteId)
.content(objectMapper.writeValueAsString(voteRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.cause").exists())
.andExpect(jsonPath("$.message").value("placeId는 양수여야 합니다."));
}

@DisplayName("투표 결과를 조회하면 성공한다.")
@Test
void getResult() throws Exception {
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/modu/menu/vote/service/VoteServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class VoteServiceTest extends IntegrationTestSupporter {
@Test
void saveVote() {
// given
User user1 = createUser("[email protected]");
User user2 = createUser("[email protected]");
User user3 = createUser("[email protected]");
Place place1 = createPlace("타코벨");
Place place2 = createPlace("이자카야모리");
Place place3 = createPlace("서가앤쿡 노원역점");
Expand All @@ -89,9 +92,11 @@ void saveVote() {
place2.addPlaceVibe(placeVibe2);
PlaceVibe placeVibe3 = createPlaceVibe(place3, vibe3);
place3.addPlaceVibe(placeVibe3);
userRepository.saveAll(List.of(user1, user2, user3));
placeRepository.saveAll(List.of(place1, place2, place3));
vibeRepository.saveAll(List.of(vibe1, vibe2, vibe3));
placeVibeRepository.saveAll(List.of(placeVibe1, placeVibe2, placeVibe3));

Food food1 = createFood(FoodType.LATIN);
Food food2 = createFood(FoodType.MEAT);
PlaceFood placeFood1 = createPlaceFood(place1, food1);
Expand All @@ -107,12 +112,15 @@ void saveVote() {
.placeIds(List.of(1L, 2L, 3L))
.build();

request.setAttribute("userId", user1.getId());

// when
voteService.saveVote(saveVoteRequest);


// then
assertThat(voteRepository.findAll()).isNotEmpty();
assertThat(participantRepository.findByUserId(user1.getId()).get().getVoteRole()).isEqualTo(VoteRole.ORGANIZER);
}

@DisplayName("투표를 생성할 때 ID 목록에 있는 음식점들이 존재하지 않으면 실패한다.")
Expand Down

0 comments on commit f9f1aee

Please sign in to comment.