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
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ public ResponseEntity<ApiResponse<List<PollDto>>> 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<ApiResponse<PollVoteDto>> voteByIndex(@PathVariable Long pollId, @RequestParam int index) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Long memberId = Long.parseLong(authentication.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,7 +61,7 @@ public ResponseEntity<ApiResponse<List<PostDetailDto>>> getAllPosts() {
}

@Operation(summary = "게시글 간편 전체 조회")
@GetMapping("/simple")
@GetMapping("/simplePost")
public ResponseEntity<ApiResponse<List<PostSimpleDto>>> getAllSimplePosts() {
List<PostSimpleDto> posts = postService.getAllSimplePosts();
return ResponseEntity.ok(new ApiResponse<>(200, "게시글 간편 전체 조회 성공", posts));
Expand Down Expand Up @@ -153,7 +148,7 @@ public ResponseEntity<ApiResponse<List<PostDto>>> getMyPosts() {
}

@Operation(summary = "게시글+투표 동시 등록")
@PostMapping("/with-poll")
@PostMapping("/createPost")
public ResponseEntity<ApiResponse<PostDetailDto>> createPostWithPoll(@RequestBody PostWithPollCreateDto dto) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
Expand All @@ -171,7 +166,7 @@ public ResponseEntity<ApiResponse<PostDetailDto>> createPostWithPoll(@RequestBod

@Operation(summary = "게시글 페이징 조회")
@GetMapping("/paged")
public ResponseEntity<ApiResponse<PostPageDTO>> getPostsPaged(
public ResponseEntity<ApiResponse<PostPageDto>> getPostsPaged(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
Expand All @@ -180,7 +175,37 @@ public ResponseEntity<ApiResponse<PostPageDTO>> 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<ApiResponse<PostPageDto>> getOngoingPostsPaged(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<PostDto> 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<ApiResponse<PostPageDto>> getClosedPostsPaged(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<PostDto> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
@Getter
@Setter
@NoArgsConstructor
public class PostPageDTO {
public class PostPageDto {
private List<PostDto> content;
private int page;
private int size;
private int totalPages;
private long totalElements;

public PostPageDTO(Page<PostDto> page) {
public PostPageDto(Page<PostDto> page) {
this.content = page.getContent();
this.page = page.getNumber();
this.size = page.getSize();
this.totalPages = page.getTotalPages();
this.totalElements = page.getTotalElements();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ public interface PostService {

// ===== 페이징 관련 =====
Page<PostDto> getPostsPaged(Pageable pageable);
Page<PostDto> getOngoingPostsPaged(Pageable pageable);
Page<PostDto> getClosedPostsPaged(Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,6 +255,24 @@ public Page<PostDto> getPostsPaged(Pageable pageable) {
return postRepository.findAll(pageable).map(this::convertToDto);
}

@Override
public Page<PostDto> getOngoingPostsPaged(Pageable pageable) {
Page<PostDto> allPosts = postRepository.findAll(pageable).map(this::convertToDto);
List<PostDto> 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<PostDto> getClosedPostsPaged(Pageable pageable) {
Page<PostDto> allPosts = postRepository.findAll(pageable).map(this::convertToDto);
List<PostDto> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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("[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);
// }
//}
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("[email protected]")
.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);
}
}