Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//security

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
Expand All @@ -49,8 +48,6 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.github.cdimascio:dotenv-java:3.0.0'



// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

Expand All @@ -59,8 +56,16 @@ dependencies {
implementation 'mysql:mysql-connector-java:8.0.30'
implementation 'com.h2database:h2:2.2.220'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.22.0'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'


// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}


Expand All @@ -71,4 +76,8 @@ tasks.named('test') {
test {
// 모든 테스트를 스킵하도록 설정 (임시)
enabled = false
}

clean {
delete file('src/main/generated')
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
3 changes: 1 addition & 2 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions src/main/java/com/umc/yeogi_gal_lae/api/vote/domain/Vote.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.umc.yeogi_gal_lae.api.vote.domain;


import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan;
import com.umc.yeogi_gal_lae.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Builder
@Getter @Setter
@NoArgsConstructor
Expand All @@ -19,17 +23,19 @@ public class Vote extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "trip_plan_id", nullable = false)
@JsonIgnore
private TripPlan tripPlan;

// 특정 사용자의 투표 결과 조회 빈번하게 일어날 것으로 예상되기에, VoteRoom 을 통하지 않고 바로 Use 와 매핑
// VoteRoom 은 투표 방 자체를 관리하고, Vote 는 사용자들의 실제 투표 데이터를 관리하므로 역할 분리
// 투표 데이터를 추가하거나 변경할 때 VoteRoom 에 불필요한 데이터가 섞이지 않기 위함

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "vote_room_id", nullable = false)
@JsonIgnore
private VoteRoom voteRoom;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private VoteType type;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@Override
public LocalDateTime getCreatedAt() { return super.getCreatedAt(); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.minidev.json.annotate.JsonIgnore;


@NoArgsConstructor
public class VoteRequest {


Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.umc.yeogi_gal_lae.api.vote.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;


@Builder
@Getter
@NoArgsConstructor
@Builder // 추가
@AllArgsConstructor // 필요 시 추가
public class VoteRoomRequest {

@NotNull
Expand All @@ -17,5 +21,4 @@ public class VoteRoomRequest {

@NotNull
private Long voteRoomId;

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.umc.yeogi_gal_lae.api.vote.repository.VoteRoomRepository;
import com.umc.yeogi_gal_lae.global.error.BusinessException;
import com.umc.yeogi_gal_lae.global.error.ErrorCode;
import jakarta.persistence.EntityNotFoundException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -41,64 +40,52 @@ public class ValidVoteResultService {
public boolean validResult(VoteRoomRequest voteRoomRequest) {

// 투표 완료 여부 확인
if (!checkVoteCompleted(voteRoomRequest)) { throw new BusinessException(ErrorCode.VOTE_NOT_COMPLETED_YET); }
if (!checkVoteCompleted(voteRoomRequest)) { throw new BusinessException(ErrorCode.VOTE_NOT_COMPLETED_YET);}

VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId());
TripPlan tripPlan = voteRoom.getTripPlan();
VoteCounts voteCounts = countVotes(tripPlan.getId());

if (isVoteTimeExpired(voteRoom, tripPlan)) {
voteRoomRepository.delete(voteRoom);
return true; // 재투표
}

// 찬성/반대 투표 집계
List<Vote> votes = voteRepository.findAllVotesByTripPlanId(tripPlan.getId());
long goodVotes = votes.stream().filter(v -> v.getType() == VoteType.GOOD).count();
long badVotes = votes.stream().filter(v -> v.getType() == VoteType.BAD).count();


// 반대표가 더 많을 시, 재투표를 위해 투표 방 삭제
if (goodVotes < badVotes) {
if (voteCounts.goodVotes > voteCounts.badVotes) {
tripPlan.setStatus(Status.COMPLETED);
tripPlanRepository.save(tripPlan);
} else {
voteRoomRepository.delete(voteRoom);

// roomId를 통해 roomName 가져오기
Room room = findRoomById(voteRoomRequest.getRoomId());
String roomName = room.getName();

// 투표 완료 알림 생성
notificationService.createEndNotification(roomName, tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType());
return true;
}
else{
tripPlan.setStatus(Status.COMPLETED); // 여행 계획 '완료'로 상태 변경
tripPlanRepository.save(tripPlan);

// roomId를 통해 roomName 가져오기
Room room = findRoomById(voteRoomRequest.getRoomId());
String roomName = room.getName();

// 투표 완료 알림 생성
notificationService.createEndNotification(roomName, tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType());
return false;
}
Room room = findRoomById(voteRoomRequest.getRoomId());
notificationService.createEndNotification(
room.getName(), tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType()
);

return voteCounts.goodVotes <= voteCounts.badVotes;
}

@Transactional(readOnly = true)
public boolean checkVoteCompleted(VoteRoomRequest voteRoomRequest) {
try {
VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId());
TripPlan tripPlan = findTripPlanById(voteRoomRequest.getTripId());
VoteCounts voteCounts = countVotes(tripPlan.getId());

// 반복되는 로직 헬퍼 클래스로 분리
VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId());
TripPlan tripPlan = findTripPlanById(voteRoomRequest.getTripId());
// 조건 1. 모든 멤버가 투표했는지 확인
boolean allMembersVoted = isAllMembersVoted(voteRoomRequest.getRoomId(), voteCounts.totalVotes);

// 조건 1. 모든 멤버가 투표 했는지
List<Vote> votes = voteRepository.findAllVotesByTripPlanId(tripPlan.getId()); // 여행에 해당하는 모든 투표 리스트
boolean allMembersVoted = isAllMembersVoted(voteRoomRequest.getRoomId(), votes);
// 조건 2. 투표 제한 시간 초과 확인
boolean isTimeExpired = isVoteTimeExpired(voteRoom, tripPlan);

// 조건 2. 투표 제한 시간 초과
boolean isTimeExpired = isVoteTimeExpired(voteRoom, tripPlan);
return (isTimeExpired || allMembersVoted) && (voteCounts.goodVotes != voteCounts.badVotes);
} catch (Exception e) {
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

return isTimeExpired || allMembersVoted;
private VoteCounts countVotes(Long tripPlanId) {
List<Vote> votes = voteRepository.findAllVotesByTripPlanId(tripPlanId);
long goodVotes = votes.stream().filter(v -> v.getType() == VoteType.GOOD).count();
long badVotes = votes.stream().filter(v -> v.getType() == VoteType.BAD).count();

return new VoteCounts(goodVotes, badVotes, votes.size());
}

private TripPlan findTripPlanById(Long tripId) {
Expand All @@ -116,9 +103,9 @@ private VoteRoom findVoteRoomById(Long voteRoomId) {
.orElseThrow(() -> new BusinessException(ErrorCode.VOTE_ROOM_NOT_FOUND));
}

private boolean isAllMembersVoted(Long roomId, List<Vote> votes) {
private boolean isAllMembersVoted(Long roomId, long totalVotes) {
Room room = findRoomById(roomId);
return room.getRoomMembers().size() == votes.size();
return room.getRoomMembers().size() == totalVotes;
}

private boolean isVoteTimeExpired(VoteRoom voteRoom, TripPlan tripPlan) {
Expand All @@ -127,4 +114,16 @@ private boolean isVoteTimeExpired(VoteRoom voteRoom, TripPlan tripPlan) {
voteRoom.getCreatedAt().plusSeconds(tripPlan.getVoteLimitTime().getSeconds())
);
}

private static class VoteCounts {
final long goodVotes;
final long badVotes;
final long totalVotes;

public VoteCounts(long goodVotes, long badVotes, long totalVotes) {
this.goodVotes = goodVotes;
this.badVotes = badVotes;
this.totalVotes = totalVotes;
}
}
}
Loading