Skip to content

Commit

Permalink
Merge pull request #118 from modu-menu/develop
Browse files Browse the repository at this point in the history
main에 develop merge
  • Loading branch information
eelseungmin authored Jun 10, 2024
2 parents e88be87 + f9f1aee commit 5edafbc
Show file tree
Hide file tree
Showing 13 changed files with 825 additions and 4 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);
}
2 changes: 0 additions & 2 deletions src/main/java/modu/menu/core/exception/Exception400.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import lombok.Getter;
import modu.menu.core.response.ApiFailResponse;
import modu.menu.core.response.ApiSuccessResponse;
import modu.menu.core.response.ErrorData;
import org.springframework.http.HttpStatus;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ResponseEntity<ApiFailResponse> handleConstraintViolationException(Constr

@ExceptionHandler(Exception400.class)
public ResponseEntity<ApiFailResponse> badRequest(Exception400 e) {
log.warn("Exception: 400, " + e.getMessage());
log.warn("Exception: 400, " + e.getValue());
return ResponseEntity
.status(e.status())
.body(e.body());
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/modu/menu/core/response/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ public enum ErrorMessage {

// Vote
NOT_EXIST_VOTE("해당 ID와 일치하는 투표가 존재하지 않습니다."),
CANT_INVITE_TO_END_VOTE("종료된 투표에는 초대할 수 없습니다.");
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("주최자만 투표를 종료할 수 있습니다.");

// domainEx1

Expand Down
58 changes: 58 additions & 0 deletions src/main/java/modu/menu/vote/api/VoteController.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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.VoteResultResponse;
import modu.menu.vote.service.VoteService;
Expand All @@ -29,6 +31,25 @@ public class VoteController {

private final VoteService voteService;

@Operation(summary = "투표 생성", description = "투표를 생성합니다. 투표를 생성한 회원이 주최자가 됩니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "투표 생성이 성공한 경우"),
@ApiResponse(responseCode = "400", description = "RequestBody가 형식에 맞지 않을 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "401", description = "토큰 인증이 실패한 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@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")
public ResponseEntity<ApiSuccessResponse> saveVote(
@Valid @RequestBody SaveVoteRequest saveVoteRequest
) {
voteService.saveVote(saveVoteRequest);

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

@Operation(summary = "투표 초대", description = "투표에 회원을 초대합니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
Expand All @@ -54,6 +75,43 @@ public ResponseEntity<ApiSuccessResponse> invite(
.body(new ApiSuccessResponse<>());
}

// 투표 종료
@Operation(summary = "투표 종료", description = "투표를 종료합니다. 투표 주최자만 가능합니다.")
@SecurityRequirement(name = "Authorization")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "투표 종료가 성공한 경우"),
@ApiResponse(responseCode = "400", description = "PathVariable이 형식에 맞지 않거나 투표가 이미 종료된 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "401", description = "토큰 인증이 실패한 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "403", description = "투표에 권한이 없는 회원인 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "404", description = "조회하려는 투표 자체가 존재하지 않는 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class))),
@ApiResponse(responseCode = "500", description = "그 외 서버에서 처리하지 못한 에러가 발생했을 경우", content = @Content(schema = @Schema(implementation = ApiFailResponse.class)))
})
@PatchMapping("/api/vote/{voteId}/status")
public ResponseEntity<ApiSuccessResponse> finishVote(
@Positive(message = "voteId는 양수여야 합니다.") @PathVariable("voteId") Long voteId
) {
voteService.finishVote(voteId);

return ResponseEntity.ok()
.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
23 changes: 23 additions & 0 deletions src/main/java/modu/menu/vote/api/request/SaveVoteRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package modu.menu.vote.api.request;

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

import java.util.List;

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

@Schema(description = "투표에 포함시킬 음식점 ID 목록")
@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;
}
4 changes: 4 additions & 0 deletions src/main/java/modu/menu/vote/domain/Vote.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@ public void removeReview(Review review) {
review.syncVote(null);
reviews.remove(review);
}

public void updateVoteStatus(VoteStatus voteStatus) {
this.voteStatus = voteStatus;
}
}
109 changes: 109 additions & 0 deletions src/main/java/modu/menu/vote/service/VoteService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package modu.menu.vote.service;

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;
import modu.menu.core.exception.Exception404;
import modu.menu.core.response.ErrorMessage;
import modu.menu.core.util.DistanceCalculator;
Expand All @@ -14,6 +18,8 @@
import modu.menu.user.domain.User;
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 @@ -45,6 +51,35 @@ public class VoteService {
private final FoodRepository foodRepository;
private final UserRepository userRepository;
private final ParticipantRepository participantRepository;
private final HttpServletRequest request;

// 투표 생성
@Transactional
public void saveVote(SaveVoteRequest saveVoteRequest) {

List<Place> places = placeRepository.findAllById(saveVoteRequest.getPlaceIds());

if (places.isEmpty() || places.size() != saveVoteRequest.getPlaceIds().size()) {
throw new Exception404(ErrorMessage.NOT_EXIST_PLACE);
}

Vote vote = voteRepository.save(Vote.builder()
.voteStatus(VoteStatus.ACTIVE)
.build());
places.forEach(place -> {
VoteItem voteItem = voteItemRepository.save(VoteItem.builder()
.vote(vote)
.place(place)
.build());

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

// 투표 초대
@Transactional
Expand Down Expand Up @@ -74,6 +109,80 @@ public void invite(Long voteId, Long userId) {
}
}

// 투표 종료
@Transactional
public void finishVote(Long voteId) {

Vote vote = voteRepository.findById(voteId).orElseThrow(
() -> new Exception404(ErrorMessage.NOT_EXIST_VOTE)
);
if (vote.getVoteStatus().equals(VoteStatus.END)) {
throw new Exception400(String.valueOf(voteId), ErrorMessage.CANT_FINISH_ALREADY_END_VOTE.getValue());
}

Long userId = (Long) request.getAttribute("userId");
Participant participant = participantRepository.findByUserIdAndVoteId(userId, voteId).orElseThrow(
() -> new Exception403(ErrorMessage.NOT_ALLOWED_USER)
);
if (participant.getVoteRole().equals(VoteRole.PARTICIPANT)) {
throw new Exception403(ErrorMessage.CANT_FINISH_BY_PARTICIPANT);
}

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);
}
Loading

0 comments on commit 5edafbc

Please sign in to comment.