From 22f41c99a3084f35cfc5e262e7ff12508a765f6e Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Fri, 10 Oct 2025 15:54:58 +0900 Subject: [PATCH] feat[post]:myvotedpaged --- .../poll/controller/PollController.java | 6 +- .../poll/repository/PollVoteRepository.java | 2 + .../domain/poll/service/PollService.java | 2 +- .../domain/poll/service/PollServiceImpl.java | 26 ++-- .../post/controller/PostController.java | 113 ++++++++---------- .../domain/post/service/PostService.java | 3 + .../domain/post/service/PostServiceImpl.java | 43 +++++-- .../com/ai/lawyer/global/util/AuthUtil.java | 39 ++++++ .../poll/controller/PollControllerTest.java | 2 +- .../domain/poll/service/PollServiceTest.java | 6 +- .../post/controller/PostControllerTest.java | 77 ++++++++++++ 11 files changed, 227 insertions(+), 92 deletions(-) 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 66e9ec9..6c34b1b 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 @@ -75,11 +75,7 @@ public ResponseEntity> updatePoll(@PathVariable Long pollId @DeleteMapping("/{pollId}") public ResponseEntity> deletePoll(@PathVariable Long pollId) { Long currentMemberId = AuthUtil.getCurrentMemberId(); - PollDto poll = pollService.getPoll(pollId, currentMemberId); - if (!poll.getPostId().equals(currentMemberId)) { - return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인만 투표를 삭제할 수 있습니다.", null)); - } - pollService.deletePoll(pollId); + pollService.deletePoll(pollId, currentMemberId); return ResponseEntity.ok(new ApiResponse<>(200, "투표가 삭제되었습니다.", null)); } diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepository.java b/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepository.java index 4ac9477..a2a6138 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepository.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepository.java @@ -3,10 +3,12 @@ import com.ai.lawyer.domain.poll.entity.PollVote; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface PollVoteRepository extends JpaRepository, PollVoteRepositoryCustom { Optional findByMember_MemberIdAndPoll_PollId(Long memberId, Long pollId); void deleteByMember_MemberIdAndPoll_PollId(Long memberId, Long pollId); Optional findByMember_MemberIdAndPollOptions_PollItemsId(Long memberId, Long pollItemsId); + List findByMember_MemberId(Long memberId); } diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollService.java b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollService.java index 97d79da..ebe9f03 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollService.java @@ -34,7 +34,7 @@ public interface PollService { PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto, Long memberId); void patchUpdatePoll(Long pollId, PollUpdateDto pollUpdateDto); void closePoll(Long pollId); - void deletePoll(Long pollId); + void deletePoll(Long pollId, Long memberId); // ===== 검증 관련 ===== void validatePollCreate(PollCreateDto dto); diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java index 32ffb01..feeb932 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java @@ -4,11 +4,12 @@ import com.ai.lawyer.domain.poll.repository.*; import com.ai.lawyer.domain.poll.dto.PollDto; import com.ai.lawyer.domain.member.entity.Member; -import com.ai.lawyer.domain.member.repositories.MemberRepository; +import com.ai.lawyer.domain.post.dto.PostDto; import com.ai.lawyer.domain.post.entity.Post; import com.ai.lawyer.domain.post.repository.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +32,7 @@ import com.ai.lawyer.domain.poll.dto.PollGenderStaticsDto; import com.ai.lawyer.domain.poll.dto.PollStaticsResponseDto; import com.ai.lawyer.domain.poll.dto.PollAgeStaticsDto; +import com.ai.lawyer.global.util.AuthUtil; @Service @Transactional @@ -42,7 +44,6 @@ public class PollServiceImpl implements PollService { private final PollOptionsRepository pollOptionsRepository; private final PollVoteRepository pollVoteRepository; private final PollStaticsRepository pollStaticsRepository; - private final MemberRepository memberRepository; private final PostRepository postRepository; @Override @@ -51,8 +52,7 @@ public PollDto createPoll(PollCreateDto request, Long memberId) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "게시글 ID는 필수입니다."); } validatePollCommon(request.getVoteTitle(), request.getPollOptions(), request.getReservedCloseAt()); - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(memberId); Post post = postRepository.findById(request.getPostId()) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.")); if (post.getPoll() != null) { @@ -117,8 +117,7 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { } PollOptions pollOptions = pollOptionsRepository.findById(pollItemsId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표 항목을 찾을 수 없습니다.")); - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(memberId); // USER 또는 ADMIN만 투표 가능 if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다."); @@ -190,7 +189,7 @@ public PollStaticsResponseDto getPollStatics(Long pollId) { PollAgeStaticsDto.AgeGroupCountDto dto = PollAgeStaticsDto.AgeGroupCountDto.builder() .option(option) .ageGroup(arr[1] != null ? arr[1].toString() : null) - .voteCount(arr[2] != null ? ((Number)arr[2]).longValue() : 0L) + .voteCount(arr[2] != null ? ((Number) arr[2]).longValue() : 0L) .build(); ageGroupMap.computeIfAbsent(pollItemsId, k -> new java.util.ArrayList<>()).add(dto); } @@ -214,7 +213,7 @@ public PollStaticsResponseDto getPollStatics(Long pollId) { PollGenderStaticsDto.GenderCountDto dto = PollGenderStaticsDto.GenderCountDto.builder() .option(option) .gender(arr[1] != null ? arr[1].toString() : null) - .voteCount(arr[2] != null ? ((Number)arr[2]).longValue() : 0L) + .voteCount(arr[2] != null ? ((Number) arr[2]).longValue() : 0L) .build(); genderGroupMap.computeIfAbsent(pollItemsId, k -> new java.util.ArrayList<>()).add(dto); } @@ -246,10 +245,12 @@ public void closePoll(Long pollId) { } @Override - public void deletePoll(Long pollId) { + public void deletePoll(Long pollId, Long memberId) { Poll poll = pollRepository.findById(pollId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다.")); - + if (poll.getPost() == null || !poll.getPost().getMember().getMemberId().equals(memberId)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 삭제할 수 있습니다."); + } // 1. 이 Poll을 참조하는 Post가 있으면 연결 해제 Post post = postRepository.findAll().stream() .filter(p -> p.getPoll() != null && p.getPoll().getPollId().equals(pollId)) @@ -259,7 +260,6 @@ public void deletePoll(Long pollId) { post.setPoll(null); postRepository.save(post); } - // 2. Poll 삭제 pollRepository.deleteById(pollId); } @@ -458,12 +458,12 @@ private PollDto convertToDto(Poll poll, Long memberId, boolean withStatistics) { statics = staticsRaw.stream() .map(arr -> { String gender = arr[1] != null ? arr[1].toString() : null; - Integer age = arr[2] != null ? ((Number)arr[2]).intValue() : null; + Integer age = arr[2] != null ? ((Number) arr[2]).intValue() : null; String ageGroup = getAgeGroup(age); return PollStaticsDto.builder() .gender(gender) .ageGroup(ageGroup) - .voteCount((Long)arr[3]) + .voteCount((Long) arr[3]) .build(); }).toList(); } 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 e8bf31d..e13ec6c 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 @@ -36,16 +36,7 @@ public class PostController { @Operation(summary = "게시글 등록") @PostMapping public ResponseEntity> createPost(@RequestBody PostRequestDto postRequestDto) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - Long memberId; - if (principal instanceof org.springframework.security.core.userdetails.User user) { - memberId = Long.valueOf(user.getUsername()); - } else if (principal instanceof Long) { - memberId = (Long) principal; - } else { - throw new IllegalArgumentException("올바른 회원 ID가 아닙니다"); - } + Long memberId = AuthUtil.getAuthenticatedMemberId(); PostDto created = postService.createPost(postRequestDto, memberId); return ResponseEntity.ok(new ApiResponse<>(201, "게시글이 등록되었습니다.", created)); } @@ -91,43 +82,31 @@ public ResponseEntity>> getPostsByMember(@PathVa @Operation(summary = "게시글 수정") @PutMapping("/{postId}") public ResponseEntity> updatePost(@PathVariable Long postId, @RequestBody PostUpdateDto postUpdateDto) { - Long currentMemberId = AuthUtil.getCurrentMemberId(); - String currentRole = AuthUtil.getCurrentMemberRole(); - PostDetailDto postDetail = postService.getPostDetailById(postId, currentMemberId); + PostDetailDto postDetail = postService.getPostDetailById(postId, AuthUtil.getAuthenticatedMemberId()); Long postOwnerId = postDetail.getPost().getMemberId(); - if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { - return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 수정 가능합니다.", null)); - } + AuthUtil.validateOwnerOrAdmin(postOwnerId); postService.updatePost(postId, postUpdateDto); - PostDetailDto updated = postService.getPostDetailById(postId, currentMemberId); + PostDetailDto updated = postService.getPostDetailById(postId, AuthUtil.getAuthenticatedMemberId()); return ResponseEntity.ok(new ApiResponse<>(200, "게시글이 수정되었습니다.", updated)); } @Operation(summary = "게시글 부분 수정(PATCH)") @PatchMapping("/{postId}") public ResponseEntity> patchUpdatePost(@PathVariable Long postId, @RequestBody PostUpdateDto postUpdateDto) { - Long currentMemberId = AuthUtil.getCurrentMemberId(); - String currentRole = AuthUtil.getCurrentMemberRole(); - PostDetailDto postDetail = postService.getPostDetailById(postId, currentMemberId); + PostDetailDto postDetail = postService.getPostDetailById(postId, AuthUtil.getAuthenticatedMemberId()); Long postOwnerId = postDetail.getPost().getMemberId(); - if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { - return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 수정 가능합니다.", null)); - } + AuthUtil.validateOwnerOrAdmin(postOwnerId); postService.patchUpdatePost(postId, postUpdateDto); - PostDetailDto updated = postService.getPostDetailById(postId, currentMemberId); + PostDetailDto updated = postService.getPostDetailById(postId, AuthUtil.getAuthenticatedMemberId()); return ResponseEntity.ok(new ApiResponse<>(200, "게시글이 수정되었습니다.", updated)); } @Operation(summary = "게시글 삭제") @DeleteMapping("/{postId}") public ResponseEntity> deletePost(@PathVariable Long postId) { - Long currentMemberId = AuthUtil.getCurrentMemberId(); - String currentRole = AuthUtil.getCurrentMemberRole(); - PostDetailDto postDetail = postService.getPostDetailById(postId, currentMemberId); + PostDetailDto postDetail = postService.getPostDetailById(postId, AuthUtil.getAuthenticatedMemberId()); Long postOwnerId = postDetail.getPost().getMemberId(); - if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { - return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 삭제 가능합니다.", null)); - } + AuthUtil.validateOwnerOrAdmin(postOwnerId); postService.deletePost(postId); return ResponseEntity.ok(new ApiResponse<>(200, "게시글이 삭제되었습니다.", null)); } @@ -142,16 +121,7 @@ public ResponseEntity> handleResponseStatusException(ResponseS @Operation(summary = "본인 게시글 단일 조회") @GetMapping("/my/{postId}") public ResponseEntity> getMyPostById(@PathVariable Long postId) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - Long memberId; - if (principal instanceof org.springframework.security.core.userdetails.User user) { - memberId = Long.valueOf(user.getUsername()); - } else if (principal instanceof Long) { - memberId = (Long) principal; - } else { - throw new IllegalArgumentException("올바른 회원 ID가 아닙니다"); - } + Long memberId = AuthUtil.getAuthenticatedMemberId(); PostDto postDto = postService.getMyPostById(postId, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "본인 게시글 단일 조회 성공", postDto)); } @@ -159,16 +129,7 @@ public ResponseEntity> getMyPostById(@PathVariable Long pos @Operation(summary = "본인 게시글 전체 조회") @GetMapping("/my") public ResponseEntity>> getMyPosts() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - Long memberId; - if (principal instanceof org.springframework.security.core.userdetails.User user) { - memberId = Long.valueOf(user.getUsername()); - } else if (principal instanceof Long) { - memberId = (Long) principal; - } else { - throw new IllegalArgumentException("올바른 회원 ID가 아닙니다"); - } + Long memberId = AuthUtil.getAuthenticatedMemberId(); List posts = postService.getMyPosts(memberId); return ResponseEntity.ok(new ApiResponse<>(200, "본인 게시글 전체 조회 성공", posts)); } @@ -176,16 +137,7 @@ public ResponseEntity>> getMyPosts() { @Operation(summary = "게시글+투표 동시 등록") @PostMapping("/createPost") public ResponseEntity> createPostWithPoll(@RequestBody PostWithPollCreateDto dto) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - Long memberId; - if (principal instanceof org.springframework.security.core.userdetails.User user) { - memberId = Long.valueOf(user.getUsername()); - } else if (principal instanceof Long) { - memberId = (Long) principal; - } else { - throw new ResponseStatusException(org.springframework.http.HttpStatus.UNAUTHORIZED, "인증 정보가 올바르지 않습니다."); - } + Long memberId = AuthUtil.getAuthenticatedMemberId(); PostDetailDto result = postService.createPostWithPoll(dto, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "게시글+투표 등록 완료", result)); } @@ -271,4 +223,43 @@ public ResponseEntity> getTopClosedPoll() { PostDto post = postService.getTopPollByStatus(PollDto.PollStatus.CLOSED, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "마감된 투표 Top 1 조회 성공", post)); } -} \ No newline at end of file + + @Operation(summary = "내가 참여한 진행중 투표 게시글 페이징 조회") + @GetMapping("/my/ongoingPaged") + public ResponseEntity> getMyOngoingPostsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Long memberId = AuthUtil.getAuthenticatedMemberId(); + Page posts = postService.getMyOngoingPostsPaged(pageable, memberId); + PostPageDto response = new PostPageDto(posts); + return ResponseEntity.ok(new ApiResponse<>(200, "내가 참여한 진행중 투표 게시글 페이징 조회 성공", response)); + } + + @Operation(summary = "내가 참여한 마감 투표 게시글 페이징 조회") + @GetMapping("/my/closedPaged") + public ResponseEntity> getMyClosedPostsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Long memberId = AuthUtil.getAuthenticatedMemberId(); + Page posts = postService.getMyClosedPostsPaged(pageable, memberId); + PostPageDto response = new PostPageDto(posts); + return ResponseEntity.ok(new ApiResponse<>(200, "내가 참여한 마감 투표 게시글 페이징 조회 성공", response)); + } + + @Operation(summary = "내가 참여한 모든 투표 게시글 페이징 조회") + @GetMapping("/my/votedPaged") + public ResponseEntity> getMyVotedPostsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Long memberId = AuthUtil.getAuthenticatedMemberId(); + Page posts = postService.getMyVotedPostsPaged(pageable, memberId); + PostPageDto response = new PostPageDto(posts); + return ResponseEntity.ok(new ApiResponse<>(200, "내가 참여한 모든 투표 게시글 페이징 조회 성공", response)); + } +} 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 a2ed98b..2988846 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 @@ -36,6 +36,9 @@ public interface PostService { Page getPostsPaged(Pageable pageable, Long memberId); Page getOngoingPostsPaged(Pageable pageable, Long memberId); Page getClosedPostsPaged(Pageable pageable, Long memberId); + Page getMyOngoingPostsPaged(Pageable pageable, Long memberId); + Page getMyClosedPostsPaged(Pageable pageable, Long memberId); + Page getMyVotedPostsPaged(Pageable pageable, Long memberId); // ===== 투표 Top 관련 ===== List getTopNPollsByStatus(PollDto.PollStatus status, int n, Long memberId); 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 cd27542..1e85a4a 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 @@ -2,6 +2,7 @@ import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.member.repositories.MemberRepository; +import com.ai.lawyer.domain.poll.entity.PollVote; import com.ai.lawyer.domain.post.dto.PostDto; import com.ai.lawyer.domain.post.dto.PostDetailDto; import com.ai.lawyer.domain.post.dto.PostRequestDto; @@ -19,6 +20,7 @@ import com.ai.lawyer.domain.poll.entity.PollOptions; import com.ai.lawyer.domain.poll.repository.PollVoteRepository; import com.ai.lawyer.domain.poll.service.PollService; +import com.ai.lawyer.global.util.AuthUtil; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.PageImpl; @@ -62,8 +64,7 @@ public PostDto createPost(PostRequestDto postRequestDto, Long memberId) { postRequestDto.getPostContent() == null || postRequestDto.getPostContent().trim().isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "게시글 제목과 내용은 필수입니다."); } - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(memberId); Post post = Post.builder() .member(member) .postName(postRequestDto.getPostName()) @@ -96,8 +97,7 @@ public PostDetailDto getPostById(Long postId) { @Override public List getPostsByMemberId(Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(memberId); List posts = postRepository.findByMember(member); if (posts.isEmpty()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 회원의 게시글이 없습니다."); @@ -164,8 +164,7 @@ public PostDto getMyPostById(Long postId, Long requesterMemberId) { } public List getMyPosts(Long requesterMemberId) { - Member member = memberRepository.findById(requesterMemberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(requesterMemberId); List posts = postRepository.findByMember(member); // 본인 게시글이 없으면 빈 리스트 반환 return posts.stream() @@ -210,8 +209,7 @@ public PostDetailDto createPostWithPoll(PostWithPollCreateDto dto, Long memberId } var pollDto = dto.getPoll(); pollService.validatePollCreate(pollDto); - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다.")); + Member member = AuthUtil.getMemberOrThrow(memberId); Post post = Post.builder() .member(member) .postName(postDto.getPostName()) @@ -326,4 +324,33 @@ private PostDto convertToDto(Post entity, Long memberId) { .poll(pollDto) .build(); } + + private Page getMyVotedPostsPagedByStatus(Pageable pageable, Long memberId, Poll.PollStatus status) { + List votes = pollVoteRepository.findByMember_MemberId(memberId); + List pollIds = votes.stream().map(v -> v.getPoll().getPollId()).distinct().toList(); + List posts = postRepository.findAll().stream() + .filter(p -> p.getPoll() != null && pollIds.contains(p.getPoll().getPollId()) + && (status == null || p.getPoll().getStatus() == status)) + .toList(); + List postDtos = posts.stream().map(p -> convertToDto(p, memberId)).toList(); + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), postDtos.size()); + List paged = start < end ? postDtos.subList(start, end) : List.of(); + return new org.springframework.data.domain.PageImpl<>(paged, pageable, postDtos.size()); + } + + @Override + public Page getMyVotedPostsPaged(Pageable pageable, Long memberId) { + return getMyVotedPostsPagedByStatus(pageable, memberId, null); + } + + @Override + public Page getMyOngoingPostsPaged(Pageable pageable, Long memberId) { + return getMyVotedPostsPagedByStatus(pageable, memberId, Poll.PollStatus.ONGOING); + } + + @Override + public Page getMyClosedPostsPaged(Pageable pageable, Long memberId) { + return getMyVotedPostsPagedByStatus(pageable, memberId, Poll.PollStatus.CLOSED); + } } diff --git a/backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java b/backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java index b41e07b..aa8a7a6 100644 --- a/backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java +++ b/backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java @@ -3,8 +3,22 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; +import com.ai.lawyer.domain.member.repositories.MemberRepository; +import com.ai.lawyer.domain.member.entity.Member; +@Component public class AuthUtil { + private static MemberRepository memberRepository; + + @Autowired + public AuthUtil(MemberRepository memberRepository) { + AuthUtil.memberRepository = memberRepository; + } + public static Long getCurrentMemberId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { @@ -40,4 +54,29 @@ public static String getCurrentMemberRole() { .orElse(null); } + public static Member getMemberOrThrow(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다")); + } + + public static Long getAuthenticatedMemberId() { + try { + Long memberId = getCurrentMemberId(); + if (memberId == null) { + throw new IllegalArgumentException(); + } + return memberId; + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다"); + } + } + + public static void validateOwnerOrAdmin(Long ownerId) { + Long currentMemberId = getAuthenticatedMemberId(); + String currentRole = getCurrentMemberRole(); + if (!ownerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인 또는 관리자만 수정 가능합니다."); + } + } + } diff --git a/backend/src/test/java/com/ai/lawyer/domain/poll/controller/PollControllerTest.java b/backend/src/test/java/com/ai/lawyer/domain/poll/controller/PollControllerTest.java index 7425db9..ce8b037 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/poll/controller/PollControllerTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/poll/controller/PollControllerTest.java @@ -114,7 +114,7 @@ void t4() throws Exception { void t5() throws Exception { PollDto pollDto = PollDto.builder().pollId(1L).postId(1L).build(); Mockito.when(pollService.getPoll(Mockito.eq(1L), Mockito.anyLong())).thenReturn(pollDto); - Mockito.doNothing().when(pollService).deletePoll(Mockito.anyLong()); + Mockito.doNothing().when(pollService).deletePoll(Mockito.anyLong(), Mockito.anyLong()); mockMvc.perform( org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete("/api/polls/1") diff --git a/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollServiceTest.java b/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollServiceTest.java index 1f986f6..888b2b1 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollServiceTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/poll/service/PollServiceTest.java @@ -72,9 +72,9 @@ void t5() { @Test @DisplayName("투표 삭제") void t6() { - Mockito.doNothing().when(pollService).deletePoll(Mockito.anyLong()); - pollService.deletePoll(1L); - Mockito.verify(pollService).deletePoll(1L); + Mockito.doNothing().when(pollService).deletePoll(Mockito.anyLong(), Mockito.anyLong()); + pollService.deletePoll(1L, 1L); + Mockito.verify(pollService).deletePoll(1L, 1L); } @Test diff --git a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java index 22d0fbc..05ef3d0 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java @@ -187,4 +187,81 @@ void t7() throws Exception { .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.totalPages").value(1)) .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.totalElements").value(1)); } + + @Test + @DisplayName("게시글 간편 전체 조회") + void t8() throws Exception { + List posts = java.util.Collections.emptyList(); + Mockito.when(postService.getAllSimplePosts()).thenReturn(posts); + mockMvc.perform(get("/api/posts/simplePost") + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result").isArray()); + } + + @Test + @DisplayName("본인 게시글 단일 조회") + void t9() throws Exception { + com.ai.lawyer.domain.post.dto.PostDto postDto = com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("테스트 제목").build(); + Mockito.when(postService.getMyPostById(Mockito.eq(1L), Mockito.anyLong())).thenReturn(postDto); + mockMvc.perform(get("/api/posts/my/1") + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.postId").value(1L)); + } + + @Test + @DisplayName("본인 게시글 전체 조회") + void t10() throws Exception { + List posts = java.util.Collections.emptyList(); + Mockito.when(postService.getMyPosts(Mockito.anyLong())).thenReturn(posts); + mockMvc.perform(get("/api/posts/my") + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result").isArray()); + } + + @Test + @DisplayName("게시글+투표 동시 등록") + void t11() throws Exception { + com.ai.lawyer.domain.post.dto.PostDetailDto result = com.ai.lawyer.domain.post.dto.PostDetailDto.builder().post( + com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("테스트 제목").build() + ).build(); + com.ai.lawyer.domain.post.dto.PostWithPollCreateDto dto = com.ai.lawyer.domain.post.dto.PostWithPollCreateDto.builder().build(); + Mockito.when(postService.createPostWithPoll(Mockito.any(), Mockito.anyLong())).thenReturn(result); + mockMvc.perform(post("/api/posts/createPost") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto)) + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.post.postId").value(1L)); + } + + @Test + @DisplayName("진행중 투표 게시글 페이징 조회") + void t12() throws Exception { + org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(0, 10); + org.springframework.data.domain.PageImpl page = new org.springframework.data.domain.PageImpl<>(java.util.List.of(), pageable, 0); + Mockito.when(postService.getOngoingPostsPaged(Mockito.any(), Mockito.anyLong())).thenReturn(page); + mockMvc.perform(get("/api/posts/ongoingPaged") + .param("page", "0") + .param("size", "10") + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.content").isArray()); + } + + @Test + @DisplayName("마감 투표 게시글 페이징 조회") + void t13() throws Exception { + org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(0, 10); + org.springframework.data.domain.PageImpl page = new org.springframework.data.domain.PageImpl<>(java.util.List.of(), pageable, 0); + Mockito.when(postService.getClosedPostsPaged(Mockito.any(), Mockito.anyLong())).thenReturn(page); + mockMvc.perform(get("/api/posts/closedPaged") + .param("page", "0") + .param("size", "10") + .cookie(new Cookie("accessToken", "valid-access-token"))) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.result.content").isArray()); + } } \ No newline at end of file