Skip to content

Commit 570ab0a

Browse files
Merge pull request #269 from prgrms-web-devcourse-final-project/refactor/EA3-183-time-vote
[EA3-183] refactor : 모임 일정 조율 관련 전면 리팩토링(hard delete → soft delete + 스케줄러로 삭제)
2 parents 6905f63 + 2c21b47 commit 570ab0a

16 files changed

+295
-166
lines changed

src/main/java/grep/neogulcoder/domain/timevote/TimeVoteStat.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,25 @@ public class TimeVoteStat extends BaseEntity {
3434
@Column(nullable = false)
3535
private Long voteCount;
3636

37-
@Version
38-
@Column(nullable = false)
39-
private Long version;
40-
4137
protected TimeVoteStat() {}
4238

4339
@Builder
44-
public TimeVoteStat(TimeVotePeriod period, LocalDateTime timeSlot, Long voteCount, Long version) {
40+
public TimeVoteStat(TimeVotePeriod period, LocalDateTime timeSlot, Long voteCount) {
4541
this.period = period;
4642
this.timeSlot = timeSlot;
4743
this.voteCount = voteCount;
48-
this.version = version;
4944
}
5045

5146
public static TimeVoteStat of(TimeVotePeriod period, LocalDateTime timeSlot, Long voteCount) {
5247
return TimeVoteStat.builder()
5348
.period(period)
5449
.timeSlot(timeSlot)
5550
.voteCount(voteCount)
56-
.version(0L)
5751
.build();
5852
}
5953

6054
public void addVotes(Long countToAdd) {
61-
log.debug("addVotes: 이전 voteCount={}, 추가 count={}, 이전 version={}", this.voteCount, countToAdd, this.version);
55+
log.debug("addVotes: 이전 voteCount={}, 추가 count={}, 이전 version={}", this.voteCount, countToAdd);
6256
this.voteCount += countToAdd;
6357
}
6458
}

src/main/java/grep/neogulcoder/domain/timevote/exception/code/TimeVoteErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public enum TimeVoteErrorCode implements ErrorCode {
1717
INVALID_TIME_VOTE_PERIOD("TVP_003", HttpStatus.BAD_REQUEST, "모일 일정 조율 기간은 최대 7일까지 설정할 수 있습니다."),
1818
TIME_VOTE_PERIOD_START_DATE_IN_PAST("TVP_004", HttpStatus.BAD_REQUEST, "투표 시작일은 현재 시각보다 이전일 수 없습니다."),
1919
TIME_VOTE_INVALID_DATE_RANGE("TVP_005", HttpStatus.BAD_REQUEST, "종료일은 시작일보다 이후여야 합니다."),
20+
TIME_VOTE_DELETE_FAILED("TVP_006", HttpStatus.INTERNAL_SERVER_ERROR, "기존 투표 데이터 삭제 중 오류가 발생했습니다."),
2021

2122
// Time Vote, Time Vote Stats (e.g. 투표 기간 관련)
2223
TIME_VOTE_OUT_OF_RANGE("TVAS_001", HttpStatus.BAD_REQUEST, "선택한 시간이 투표 기간을 벗어났습니다."),

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVotePeriodRepository.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
import grep.neogulcoder.domain.timevote.TimeVotePeriod;
44
import java.util.Optional;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
69

710
public interface TimeVotePeriodRepository extends JpaRepository<TimeVotePeriod, Long> {
811

9-
void deleteAllByStudyId(Long studyId);
12+
@Modifying
13+
@Query("UPDATE TimeVotePeriod p SET p.activated = false WHERE p.studyId = :studyId")
14+
void deactivateAllByStudyId(@Param("studyId") Long studyId);
1015

11-
boolean existsByStudyId(Long studyId);
16+
boolean existsByStudyIdAndActivatedTrue(Long studyId);
1217

13-
Optional<TimeVotePeriod> findTopByStudyIdOrderByStartDateDesc(Long studyId);
18+
Optional<TimeVotePeriod> findTopByStudyIdAndActivatedTrueOrderByStartDateDesc(Long studyId);
1419
}

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteQueryRepository.java

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package grep.neogulcoder.domain.timevote.repository;
22

3-
import com.querydsl.core.types.dsl.BooleanPath;
4-
import com.querydsl.core.types.dsl.Expressions;
5-
import com.querydsl.core.types.dsl.NumberPath;
6-
import com.querydsl.core.types.dsl.StringPath;
3+
import com.querydsl.core.types.dsl.BooleanExpression;
4+
import com.querydsl.jpa.JPAExpressions;
75
import com.querydsl.jpa.impl.JPAQueryFactory;
86
import grep.neogulcoder.domain.study.QStudyMember;
97
import grep.neogulcoder.domain.timevote.dto.response.TimeVoteSubmissionStatusResponse;
@@ -24,45 +22,44 @@ public TimeVoteQueryRepository(EntityManager em) {
2422
this.queryFactory = new JPAQueryFactory(em);
2523
}
2624

27-
public List<TimeVoteSubmissionStatusResponse> findSubmissionStatuses(Long studyId, Long periodId) {
25+
public List<TimeVoteSubmissionStatusResponse> findSubmissionStatuses(Long studyId,
26+
Long periodId) {
2827
QStudyMember studyMember = QStudyMember.studyMember;
2928
QTimeVote timeVote = QTimeVote.timeVote;
3029
QUser user = QUser.user;
3130

32-
// select절에서 alias를 지정해 Tuple에서 이름 기반으로 값을 꺼낼 수 있도록 Path 객체 생성
33-
NumberPath<Long> aliasStudyMemberId = Expressions.numberPath(Long.class, "aliasStudyMemberId");
34-
StringPath aliasNickname = Expressions.stringPath("aliasNickname");
35-
StringPath aliasProfileImageUrl = Expressions.stringPath("aliasProfileImageUrl");
36-
BooleanPath aliasIsSubmitted = Expressions.booleanPath("aliasIsSubmitted");
31+
BooleanExpression existsVoteSubquery = JPAExpressions
32+
.selectOne()
33+
.from(timeVote)
34+
.where(
35+
timeVote.studyMemberId.eq(studyMember.id)
36+
.and(timeVote.period.periodId.eq(periodId))
37+
.and(timeVote.activated.isTrue())
38+
)
39+
.exists();
40+
3741

3842
List<Tuple> results = queryFactory
3943
.select(
40-
studyMember.id.as(aliasStudyMemberId),
41-
user.nickname.as(aliasNickname),
42-
user.profileImageUrl.as(aliasProfileImageUrl),
43-
timeVote.voteId.count().gt(0).as(aliasIsSubmitted)
44+
studyMember.id,
45+
user.nickname,
46+
user.profileImageUrl,
47+
existsVoteSubquery
4448
)
4549
.from(studyMember)
46-
.leftJoin(user).on(studyMember.userId.eq(user.id))
47-
.leftJoin(timeVote).on(
48-
timeVote.period.periodId.eq(periodId)
49-
.and(timeVote.studyMemberId.eq(studyMember.id))
50-
)
50+
.join(user).on(studyMember.userId.eq(user.id))
5151
.where(
5252
studyMember.study.id.eq(studyId),
5353
studyMember.activated.isTrue()
5454
)
55-
// 중복 방지 (id, 닉네임, 프로필 기준으로 그룹핑)
56-
.groupBy(studyMember.id, user.nickname, user.profileImageUrl)
5755
.fetch();
5856

59-
// Tuple 결과를 DTO 로 변환 (alias 기반으로 값 추출)
6057
return results.stream()
6158
.map(tuple -> TimeVoteSubmissionStatusResponse.builder()
62-
.studyMemberId(tuple.get(aliasStudyMemberId))
63-
.nickname(tuple.get(aliasNickname))
64-
.profileImageUrl(tuple.get(aliasProfileImageUrl))
65-
.isSubmitted(tuple.get(aliasIsSubmitted))
59+
.studyMemberId(tuple.get(studyMember.id))
60+
.nickname(tuple.get(user.nickname))
61+
.profileImageUrl(tuple.get(user.profileImageUrl))
62+
.isSubmitted(tuple.get(existsVoteSubquery))
6663
.build())
6764
.collect(Collectors.toList());
6865
}

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteRepository.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
import grep.neogulcoder.domain.timevote.TimeVotePeriod;
55
import java.util.List;
66
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
710

811
public interface TimeVoteRepository extends JpaRepository<TimeVote, Long> {
912

10-
void deleteAllByPeriod_StudyId(Long studyId);
13+
@Modifying(clearAutomatically = true)
14+
@Query("UPDATE TimeVote v SET v.activated = false WHERE v.period.studyId = :studyId")
15+
void deactivateAllByPeriod_StudyId(@Param("studyId") Long studyId);
1116

12-
List<TimeVote> findByPeriodAndStudyMemberId(TimeVotePeriod period, Long userId);
17+
List<TimeVote> findByPeriodAndStudyMemberIdAndActivatedTrue(TimeVotePeriod period, Long studyMemberId);
1318

14-
void deleteAllByPeriodAndStudyMemberId(TimeVotePeriod period, Long studyMemberId);
19+
@Modifying(clearAutomatically = true)
20+
@Query("UPDATE TimeVote v SET v.activated = false WHERE v.period = :period AND v.studyMemberId = :memberId")
21+
void deactivateByPeriodAndStudyMember(@Param("period") TimeVotePeriod period, @Param("memberId") Long memberId);
1522

16-
boolean existsByPeriodAndStudyMemberId(TimeVotePeriod period, Long studyMemberId);
23+
boolean existsByPeriodAndStudyMemberIdAndActivatedTrue(TimeVotePeriod period, Long studyMemberId);
1724
}

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteStatQueryRepository.java

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import grep.neogulcoder.domain.timevote.TimeVotePeriod;
66
import grep.neogulcoder.domain.timevote.TimeVoteStat;
77
import grep.neogulcoder.domain.timevote.QTimeVote;
8-
import grep.neogulcoder.domain.timevote.QTimeVoteStat;
98
import jakarta.persistence.EntityManager;
10-
import java.time.LocalDateTime;
119
import java.util.List;
1210
import java.util.stream.Collectors;
11+
import lombok.extern.slf4j.Slf4j;
1312
import org.springframework.stereotype.Repository;
1413

14+
@Slf4j
1515
@Repository
1616
public class TimeVoteStatQueryRepository {
1717

@@ -29,29 +29,19 @@ public List<TimeVoteStat> countStatsByPeriod(TimeVotePeriod period) {
2929
List<Tuple> result = queryFactory
3030
.select(timeVote.timeSlot, timeVote.count())
3131
.from(timeVote)
32-
.where(timeVote.period.eq(period))
32+
.where(
33+
timeVote.period.eq(period),
34+
timeVote.activated.isTrue()
35+
)
3336
.groupBy(timeVote.timeSlot)
3437
.fetch();
3538

39+
for (Tuple tuple : result) {
40+
log.info(">>> 통계 디버깅: timeSlot={}, count={}", tuple.get(timeVote.timeSlot), tuple.get(timeVote.count()));
41+
}
42+
3643
return result.stream()
3744
.map(tuple -> TimeVoteStat.of(period, tuple.get(timeVote.timeSlot), tuple.get(timeVote.count())))
3845
.collect(Collectors.toList());
3946
}
40-
41-
public void incrementOrInsert(TimeVotePeriod period, LocalDateTime slot, Long countToAdd) {
42-
QTimeVoteStat stat = QTimeVoteStat.timeVoteStat;
43-
44-
TimeVoteStat existing = queryFactory
45-
.selectFrom(stat)
46-
.where(stat.period.eq(period), stat.timeSlot.eq(slot))
47-
.fetchOne();
48-
49-
if (existing != null) {
50-
existing.addVotes(countToAdd);
51-
} else {
52-
TimeVoteStat newStat = TimeVoteStat.of(period, slot, countToAdd);
53-
em.persist(newStat);
54-
em.flush();
55-
}
56-
}
5747
}

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteStatRepository.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,57 @@
22

33
import grep.neogulcoder.domain.timevote.TimeVotePeriod;
44
import grep.neogulcoder.domain.timevote.TimeVoteStat;
5+
import java.time.LocalDateTime;
56
import java.util.List;
67
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
79
import org.springframework.data.jpa.repository.Query;
810
import org.springframework.data.repository.query.Param;
911

1012
public interface TimeVoteStatRepository extends JpaRepository<TimeVoteStat, Long> {
1113

12-
void deleteAllByPeriod_StudyId(Long studyId);
14+
@Modifying
15+
@Query("UPDATE TimeVoteStat s SET s.activated = false WHERE s.period.studyId = :studyId")
16+
void deactivateAllByPeriod_StudyId(@Param("studyId") Long studyId);
1317

14-
void deleteByPeriod(TimeVotePeriod period);
1518

16-
@Query("SELECT s FROM TimeVoteStat s WHERE s.period.periodId = :periodId")
19+
@Modifying
20+
@Query("UPDATE TimeVoteStat s SET s.activated = false WHERE s.period = :period")
21+
void softDeleteByPeriod(@Param("period") TimeVotePeriod period);
22+
23+
@Query("SELECT s FROM TimeVoteStat s WHERE s.period.periodId = :periodId AND s.activated = true")
1724
List<TimeVoteStat> findAllByPeriodId(@Param("periodId") Long periodId);
25+
26+
@Modifying(clearAutomatically = true)
27+
@Query(value = """
28+
29+
INSERT INTO time_vote_stat (period_id, time_slot, vote_count, activated, created_date, modified_date)
30+
VALUES (:periodId, :timeSlot, :count, true, now(), now())
31+
ON CONFLICT (period_id, time_slot)
32+
DO UPDATE SET
33+
vote_count = time_vote_stat.vote_count + EXCLUDED.vote_count,
34+
modified_date = now(),
35+
activated = true
36+
""", nativeQuery = true)
37+
void upsertVoteStat(
38+
@Param("periodId") Long periodId,
39+
@Param("timeSlot") LocalDateTime timeSlot,
40+
@Param("count") Long count
41+
);
42+
43+
@Modifying(clearAutomatically = true)
44+
@Query(value = """
45+
INSERT INTO time_vote_stat (period_id, time_slot, vote_count, activated, created_date, modified_date)
46+
VALUES (:periodId, :timeSlot, :voteCount, true, now(), now())
47+
ON CONFLICT (period_id, time_slot)
48+
DO UPDATE SET
49+
vote_count = EXCLUDED.vote_count,
50+
modified_date = now(),
51+
activated = true
52+
""", nativeQuery = true)
53+
void bulkUpsertStat(
54+
@Param("periodId") Long periodId,
55+
@Param("timeSlot") LocalDateTime timeSlot,
56+
@Param("voteCount") Long voteCount
57+
);
1858
}

src/main/java/grep/neogulcoder/domain/timevote/service/TimeVoteMapper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,14 @@ public List<TimeVote> toEntities(TimeVoteUpdateRequest request, TimeVotePeriod p
4242
.build())
4343
.collect(Collectors.toList());
4444
}
45+
46+
public List<TimeVote> toEntities(List<LocalDateTime> timeSlots, TimeVotePeriod period, Long studyMemberId) {
47+
return timeSlots.stream()
48+
.map(slot -> TimeVote.builder()
49+
.period(period)
50+
.studyMemberId(studyMemberId)
51+
.timeSlot(slot)
52+
.build())
53+
.collect(Collectors.toList());
54+
}
4555
}

src/main/java/grep/neogulcoder/domain/timevote/service/period/TimeVotePeriodService.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package grep.neogulcoder.domain.timevote.service.period;
22

3+
import static grep.neogulcoder.domain.timevote.exception.code.TimeVoteErrorCode.*;
4+
35
import grep.neogulcoder.domain.timevote.dto.request.TimeVotePeriodCreateRequest;
46
import grep.neogulcoder.domain.timevote.dto.response.TimeVotePeriodResponse;
57
import grep.neogulcoder.domain.timevote.TimeVotePeriod;
@@ -8,13 +10,16 @@
810
import grep.neogulcoder.domain.timevote.repository.TimeVoteRepository;
911
import grep.neogulcoder.domain.timevote.repository.TimeVoteStatRepository;
1012
import grep.neogulcoder.domain.timevote.service.TimeVoteMapper;
13+
import grep.neogulcoder.global.exception.business.BusinessException;
1114
import java.time.LocalDateTime;
1215
import java.time.LocalTime;
1316
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
1418
import org.springframework.context.ApplicationEventPublisher;
1519
import org.springframework.stereotype.Service;
1620
import org.springframework.transaction.annotation.Transactional;
1721

22+
@Slf4j
1823
@Service
1924
@Transactional
2025
@RequiredArgsConstructor
@@ -28,30 +33,51 @@ public class TimeVotePeriodService {
2833
private final ApplicationEventPublisher eventPublisher;
2934

3035
public TimeVotePeriodResponse createTimeVotePeriodAndReturn(TimeVotePeriodCreateRequest request, Long studyId, Long userId) {
36+
log.info("[TimeVotePeriod] 투표 기간 생성 시작 - studyId={}, userId={}, 요청={}", studyId, userId, request);
37+
3138
// 입력값 검증 (스터디 존재, 멤버 활성화 여부, 리더 여부, 날짜 유효성 등)
3239
timeVotePeriodValidator.validatePeriodCreateRequestAndReturnMember(request, studyId, userId);
33-
3440
LocalDateTime adjustedEndDate = adjustEndDate(request.getEndDate());
3541

36-
if (timeVotePeriodRepository.existsByStudyId(studyId)) { deleteAllTimeVoteDate(studyId); }
42+
try {
43+
if (timeVotePeriodRepository.existsByStudyIdAndActivatedTrue(studyId)) {
44+
log.info("[TimeVotePeriod] 기존 투표 기간 존재, 전체 투표 데이터 삭제 진행");
45+
deleteAllTimeVoteDate(studyId);
46+
}
47+
} catch (Exception e) {
48+
log.error("[TimeVotePeriod] 기존 투표 삭제 중 오류 발생 - studyId={}, error={}", studyId, e.getMessage(), e);
49+
throw new BusinessException(TIME_VOTE_DELETE_FAILED);
50+
}
3751

3852
TimeVotePeriod savedPeriod = timeVotePeriodRepository.save(timeVoteMapper.toEntity(request, studyId, adjustedEndDate));
53+
log.info("[TimeVotePeriod] 투표 기간 저장 완료 - periodId={}", savedPeriod.getPeriodId());
3954

4055
// 알림 메시지를 위한 스터디 유효성 검증 (존재 확인)
4156
timeVotePeriodValidator.getValidStudy(studyId);
4257

4358
// 리더 자신을 제외한 나머지 멤버들에게 투표 요청 알림 저장
4459
eventPublisher.publishEvent(new TimeVotePeriodCreatedEvent(studyId, userId));
60+
log.info("[TimeVotePeriod] 투표 요청 알림 이벤트 발행 완료 - studyId={}, userId={}", studyId, userId);
4561
return TimeVotePeriodResponse.from(savedPeriod);
4662
}
4763

4864
// 해당 스터디 ID로 등록된 모든 투표 관련 데이터 삭제
4965
public void deleteAllTimeVoteDate(Long studyId) {
66+
long start = System.currentTimeMillis();
67+
log.info("[TimeVotePeriod] 전체 투표 데이터 삭제 시작 - studyId={}", studyId);
68+
5069
timeVotePeriodValidator.getValidStudy(studyId);
5170

52-
timeVoteRepository.deleteAllByPeriod_StudyId(studyId);
53-
timeVoteStatRepository.deleteAllByPeriod_StudyId(studyId);
54-
timeVotePeriodRepository.deleteAllByStudyId(studyId);
71+
timeVoteRepository.deactivateAllByPeriod_StudyId(studyId);
72+
log.info("[TimeVotePeriod] 투표 soft delete 완료");
73+
74+
timeVoteStatRepository.deactivateAllByPeriod_StudyId(studyId);
75+
log.info("[TimeVotePeriod] 통계 soft delete 완료");
76+
77+
timeVotePeriodRepository.deactivateAllByStudyId(studyId);
78+
log.info("[TimeVotePeriod] 투표 기간 hard delete 완료");
79+
80+
log.info("[TimeVotePeriod] 전체 삭제 완료 - {}ms 소요", System.currentTimeMillis() - start);
5581
}
5682

5783
// 투표 기간의 종료일을 '해당 일의 23:59:59'로 보정

0 commit comments

Comments
 (0)