Skip to content

Commit

Permalink
Merge pull request #122 from modu-menu/develop
Browse files Browse the repository at this point in the history
main에 develop merge
  • Loading branch information
eelseungmin authored Jun 15, 2024
2 parents 5015e60 + c3514b6 commit 53d0419
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 139 deletions.
20 changes: 8 additions & 12 deletions src/main/java/modu/menu/vote/api/VoteController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.RequiredArgsConstructor;
import modu.menu.core.exception.Exception401;
import modu.menu.core.response.ApiFailResponse;
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.TurnoutResponse;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.api.response.VoteResponse;
import modu.menu.vote.service.VoteService;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -76,7 +76,6 @@ public ResponseEntity<ApiSuccessResponse> invite(
.body(new ApiSuccessResponse<>());
}

// 투표 종료
@Operation(summary = "투표 종료", description = "투표를 종료합니다. 투표 주최자만 가능합니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
Expand Down Expand Up @@ -127,11 +126,7 @@ public ResponseEntity<ApiSuccessResponse<TurnoutResponse>> getTurnout(
.body(new ApiSuccessResponse<>(response));
}

/**
* 회원의 위도와 경도를 외부에 노출해선 안 된다고 판단하여
* HTTPS로 통신하는 점을 이용해 RequestBody에 위치 데이터를 담아서 요청하도록 설계
*/
@Operation(summary = "투표 결과 조회", description = "투표 결과를 조회합니다. 회원이라면 모두 조회 가능합니다.")
@Operation(summary = "투표 현황, 결과 조회", description = "투표 현황 또는 결과를 조회합니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회가 성공한 경우"),
Expand All @@ -140,13 +135,14 @@ public ResponseEntity<ApiSuccessResponse<TurnoutResponse>> getTurnout(
@ApiResponse(responseCode = "404", description = "조회하려는 투표 자체가 존재하지 않는 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "500", description = "그 외 서버에서 처리하지 못한 에러가 발생했을 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class)))
})
@PostMapping("/api/vote/{voteId}/result")
public ResponseEntity<ApiSuccessResponse<VoteResultResponse>> getResult(
@GetMapping("/api/vote/{voteId}")
public ResponseEntity<ApiSuccessResponse<VoteResponse>> getVote(
@Positive(message = "voteId는 양수여야 합니다.") @PathVariable("voteId") Long voteId,
@Valid @RequestBody VoteResultRequest voteResultRequest
@PositiveOrZero(message = "위도는 0 또는 양수여야 합니다.") @RequestParam(defaultValue = "37.505098") Double latitude,
@PositiveOrZero(message = "경도는 0 또는 양수여야 합니다.") @RequestParam(defaultValue = "127.032941") Double longitude
) {

VoteResultResponse response = voteService.getResult(voteId, voteResultRequest);
VoteResponse response = voteService.getVote(voteId, latitude, longitude);

return ResponseEntity.ok()
.body(new ApiSuccessResponse<>(response));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import java.util.List;

@Schema(description = "투표 결과 조회 응답 DTO")
@Schema(description = "투표 현황, 결과 조회 응답 DTO")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class VoteResultResponse {
public class VoteResponse {

@Schema(description = "투표 결과 목록")
private List<VoteResultServiceResponse> results;
@Schema(description = "투표에 포함된 음식점 목록")
private List<VoteResultServiceResponse> vote;
}
29 changes: 17 additions & 12 deletions src/main/java/modu/menu/vote/service/VoteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
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.TurnoutResponse;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.api.response.VoteResponse;
import modu.menu.vote.domain.Vote;
import modu.menu.vote.domain.VoteStatus;
import modu.menu.vote.repository.VoteRepository;
Expand Down Expand Up @@ -222,9 +221,8 @@ public TurnoutResponse getTurnout(Long voteId) {
);
}

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

// 투표 존재 여부를 확인한다. fetch join을 이용해 선택지도 함께 가져온다.
Vote vote = voteRepository.findVoteResultById(voteId).orElseThrow(
Expand All @@ -243,14 +241,17 @@ public VoteResultResponse getResult(Long voteId, VoteResultRequest voteResultReq
.mapToInt(v -> voteCountMap.getOrDefault(v.getId(), 0))
.sum();

return new VoteResultResponse(voteItems.stream()
List<Choice> choices = choiceRepository.findByVoteId(voteId);
Long userId = (Long) request.getAttribute("userId");

return new VoteResponse(voteItems.stream()
.map(voteItem -> {
Place place = voteItem.getPlace();
double distance = DistanceCalculator.calculate(
place.getLatitude(),
place.getLongitude(),
voteResultRequest.getLatitude(),
voteResultRequest.getLongitude()
latitude,
longitude
);

int voteCount = voteCountMap.getOrDefault(voteItem.getId(), 0);
Expand All @@ -266,15 +267,19 @@ public VoteResultResponse getResult(Long voteId, VoteResultRequest voteResultReq
.address(place.getAddress())
.distance(distance >= 1000.0 ? String.format("%.1f", distance / 1000.0) + "km" : Math.round(distance) + "m")
.img(place.getImageUrl())
.voteRating(Math.round(voteCount * 100.0 / voterCount) + "%")
.turnout(vote.getVoteStatus() == VoteStatus.END ? Math.round(voteCount * 100.0 / voterCount) + "%" : null) // 종료된 투표일 경우에만 수치를 표시한다.
.isVote(
choices.stream()
.anyMatch(choice -> choice.getVoteItem().getId().equals(voteItem.getId()) && choice.getUser().getId().equals(userId))
)
.build();
})
.sorted((voteResultServiceResponse1, voteResultServiceResponse2) -> {
if (parseInt(voteResultServiceResponse2.getVoteRating().replace("%", "")) == parseInt(voteResultServiceResponse1.getVoteRating().replace("%", ""))) {
if (parseInt(voteResultServiceResponse2.getTurnout().replace("%", "")) == parseInt(voteResultServiceResponse1.getTurnout().replace("%", ""))) {
return voteResultServiceResponse1.getName().compareTo(voteResultServiceResponse2.getName());
}
return parseInt(voteResultServiceResponse2.getVoteRating().replace("%", ""))
- parseInt(voteResultServiceResponse1.getVoteRating().replace("%", ""));
return parseInt(voteResultServiceResponse2.getTurnout().replace("%", ""))
- parseInt(voteResultServiceResponse1.getTurnout().replace("%", ""));
})
.toList()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class VoteResultServiceResponse {
@Schema(description = "음식점 관련 이미지 주소")
private String img;

@Schema(description = "해당 음식점 투표율", example = "70%")
private String voteRating;
@Schema(description = "해당 음식점 득표율(투표가 종료된 경우에만 표시, 그 외엔 null)", examples = {"70%", "null"})
private String turnout;

@Schema(description = "조회를 요청한 회원이 해당 장소에 투표했는지 여부", examples = "true")
private Boolean isVote;
}
116 changes: 26 additions & 90 deletions src/test/java/modu/menu/vote/api/VoteControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
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.TurnoutResponse;
import modu.menu.vote.api.response.VoteResultResponse;
import modu.menu.vote.api.response.VoteResponse;
import modu.menu.vote.service.dto.IsVoteServiceResponse;
import modu.menu.vote.service.dto.VoteResultServiceResponse;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -16,8 +15,7 @@

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
Expand Down Expand Up @@ -386,141 +384,79 @@ void getTurnoutWithZeroVoteId() throws Exception {
void getResult() throws Exception {
// given
Long voteId = 1L;
VoteResultRequest voteResultRequest = VoteResultRequest.builder()
.latitude(126.977966)
.longitude(37.566535)
.build();
Double latitude = 37.655038011447;
Double longitude = 127.06694995614;

// when
when(voteService.getResult(anyLong(), any(VoteResultRequest.class)))
.thenReturn(new VoteResultResponse(
when(voteService.getVote(anyLong(), anyDouble(), anyDouble()))
.thenReturn(new VoteResponse(
List.of(createResult("타코벨"),
createResult("매드포갈릭 강남삼성타운점"),
createResult("창고 43 강남점")))
);

// then
mockMvc.perform(post("/api/vote/{voteId}/result", voteId)
.content(objectMapper.writeValueAsString(voteResultRequest))
.contentType(MediaType.APPLICATION_JSON))
mockMvc.perform(get("/api/vote/{voteId}", voteId)
.param("latitude", String.valueOf(latitude))
.param("longitude", String.valueOf(longitude)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(200))
.andExpect(jsonPath("$.reason").value("OK"))
.andExpect(jsonPath("$.data").isMap());
}

@DisplayName("투표 결과를 조회할 때 위도는 필수이다.")
@Test
void getResultWithNoLatitude() throws Exception {
// given
Long voteId = 1L;
VoteResultRequest voteResultRequest = VoteResultRequest.builder()
.longitude(37.566535)
.build();

// when
when(voteService.getResult(anyLong(), any(VoteResultRequest.class)))
.thenReturn(new VoteResultResponse(
List.of(createResult("타코벨"),
createResult("매드포갈릭 강남삼성타운점"),
createResult("창고 43 강남점")))
);

// then
mockMvc.perform(post("/api/vote/{voteId}/result", voteId)
.content(objectMapper.writeValueAsString(voteResultRequest))
.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("위도는 필수입니다."));
}

@DisplayName("투표 결과를 조회할 때 경도는 필수이다.")
@Test
void getResultWithNoLongitude() throws Exception {
// given
Long voteId = 1L;
VoteResultRequest voteResultRequest = VoteResultRequest.builder()
.latitude(126.977966)
.build();

// when
when(voteService.getResult(anyLong(), any(VoteResultRequest.class)))
.thenReturn(new VoteResultResponse(
List.of(createResult("타코벨"),
createResult("매드포갈릭 강남삼성타운점"),
createResult("창고 43 강남점")))
);

// then
mockMvc.perform(post("/api/vote/{voteId}/result", voteId)
.content(objectMapper.writeValueAsString(voteResultRequest))
.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("경도는 필수입니다."));
}

@DisplayName("투표 결과를 조회할 때 위도는 양수이다.")
@Test
void getResultWithZeroLatitude() throws Exception {
// given
Long voteId = 1L;
VoteResultRequest voteResultRequest = VoteResultRequest.builder()
.latitude(0.0)
.longitude(37.566535)
.build();
Double latitude = -1.1;
Double longitude = 127.06694995614;

// when
when(voteService.getResult(anyLong(), any(VoteResultRequest.class)))
.thenReturn(new VoteResultResponse(
when(voteService.getVote(anyLong(), anyDouble(), anyDouble()))
.thenReturn(new VoteResponse(
List.of(createResult("타코벨"),
createResult("매드포갈릭 강남삼성타운점"),
createResult("창고 43 강남점")))
);

// then
mockMvc.perform(post("/api/vote/{voteId}/result", voteId)
.content(objectMapper.writeValueAsString(voteResultRequest))
.contentType(MediaType.APPLICATION_JSON))
mockMvc.perform(get("/api/vote/{voteId}", voteId)
.param("latitude", String.valueOf(latitude))
.param("longitude", String.valueOf(longitude)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.cause").exists())
.andExpect(jsonPath("$.message").value("위도는 양수여야 합니다."));
.andExpect(jsonPath("$.message").value("위도는 0 또는 양수여야 합니다."));
}

@DisplayName("투표 결과를 조회할 때 경도는 양수이다.")
@Test
void getResultWithZeroLongitude() throws Exception {
// given
Long voteId = 1L;
VoteResultRequest voteResultRequest = VoteResultRequest.builder()
.latitude(126.977966)
.longitude(-1.1)
.build();
Double latitude = 37.655038011447;
Double longitude = -1.1;

// when
when(voteService.getResult(anyLong(), any(VoteResultRequest.class)))
.thenReturn(new VoteResultResponse(
when(voteService.getVote(anyLong(), anyDouble(), anyDouble()))
.thenReturn(new VoteResponse(
List.of(createResult("타코벨"),
createResult("매드포갈릭 강남삼성타운점"),
createResult("창고 43 강남점")))
);

// then
mockMvc.perform(post("/api/vote/{voteId}/result", voteId)
.content(objectMapper.writeValueAsString(voteResultRequest))
.contentType(MediaType.APPLICATION_JSON))
mockMvc.perform(get("/api/vote/{voteId}", voteId)
.param("latitude", String.valueOf(latitude))
.param("longitude", String.valueOf(longitude)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.reason").value("Bad Request"))
.andExpect(jsonPath("$.cause").exists())
.andExpect(jsonPath("$.message").value("경도는 양수여야 합니다."));
.andExpect(jsonPath("$.message").value("경도는 0 또는 양수여야 합니다."));
}

private VoteResultServiceResponse createResult(String name) {
Expand All @@ -531,7 +467,7 @@ private VoteResultServiceResponse createResult(String name) {
.address("서울 종로구 삼일대로")
.distance("10m")
.img("이미지 주소")
.voteRating("70%")
.turnout("70%")
.build();
}
}
Loading

0 comments on commit 53d0419

Please sign in to comment.