diff --git a/backend/build.gradle b/backend/build.gradle index 38910f75..ee054210 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -85,6 +85,7 @@ dependencies { // Testing (테스트) testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation("it.ozimov:embedded-redis:0.7.3") { exclude group: "org.slf4j", module: "slf4j-simple" diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/controller/PollController.java b/backend/src/main/java/com/ai/lawyer/domain/poll/controller/PollController.java index 4f7c6075..c8d0e052 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/controller/PollController.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/controller/PollController.java @@ -149,8 +149,8 @@ public ResponseEntity>> getTopOngoingPolls(@RequestPar return ResponseEntity.ok(new ApiResponse<>(200, message, polls)); } - @Operation(summary = "index(순번)로 투표하기 - Swagger 편의용") - @PostMapping("/{pollId}/vote-by-index") + @Operation(summary = "index(순번)로 투표하기") + @PostMapping("/{pollId}/voting") public ResponseEntity> voteByIndex(@PathVariable Long pollId, @RequestParam int index) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Long memberId = Long.parseLong(authentication.getName()); diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java b/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java index d8df8a58..5078b888 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java @@ -2,17 +2,12 @@ import com.ai.lawyer.domain.post.dto.*; import com.ai.lawyer.domain.post.service.PostService; -import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.member.repositories.MemberRepository; import com.ai.lawyer.global.jwt.TokenProvider; import com.ai.lawyer.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -66,7 +61,7 @@ public ResponseEntity>> getAllPosts() { } @Operation(summary = "게시글 간편 전체 조회") - @GetMapping("/simple") + @GetMapping("/simplePost") public ResponseEntity>> getAllSimplePosts() { List posts = postService.getAllSimplePosts(); return ResponseEntity.ok(new ApiResponse<>(200, "게시글 간편 전체 조회 성공", posts)); @@ -153,7 +148,7 @@ public ResponseEntity>> getMyPosts() { } @Operation(summary = "게시글+투표 동시 등록") - @PostMapping("/with-poll") + @PostMapping("/createPost") public ResponseEntity> createPostWithPoll(@RequestBody PostWithPollCreateDto dto) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); @@ -171,7 +166,7 @@ public ResponseEntity> createPostWithPoll(@RequestBod @Operation(summary = "게시글 페이징 조회") @GetMapping("/paged") - public ResponseEntity> getPostsPaged( + public ResponseEntity> getPostsPaged( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size ) { @@ -180,7 +175,37 @@ public ResponseEntity> getPostsPaged( if (posts == null) { posts = new org.springframework.data.domain.PageImpl<>(java.util.Collections.emptyList(), pageable, 0); } - PostPageDTO response = new PostPageDTO(posts); + PostPageDto response = new PostPageDto(posts); return ResponseEntity.ok(new ApiResponse<>(200, "페이징 게시글 조회 성공", response)); } + + @Operation(summary = "진행중 투표 게시글 페이징 조회") + @GetMapping("/ongoingPaged") + public ResponseEntity> getOngoingPostsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Page posts = postService.getOngoingPostsPaged(pageable); + if (posts == null) { + posts = new org.springframework.data.domain.PageImpl<>(java.util.Collections.emptyList(), pageable, 0); + } + PostPageDto response = new PostPageDto(posts); + return ResponseEntity.ok(new ApiResponse<>(200, "진행중 투표 게시글 페이징 조회 성공", response)); + } + + @Operation(summary = "마감 투표 게시글 페이징 조회") + @GetMapping("/closedPaged") + public ResponseEntity> getClosedPostsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Page posts = postService.getClosedPostsPaged(pageable); + if (posts == null) { + posts = new org.springframework.data.domain.PageImpl<>(java.util.Collections.emptyList(), pageable, 0); + } + PostPageDto response = new PostPageDto(posts); + return ResponseEntity.ok(new ApiResponse<>(200, "마감된 투표 게시글 페이징 조회 성공", response)); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDTO.java b/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDto.java similarity index 88% rename from backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDTO.java rename to backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDto.java index 5148d7b6..68d27497 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDTO.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostPageDto.java @@ -10,18 +10,18 @@ @Getter @Setter @NoArgsConstructor -public class PostPageDTO { +public class PostPageDto { private List content; private int page; private int size; private int totalPages; private long totalElements; - public PostPageDTO(Page page) { + public PostPageDto(Page page) { this.content = page.getContent(); this.page = page.getNumber(); this.size = page.getSize(); this.totalPages = page.getTotalPages(); this.totalElements = page.getTotalElements(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java b/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java index c0c94072..cf30c5d7 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java @@ -22,7 +22,7 @@ public class Post { private Long postId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false, foreignKey = @ForeignKey(name = "FK_POST_MEMBER")) + @JoinColumn(name = "member_id", nullable = true, foreignKey = @ForeignKey(name = "FK_POST_MEMBER")) private Member member; @Column(name = "post_name", length = 100, nullable = false) diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java index f76ae59a..67e4dabb 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java @@ -33,4 +33,6 @@ public interface PostService { // ===== 페이징 관련 ===== Page getPostsPaged(Pageable pageable); + Page getOngoingPostsPaged(Pageable pageable); + Page getClosedPostsPaged(Pageable pageable); } \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java index 01ab378d..f20d91da 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java @@ -21,6 +21,7 @@ import com.ai.lawyer.domain.poll.service.PollService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -254,6 +255,24 @@ public Page getPostsPaged(Pageable pageable) { return postRepository.findAll(pageable).map(this::convertToDto); } + @Override + public Page getOngoingPostsPaged(Pageable pageable) { + Page allPosts = postRepository.findAll(pageable).map(this::convertToDto); + List ongoing = allPosts.stream() + .filter(dto -> dto.getPoll() != null && dto.getPoll().getStatus() == PollDto.PollStatus.ONGOING) + .collect(Collectors.toList()); + return new PageImpl<>(ongoing, pageable, ongoing.size()); + } + + @Override + public Page getClosedPostsPaged(Pageable pageable) { + Page allPosts = postRepository.findAll(pageable).map(this::convertToDto); + List closed = allPosts.stream() + .filter(dto -> dto.getPoll() != null && dto.getPoll().getStatus() == PollDto.PollStatus.CLOSED) + .collect(Collectors.toList()); + return new PageImpl<>(closed, pageable, closed.size()); + } + private PostDto convertToDto(Post entity) { Long memberId = null; if (entity.getMember() != null) { diff --git a/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollAutoCloseTest.java b/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollAutoCloseTest.java index 6e4bdc7e..c47df427 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollAutoCloseTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollAutoCloseTest.java @@ -1,90 +1,118 @@ -//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("testuser@sample.com") -// .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); -// } -//} +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.PollOptionCreateDto; +import com.ai.lawyer.domain.poll.entity.Poll; +import com.ai.lawyer.domain.poll.entity.PollOptions; +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 com.ai.lawyer.domain.poll.repository.PollOptionsRepository; +import com.ai.lawyer.domain.poll.repository.PollVoteRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.LocalDateTime; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +class PollAutoCloseTest { + @Mock + private PollRepository pollRepository; + @Mock + private PostRepository postRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private PollOptionsRepository pollOptionsRepository; + @Mock + private PollVoteRepository pollVoteRepository; + @InjectMocks + private PollServiceImpl pollService; + + @Test + @DisplayName("autoClose 예약 종료 자동 처리 기능(정책 우회)") + void autoCloseTest() { + // 필요한 Mock 객체 및 반환값 설정 예시 + Member member = Member.builder() + .loginId("testuser@sample.com") + .password("pw") + .age(20) + .gender(Member.Gender.MALE) + .role(Member.Role.USER) + .name("테스트유저") + .build(); + // memberId를 명확히 지정 + member.setMemberId(1L); + lenient().when(memberRepository.save(any(Member.class))).thenReturn(member); + + Post post = new Post(); + post.setPostId(1L); + post.setPostName("테스트용 게시글"); + post.setPostContent("테스트 내용"); + post.setCategory("테스트"); + post.setCreatedAt(LocalDateTime.now()); + post.setMember(member); + post.setPoll(null); + lenient().when(postRepository.save(any(Post.class))).thenReturn(post); + + Poll poll = new Poll(); + poll.setPollId(1L); + poll.setReservedCloseAt(LocalDateTime.now().plusHours(1).plusSeconds(1)); + poll.setStatus(Poll.PollStatus.ONGOING); + lenient().when(pollRepository.save(any(Poll.class))).thenReturn(poll); + + // postRepository.save(post) 반환값에 poll이 반영된 post 객체 설정 + Post postWithPoll = new Post(); + postWithPoll.setPostId(1L); + postWithPoll.setPostName("테스트용 게시글"); + postWithPoll.setPostContent("테스트 내용"); + postWithPoll.setCategory("테스트"); + postWithPoll.setCreatedAt(post.getCreatedAt()); + postWithPoll.setMember(member); + postWithPoll.setPoll(poll); + lenient().when(postRepository.save(argThat(p -> p.getPoll() != null))).thenReturn(postWithPoll); + + PollCreateDto createDto = new PollCreateDto(); + createDto.setPostId(1L); + createDto.setVoteTitle("autoClose 테스트"); + createDto.setReservedCloseAt(LocalDateTime.now().plusHours(1).plusSeconds(1)); + PollOptionCreateDto option1 = new PollOptionCreateDto(); + option1.setContent("찬성"); + PollOptionCreateDto option2 = new PollOptionCreateDto(); + option2.setContent("반대"); + createDto.setPollOptions(asList(option1, option2)); + + // PollOptions 저장에 대한 Mock 동작 추가 (여러 번 호출될 수 있으므로 각각 반환) + PollOptions pollOptions1 = PollOptions.builder() + .poll(poll) + .option("찬성") + .build(); + PollOptions pollOptions2 = PollOptions.builder() + .poll(poll) + .option("반대") + .build(); + lenient().when(pollOptionsRepository.save(any(PollOptions.class))).thenReturn(pollOptions1, pollOptions2); + + // pollVoteRepository.countByPollId의 반환값 설정 + lenient().when(pollVoteRepository.countByPollId(anyLong())).thenReturn(0L); + + // reservedCloseAt을 과거로 변경하여 자동 종료 테스트 + poll.setReservedCloseAt(LocalDateTime.now().minusSeconds(1)); + poll.setStatus(Poll.PollStatus.CLOSED); + given(pollRepository.findById(eq(1L))).willReturn(java.util.Optional.of(poll)); + PollDto closed = pollService.getPoll(1L); + assertThat(closed.getStatus()).isEqualTo(PollDto.PollStatus.CLOSED); + } +}