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
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.post.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Optional;

public interface PollRepository extends JpaRepository<Poll, Long> {
public interface PollRepository extends JpaRepository<Poll, Long>, PollRepositoryCustom {
Optional<Poll> findByPost(Post post);

@Query("SELECT p FROM Poll p LEFT JOIN PollVote v ON p = v.poll GROUP BY p ORDER BY COUNT(v) DESC")
List<Poll> findTopPollsByVoteCount(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ai.lawyer.domain.poll.repository;

import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.post.entity.Post;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;

public interface PollRepositoryCustom {
Optional<Poll> findByPost(Post post);
List<Poll> findTopPollsByVoteCount(Pageable pageable);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.ai.lawyer.domain.poll.repository;

import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.poll.entity.QPoll;
import com.ai.lawyer.domain.poll.entity.QPollVote;
import com.ai.lawyer.domain.post.entity.Post;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class PollRepositoryImpl implements PollRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QPoll poll = QPoll.poll;
private final QPollVote pollVote = QPollVote.pollVote;

@Override
public Optional<Poll> findByPost(Post post) {
return Optional.ofNullable(
queryFactory.selectFrom(poll)
.where(poll.getPost().eq(post))
.fetchOne()
);
}

@Override
public List<Poll> findTopPollsByVoteCount(Pageable pageable) {
return queryFactory.selectFrom(poll)
.leftJoin(pollVote).on(poll.getPollId().eq(pollVote.getPoll().getPollId()))
.groupBy(poll.getPollId())
.orderBy(pollVote.count().desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
package com.ai.lawyer.domain.poll.repository;

import com.ai.lawyer.domain.poll.entity.PollVote;
import com.ai.lawyer.domain.poll.entity.Poll;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface PollVoteRepository extends JpaRepository<PollVote, Long> {
@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")
List<Object[]> findTopPollByStatus(@Param("status") Poll.PollStatus status);
public interface PollVoteRepository extends JpaRepository<PollVote, Long>, PollVoteRepositoryCustom {
}

@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")
List<Object[]> findTopNPollByStatus(@Param("status") Poll.PollStatus status, Pageable pageable);

@Query("SELECT COUNT(v.pollVoteId) FROM PollVote v WHERE v.poll.pollId = :pollId")
Long countByPollId(@Param("pollId") Long pollId);

@Query("SELECT COUNT(v.pollVoteId) FROM PollVote v WHERE v.pollOptions.pollItemsId = :pollOptionId")
Long countByPollOptionId(@Param("pollOptionId") Long pollOptionId);

@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")
java.util.List<Object[]> countStaticsByPollOptionIds(@Param("pollOptionIds") java.util.List<Long> pollOptionIds);

@Query("SELECT o.option, " +
"CASE WHEN m.age < 20 THEN '10대' WHEN m.age < 30 THEN '20대' " +
"WHEN m.age < 40 THEN '30대' WHEN m.age < 50 THEN '40대' WHEN m.age < 60 THEN '50대' " +
"WHEN m.age < 70 THEN '60대' WHEN m.age < 80 THEN '70대' ELSE '80대 이상' END, " +
"COUNT(v) " +
"FROM PollVote v JOIN v.pollOptions o JOIN v.member m " +
"WHERE o.poll.pollId = :pollId " +
"GROUP BY o.option, " +
"CASE WHEN m.age < 20 THEN '10대' WHEN m.age < 30 THEN '20대' " +
"WHEN m.age < 40 THEN '30대' WHEN m.age < 50 THEN '40대' WHEN m.age < 60 THEN '50대' " +
"WHEN m.age < 70 THEN '60대' WHEN m.age < 80 THEN '70대' ELSE '80대 이상' END")
List<Object[]> getOptionAgeStatics(@Param("pollId") Long pollId);

@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")
List<Object[]> getOptionGenderStatics(@Param("pollId") Long pollId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ai.lawyer.domain.poll.repository;

import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.poll.entity.PollVote;
import org.springframework.data.domain.Pageable;
import java.util.List;

public interface PollVoteRepositoryCustom {
List<Object[]> findTopPollByStatus(Poll.PollStatus status);
List<Object[]> findTopNPollByStatus(Poll.PollStatus status, Pageable pageable);
Long countByPollId(Long pollId);
Long countByPollOptionId(Long pollOptionId);
List<Object[]> countStaticsByPollOptionIds(List<Long> pollOptionIds);
List<Object[]> getOptionAgeStatics(Long pollId);
List<Object[]> getOptionGenderStatics(Long pollId);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.ai.lawyer.domain.poll.repository;

import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.poll.entity.QPoll;
import com.ai.lawyer.domain.poll.entity.QPollOptions;
import com.ai.lawyer.domain.poll.entity.QPollVote;
import com.ai.lawyer.domain.member.entity.QMember;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import com.querydsl.core.Tuple;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class PollVoteRepositoryImpl implements PollVoteRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QPollVote pollVote = QPollVote.pollVote;
private final QPoll poll = QPoll.poll;
private final QPollOptions pollOptions = QPollOptions.pollOptions;
private final QMember member = QMember.member;

@Override
public List<Object[]> findTopPollByStatus(Poll.PollStatus status) {
List<Tuple> tuples = queryFactory.select(poll.getPollId(), pollVote.count())
.from(pollVote)
.join(pollVote.getPoll(), poll)
.where(poll.getStatus().eq(status))
.groupBy(poll.getPollId())
.orderBy(pollVote.count().desc())
.fetch();
return tuples.stream().map(Tuple::toArray).toList();
}

@Override
public List<Object[]> findTopNPollByStatus(Poll.PollStatus status, Pageable pageable) {
List<Tuple> tuples = queryFactory.select(poll.getPollId(), pollVote.count())
.from(pollVote)
.join(pollVote.getPoll(), poll)
.where(poll.getStatus().eq(status))
.groupBy(poll.getPollId())
.orderBy(pollVote.count().desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return tuples.stream().map(Tuple::toArray).toList();
}

@Override
public Long countByPollId(Long pollId) {
return queryFactory.select(pollVote.count())
.from(pollVote)
.join(pollVote.getPoll(), poll)
.where(poll.getPollId().eq(pollId))
.fetchOne();
}

@Override
public Long countByPollOptionId(Long pollOptionId) {
return queryFactory.select(pollVote.count())
.from(pollVote)
.join(pollVote.getPollOptions(), pollOptions)
.where(pollOptions.getPollItemsId().eq(pollOptionId))
.fetchOne();
}

@Override
public List<Object[]> countStaticsByPollOptionIds(List<Long> pollOptionIds) {
List<Tuple> tuples = queryFactory.select(pollOptions.getPollItemsId(), member.getGender(), member.getAge(), pollVote.count())
.from(pollVote)
.join(pollVote.getPollOptions(), pollOptions)
.join(pollVote.getMember(), member)
.where(pollOptions.getPollItemsId().in(pollOptionIds))
.groupBy(pollOptions.getPollItemsId(), member.getGender(), member.getAge())
.fetch();
return tuples.stream().map(Tuple::toArray).toList();
}

@Override
public List<Object[]> getOptionAgeStatics(Long pollId) {
List<Tuple> tuples = queryFactory.select(
pollOptions.getOption(),
new com.querydsl.core.types.dsl.CaseBuilder()
.when(member.getAge().lt(20)).then("10대")
.when(member.getAge().lt(30)).then("20대")
.when(member.getAge().lt(40)).then("30대")
.when(member.getAge().lt(50)).then("40대")
.when(member.getAge().lt(60)).then("50대")
.when(member.getAge().lt(70)).then("60대")
.when(member.getAge().lt(80)).then("70대")
.otherwise("80대 이상"),
pollVote.count())
.from(pollVote)
.join(pollVote.getPollOptions(), pollOptions)
.join(pollVote.getMember(), member)
.where(pollOptions.getPoll().getPollId().eq(pollId))
.groupBy(pollOptions.getOption(),
new com.querydsl.core.types.dsl.CaseBuilder()
.when(member.getAge().lt(20)).then("10대")
.when(member.getAge().lt(30)).then("20대")
.when(member.getAge().lt(40)).then("30대")
.when(member.getAge().lt(50)).then("40대")
.when(member.getAge().lt(60)).then("50대")
.when(member.getAge().lt(70)).then("60대")
.when(member.getAge().lt(80)).then("70대")
.otherwise("80대 이상"))
.fetch();
return tuples.stream().map(Tuple::toArray).toList();
}

@Override
public List<Object[]> getOptionGenderStatics(Long pollId) {
List<Tuple> tuples = queryFactory.select(
pollOptions.getOption(),
member.getGender(),
pollVote.count())
.from(pollVote)
.join(pollVote.getPollOptions(), pollOptions)
.join(pollVote.getMember(), member)
.where(pollOptions.getPoll().getPollId().eq(pollId))
.groupBy(pollOptions.getOption(), member.getGender())
.fetch();
return tuples.stream().map(Tuple::toArray).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public PollDto createPoll(PollCreateDto request, Long memberId) {
public PollDto getPoll(Long pollId) {
Poll poll = pollRepository.findById(pollId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다."));
autoCloseIfNeeded(poll);
autoClose(poll);
if (poll.getStatus() == Poll.PollStatus.CLOSED) {
return getPollWithStatistics(pollId);
}
Expand All @@ -99,7 +99,7 @@ public PollDto getPoll(Long pollId) {
public List<PollDto> getPollsByStatus(PollDto.PollStatus status) {
List<Poll> polls = pollRepository.findAll();
for (Poll poll : polls) {
autoCloseIfNeeded(poll);
autoClose(poll);
}
List<PollDto> pollDtos = polls.stream()
.filter(p -> p.getStatus().name().equals(status.name()))
Expand Down Expand Up @@ -516,7 +516,7 @@ private String getAgeGroup(Integer age) {
}

// 자동 종료 로직 보강
private void autoCloseIfNeeded(Poll poll) {
private void autoClose(Poll poll) {
LocalDateTime now = java.time.LocalDateTime.now();
if (poll.getStatus() == Poll.PollStatus.ONGOING) {
if (poll.getReservedCloseAt() != null && poll.getReservedCloseAt().isBefore(now)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.ai.lawyer.domain.poll.service;

import com.ai.lawyer.domain.poll.dto.PollCreateDto;
import com.ai.lawyer.domain.poll.dto.PollDto;
import com.ai.lawyer.domain.poll.dto.PollUpdateDto;
import com.ai.lawyer.domain.poll.dto.PollVoteDto;
import com.ai.lawyer.domain.poll.dto.PollStaticsResponseDto;
import com.ai.lawyer.domain.poll.entity.Poll;
import com.ai.lawyer.domain.post.entity.Post;
import com.ai.lawyer.domain.post.repository.PostRepository;
import com.ai.lawyer.domain.member.entity.Member;
import com.ai.lawyer.domain.member.repositories.MemberRepository;
import com.ai.lawyer.domain.poll.repository.PollRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest
@Transactional
class PollAutoCloseTest {
@Autowired
private PollService pollService;

@Autowired
private PostRepository postRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private PollRepository pollRepository;

@Test
@DisplayName("autoClose 예약 종료 자동 처리 기능(정책 우회)")
void autoCloseTest() throws Exception {
// 테스트용 member 생성
Member member = Member.builder()
.loginId("[email protected]")
.password("pw")
.age(20)
.gender(Member.Gender.MALE)
.role(Member.Role.USER)
.name("테스트유저")
.build();
member = memberRepository.save(member);

// 테스트용 post 생성
Post post = new Post();
post.setPostName("테스트용 게시글");
post.setPostContent("테스트 내용");
post.setCategory("테스트");
post.setCreatedAt(java.time.LocalDateTime.now());
post.setMember(member);
post = postRepository.save(post);

PollCreateDto createDto = new PollCreateDto();
createDto.setPostId(post.getPostId());
createDto.setVoteTitle("autoClose 테스트");
createDto.setReservedCloseAt(java.time.LocalDateTime.now().plusHours(1).plusSeconds(1));
// 투표 항목 2개 추가
var option1 = new com.ai.lawyer.domain.poll.dto.PollOptionCreateDto();
option1.setContent("찬성");
var option2 = new com.ai.lawyer.domain.poll.dto.PollOptionCreateDto();
option2.setContent("반대");
createDto.setPollOptions(java.util.Arrays.asList(option1, option2));
PollDto created = pollService.createPoll(createDto, member.getMemberId());

// 2. 생성 직후 상태는 ONGOING이어야 함
PollDto ongoing = pollService.getPoll(created.getPollId());
assertThat(ongoing.getStatus()).isEqualTo(PollDto.PollStatus.ONGOING);

// 3. reservedCloseAt을 DB에서 과거로 강제 변경
var poll = pollRepository.findById(created.getPollId()).get();
var reservedCloseAtField = poll.getClass().getDeclaredField("reservedCloseAt");
reservedCloseAtField.setAccessible(true);
reservedCloseAtField.set(poll, java.time.LocalDateTime.now().minusSeconds(1));
pollRepository.save(poll);

// 4. getPoll 호출 시 자동 종료(CLOSED)로 바뀌는지 확인
PollDto closed = pollService.getPoll(created.getPollId());
assertThat(closed.getStatus()).isEqualTo(PollDto.PollStatus.CLOSED);
}
}