Skip to content

Commit 4ee6a4e

Browse files
authored
Merge branch 'prgrms-web-devcourse-final-project:develop' into feat/ai
2 parents a33581b + e9dd1ae commit 4ee6a4e

File tree

9 files changed

+295
-45
lines changed

9 files changed

+295
-45
lines changed

.github/workflows/CI-CD_Pipeline.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ jobs:
201201
aws-region: ${{ secrets.AWS_REGION }}
202202
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
203203
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
204-
instance-ids: "i-03509f63569ddb509"
204+
instance-ids: "i-0dd9feb5a0dee9edd"
205205
working-directory: /
206206
comment: Deploy
207207
command: |

backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollRepository.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
import com.ai.lawyer.domain.poll.entity.Poll;
44
import com.ai.lawyer.domain.post.entity.Post;
55
import org.springframework.data.jpa.repository.JpaRepository;
6-
import org.springframework.data.jpa.repository.Query;
76
import org.springframework.data.domain.Pageable;
87

98
import java.util.List;
109
import java.util.Optional;
1110

12-
public interface PollRepository extends JpaRepository<Poll, Long> {
11+
public interface PollRepository extends JpaRepository<Poll, Long>, PollRepositoryCustom {
1312
Optional<Poll> findByPost(Post post);
14-
15-
@Query("SELECT p FROM Poll p LEFT JOIN PollVote v ON p = v.poll GROUP BY p ORDER BY COUNT(v) DESC")
16-
List<Poll> findTopPollsByVoteCount(Pageable pageable);
1713
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.ai.lawyer.domain.poll.repository;
2+
3+
import com.ai.lawyer.domain.poll.entity.Poll;
4+
import com.ai.lawyer.domain.post.entity.Post;
5+
import org.springframework.data.domain.Pageable;
6+
import java.util.List;
7+
import java.util.Optional;
8+
9+
public interface PollRepositoryCustom {
10+
Optional<Poll> findByPost(Post post);
11+
List<Poll> findTopPollsByVoteCount(Pageable pageable);
12+
}
13+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.ai.lawyer.domain.poll.repository;
2+
3+
import com.ai.lawyer.domain.poll.entity.Poll;
4+
import com.ai.lawyer.domain.poll.entity.QPoll;
5+
import com.ai.lawyer.domain.poll.entity.QPollVote;
6+
import com.ai.lawyer.domain.post.entity.Post;
7+
import com.querydsl.jpa.impl.JPAQueryFactory;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.stereotype.Repository;
11+
12+
import java.util.List;
13+
import java.util.Optional;
14+
15+
@Repository
16+
@RequiredArgsConstructor
17+
public class PollRepositoryImpl implements PollRepositoryCustom {
18+
private final JPAQueryFactory queryFactory;
19+
private final QPoll poll = QPoll.poll;
20+
private final QPollVote pollVote = QPollVote.pollVote;
21+
22+
@Override
23+
public Optional<Poll> findByPost(Post post) {
24+
return Optional.ofNullable(
25+
queryFactory.selectFrom(poll)
26+
.where(poll.getPost().eq(post))
27+
.fetchOne()
28+
);
29+
}
30+
31+
@Override
32+
public List<Poll> findTopPollsByVoteCount(Pageable pageable) {
33+
return queryFactory.selectFrom(poll)
34+
.leftJoin(pollVote).on(poll.getPollId().eq(pollVote.getPoll().getPollId()))
35+
.groupBy(poll.getPollId())
36+
.orderBy(pollVote.count().desc())
37+
.offset(pageable.getOffset())
38+
.limit(pageable.getPageSize())
39+
.fetch();
40+
}
41+
}
Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,8 @@
11
package com.ai.lawyer.domain.poll.repository;
22

33
import com.ai.lawyer.domain.poll.entity.PollVote;
4-
import com.ai.lawyer.domain.poll.entity.Poll;
54
import org.springframework.data.jpa.repository.JpaRepository;
6-
import org.springframework.data.jpa.repository.Query;
7-
import org.springframework.data.domain.Pageable;
8-
import org.springframework.data.repository.query.Param;
9-
import java.util.List;
105

11-
public interface PollVoteRepository extends JpaRepository<PollVote, Long> {
12-
@Query("SELECT v.poll.pollId, COUNT(v.pollVoteId) FROM PollVote v WHERE v.poll.status = :status GROUP BY v.poll.pollId ORDER BY COUNT(v.pollVoteId) DESC")
13-
List<Object[]> findTopPollByStatus(@Param("status") Poll.PollStatus status);
6+
public interface PollVoteRepository extends JpaRepository<PollVote, Long>, PollVoteRepositoryCustom {
7+
}
148

15-
@Query("SELECT p.pollId, COUNT(v) as voteCount FROM PollVote v JOIN v.poll p WHERE p.status = :status GROUP BY p.pollId ORDER BY voteCount DESC")
16-
List<Object[]> findTopNPollByStatus(@Param("status") Poll.PollStatus status, Pageable pageable);
17-
18-
@Query("SELECT COUNT(v.pollVoteId) FROM PollVote v WHERE v.poll.pollId = :pollId")
19-
Long countByPollId(@Param("pollId") Long pollId);
20-
21-
@Query("SELECT COUNT(v.pollVoteId) FROM PollVote v WHERE v.pollOptions.pollItemsId = :pollOptionId")
22-
Long countByPollOptionId(@Param("pollOptionId") Long pollOptionId);
23-
24-
@Query("SELECT v.pollOptions.pollItemsId, v.member.gender, v.member.age, COUNT(v.pollVoteId) FROM PollVote v WHERE v.pollOptions.pollItemsId IN :pollOptionIds GROUP BY v.pollOptions.pollItemsId, v.member.gender, v.member.age")
25-
java.util.List<Object[]> countStaticsByPollOptionIds(@Param("pollOptionIds") java.util.List<Long> pollOptionIds);
26-
27-
@Query("SELECT o.option, " +
28-
"CASE WHEN m.age < 20 THEN '10대' WHEN m.age < 30 THEN '20대' " +
29-
"WHEN m.age < 40 THEN '30대' WHEN m.age < 50 THEN '40대' WHEN m.age < 60 THEN '50대' " +
30-
"WHEN m.age < 70 THEN '60대' WHEN m.age < 80 THEN '70대' ELSE '80대 이상' END, " +
31-
"COUNT(v) " +
32-
"FROM PollVote v JOIN v.pollOptions o JOIN v.member m " +
33-
"WHERE o.poll.pollId = :pollId " +
34-
"GROUP BY o.option, " +
35-
"CASE WHEN m.age < 20 THEN '10대' WHEN m.age < 30 THEN '20대' " +
36-
"WHEN m.age < 40 THEN '30대' WHEN m.age < 50 THEN '40대' WHEN m.age < 60 THEN '50대' " +
37-
"WHEN m.age < 70 THEN '60대' WHEN m.age < 80 THEN '70대' ELSE '80대 이상' END")
38-
List<Object[]> getOptionAgeStatics(@Param("pollId") Long pollId);
39-
40-
@Query("SELECT o.option, m.gender, COUNT(v) FROM PollVote v JOIN v.pollOptions o JOIN v.member m WHERE o.poll.pollId = :pollId GROUP BY o.option, m.gender")
41-
List<Object[]> getOptionGenderStatics(@Param("pollId") Long pollId);
42-
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ai.lawyer.domain.poll.repository;
2+
3+
import com.ai.lawyer.domain.poll.entity.Poll;
4+
import com.ai.lawyer.domain.poll.entity.PollVote;
5+
import org.springframework.data.domain.Pageable;
6+
import java.util.List;
7+
8+
public interface PollVoteRepositoryCustom {
9+
List<Object[]> findTopPollByStatus(Poll.PollStatus status);
10+
List<Object[]> findTopNPollByStatus(Poll.PollStatus status, Pageable pageable);
11+
Long countByPollId(Long pollId);
12+
Long countByPollOptionId(Long pollOptionId);
13+
List<Object[]> countStaticsByPollOptionIds(List<Long> pollOptionIds);
14+
List<Object[]> getOptionAgeStatics(Long pollId);
15+
List<Object[]> getOptionGenderStatics(Long pollId);
16+
}
17+
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.ai.lawyer.domain.poll.repository;
2+
3+
import com.ai.lawyer.domain.poll.entity.Poll;
4+
import com.ai.lawyer.domain.poll.entity.QPoll;
5+
import com.ai.lawyer.domain.poll.entity.QPollOptions;
6+
import com.ai.lawyer.domain.poll.entity.QPollVote;
7+
import com.ai.lawyer.domain.member.entity.QMember;
8+
import com.querydsl.jpa.impl.JPAQueryFactory;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.stereotype.Repository;
12+
import com.querydsl.core.Tuple;
13+
14+
import java.util.List;
15+
16+
@Repository
17+
@RequiredArgsConstructor
18+
public class PollVoteRepositoryImpl implements PollVoteRepositoryCustom {
19+
private final JPAQueryFactory queryFactory;
20+
private final QPollVote pollVote = QPollVote.pollVote;
21+
private final QPoll poll = QPoll.poll;
22+
private final QPollOptions pollOptions = QPollOptions.pollOptions;
23+
private final QMember member = QMember.member;
24+
25+
@Override
26+
public List<Object[]> findTopPollByStatus(Poll.PollStatus status) {
27+
List<Tuple> tuples = queryFactory.select(poll.getPollId(), pollVote.count())
28+
.from(pollVote)
29+
.join(pollVote.getPoll(), poll)
30+
.where(poll.getStatus().eq(status))
31+
.groupBy(poll.getPollId())
32+
.orderBy(pollVote.count().desc())
33+
.fetch();
34+
return tuples.stream().map(Tuple::toArray).toList();
35+
}
36+
37+
@Override
38+
public List<Object[]> findTopNPollByStatus(Poll.PollStatus status, Pageable pageable) {
39+
List<Tuple> tuples = queryFactory.select(poll.getPollId(), pollVote.count())
40+
.from(pollVote)
41+
.join(pollVote.getPoll(), poll)
42+
.where(poll.getStatus().eq(status))
43+
.groupBy(poll.getPollId())
44+
.orderBy(pollVote.count().desc())
45+
.offset(pageable.getOffset())
46+
.limit(pageable.getPageSize())
47+
.fetch();
48+
return tuples.stream().map(Tuple::toArray).toList();
49+
}
50+
51+
@Override
52+
public Long countByPollId(Long pollId) {
53+
return queryFactory.select(pollVote.count())
54+
.from(pollVote)
55+
.join(pollVote.getPoll(), poll)
56+
.where(poll.getPollId().eq(pollId))
57+
.fetchOne();
58+
}
59+
60+
@Override
61+
public Long countByPollOptionId(Long pollOptionId) {
62+
return queryFactory.select(pollVote.count())
63+
.from(pollVote)
64+
.join(pollVote.getPollOptions(), pollOptions)
65+
.where(pollOptions.getPollItemsId().eq(pollOptionId))
66+
.fetchOne();
67+
}
68+
69+
@Override
70+
public List<Object[]> countStaticsByPollOptionIds(List<Long> pollOptionIds) {
71+
List<Tuple> tuples = queryFactory.select(pollOptions.getPollItemsId(), member.getGender(), member.getAge(), pollVote.count())
72+
.from(pollVote)
73+
.join(pollVote.getPollOptions(), pollOptions)
74+
.join(pollVote.getMember(), member)
75+
.where(pollOptions.getPollItemsId().in(pollOptionIds))
76+
.groupBy(pollOptions.getPollItemsId(), member.getGender(), member.getAge())
77+
.fetch();
78+
return tuples.stream().map(Tuple::toArray).toList();
79+
}
80+
81+
@Override
82+
public List<Object[]> getOptionAgeStatics(Long pollId) {
83+
List<Tuple> tuples = queryFactory.select(
84+
pollOptions.getOption(),
85+
new com.querydsl.core.types.dsl.CaseBuilder()
86+
.when(member.getAge().lt(20)).then("10대")
87+
.when(member.getAge().lt(30)).then("20대")
88+
.when(member.getAge().lt(40)).then("30대")
89+
.when(member.getAge().lt(50)).then("40대")
90+
.when(member.getAge().lt(60)).then("50대")
91+
.when(member.getAge().lt(70)).then("60대")
92+
.when(member.getAge().lt(80)).then("70대")
93+
.otherwise("80대 이상"),
94+
pollVote.count())
95+
.from(pollVote)
96+
.join(pollVote.getPollOptions(), pollOptions)
97+
.join(pollVote.getMember(), member)
98+
.where(pollOptions.getPoll().getPollId().eq(pollId))
99+
.groupBy(pollOptions.getOption(),
100+
new com.querydsl.core.types.dsl.CaseBuilder()
101+
.when(member.getAge().lt(20)).then("10대")
102+
.when(member.getAge().lt(30)).then("20대")
103+
.when(member.getAge().lt(40)).then("30대")
104+
.when(member.getAge().lt(50)).then("40대")
105+
.when(member.getAge().lt(60)).then("50대")
106+
.when(member.getAge().lt(70)).then("60대")
107+
.when(member.getAge().lt(80)).then("70대")
108+
.otherwise("80대 이상"))
109+
.fetch();
110+
return tuples.stream().map(Tuple::toArray).toList();
111+
}
112+
113+
@Override
114+
public List<Object[]> getOptionGenderStatics(Long pollId) {
115+
List<Tuple> tuples = queryFactory.select(
116+
pollOptions.getOption(),
117+
member.getGender(),
118+
pollVote.count())
119+
.from(pollVote)
120+
.join(pollVote.getPollOptions(), pollOptions)
121+
.join(pollVote.getMember(), member)
122+
.where(pollOptions.getPoll().getPollId().eq(pollId))
123+
.groupBy(pollOptions.getOption(), member.getGender())
124+
.fetch();
125+
return tuples.stream().map(Tuple::toArray).toList();
126+
}
127+
}

backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public PollDto createPoll(PollCreateDto request, Long memberId) {
8888
public PollDto getPoll(Long pollId) {
8989
Poll poll = pollRepository.findById(pollId)
9090
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다."));
91-
autoCloseIfNeeded(poll);
91+
autoClose(poll);
9292
if (poll.getStatus() == Poll.PollStatus.CLOSED) {
9393
return getPollWithStatistics(pollId);
9494
}
@@ -99,7 +99,7 @@ public PollDto getPoll(Long pollId) {
9999
public List<PollDto> getPollsByStatus(PollDto.PollStatus status) {
100100
List<Poll> polls = pollRepository.findAll();
101101
for (Poll poll : polls) {
102-
autoCloseIfNeeded(poll);
102+
autoClose(poll);
103103
}
104104
List<PollDto> pollDtos = polls.stream()
105105
.filter(p -> p.getStatus().name().equals(status.name()))
@@ -516,7 +516,7 @@ private String getAgeGroup(Integer age) {
516516
}
517517

518518
// 자동 종료 로직 보강
519-
private void autoCloseIfNeeded(Poll poll) {
519+
private void autoClose(Poll poll) {
520520
LocalDateTime now = java.time.LocalDateTime.now();
521521
if (poll.getStatus() == Poll.PollStatus.ONGOING) {
522522
if (poll.getReservedCloseAt() != null && poll.getReservedCloseAt().isBefore(now)) {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.ai.lawyer.domain.poll.service;
2+
3+
import com.ai.lawyer.domain.poll.dto.PollCreateDto;
4+
import com.ai.lawyer.domain.poll.dto.PollDto;
5+
import com.ai.lawyer.domain.poll.dto.PollUpdateDto;
6+
import com.ai.lawyer.domain.poll.dto.PollVoteDto;
7+
import com.ai.lawyer.domain.poll.dto.PollStaticsResponseDto;
8+
import com.ai.lawyer.domain.poll.entity.Poll;
9+
import com.ai.lawyer.domain.post.entity.Post;
10+
import com.ai.lawyer.domain.post.repository.PostRepository;
11+
import com.ai.lawyer.domain.member.entity.Member;
12+
import com.ai.lawyer.domain.member.repositories.MemberRepository;
13+
import com.ai.lawyer.domain.poll.repository.PollRepository;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Test;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.boot.test.context.SpringBootTest;
18+
import org.springframework.transaction.annotation.Transactional;
19+
import org.springframework.web.server.ResponseStatusException;
20+
21+
import java.util.List;
22+
23+
import static org.assertj.core.api.Assertions.*;
24+
25+
@SpringBootTest
26+
@Transactional
27+
class PollAutoCloseTest {
28+
@Autowired
29+
private PollService pollService;
30+
31+
@Autowired
32+
private PostRepository postRepository;
33+
34+
@Autowired
35+
private MemberRepository memberRepository;
36+
37+
@Autowired
38+
private PollRepository pollRepository;
39+
40+
@Test
41+
@DisplayName("autoClose 예약 종료 자동 처리 기능(정책 우회)")
42+
void autoCloseTest() throws Exception {
43+
// 테스트용 member 생성
44+
Member member = Member.builder()
45+
.loginId("[email protected]")
46+
.password("pw")
47+
.age(20)
48+
.gender(Member.Gender.MALE)
49+
.role(Member.Role.USER)
50+
.name("테스트유저")
51+
.build();
52+
member = memberRepository.save(member);
53+
54+
// 테스트용 post 생성
55+
Post post = new Post();
56+
post.setPostName("테스트용 게시글");
57+
post.setPostContent("테스트 내용");
58+
post.setCategory("테스트");
59+
post.setCreatedAt(java.time.LocalDateTime.now());
60+
post.setMember(member);
61+
post = postRepository.save(post);
62+
63+
PollCreateDto createDto = new PollCreateDto();
64+
createDto.setPostId(post.getPostId());
65+
createDto.setVoteTitle("autoClose 테스트");
66+
createDto.setReservedCloseAt(java.time.LocalDateTime.now().plusHours(1).plusSeconds(1));
67+
// 투표 항목 2개 추가
68+
var option1 = new com.ai.lawyer.domain.poll.dto.PollOptionCreateDto();
69+
option1.setContent("찬성");
70+
var option2 = new com.ai.lawyer.domain.poll.dto.PollOptionCreateDto();
71+
option2.setContent("반대");
72+
createDto.setPollOptions(java.util.Arrays.asList(option1, option2));
73+
PollDto created = pollService.createPoll(createDto, member.getMemberId());
74+
75+
// 2. 생성 직후 상태는 ONGOING이어야 함
76+
PollDto ongoing = pollService.getPoll(created.getPollId());
77+
assertThat(ongoing.getStatus()).isEqualTo(PollDto.PollStatus.ONGOING);
78+
79+
// 3. reservedCloseAt을 DB에서 과거로 강제 변경
80+
var poll = pollRepository.findById(created.getPollId()).get();
81+
var reservedCloseAtField = poll.getClass().getDeclaredField("reservedCloseAt");
82+
reservedCloseAtField.setAccessible(true);
83+
reservedCloseAtField.set(poll, java.time.LocalDateTime.now().minusSeconds(1));
84+
pollRepository.save(poll);
85+
86+
// 4. getPoll 호출 시 자동 종료(CLOSED)로 바뀌는지 확인
87+
PollDto closed = pollService.getPoll(created.getPollId());
88+
assertThat(closed.getStatus()).isEqualTo(PollDto.PollStatus.CLOSED);
89+
}
90+
}

0 commit comments

Comments
 (0)