diff --git a/backend/src/main/java/com/ai/lawyer/domain/lawWord/service/LawWordService.java b/backend/src/main/java/com/ai/lawyer/domain/lawWord/service/LawWordService.java index d07ea9e8..2f86f6d3 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/lawWord/service/LawWordService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/lawWord/service/LawWordService.java @@ -75,7 +75,7 @@ private String fetchAndSaveDefinition(String word) { private String fetchAndSaveDefinitionV2(String word) { try { - String url = buildKoreanDictApiUrl(word); + String url = buildApiUrlV2(word); // WebClient 호출 (동기 방식) String json = webClient.get() @@ -84,7 +84,7 @@ private String fetchAndSaveDefinitionV2(String word) { .bodyToMono(String.class) .block(); - String combinedDefinitions = extractTop3DefinitionsFromJson(json); + String combinedDefinitions = extractTop3DefinitionsFromJson(json, word); saveDefinition(word, combinedDefinitions); return combinedDefinitions; @@ -105,7 +105,7 @@ private String buildApiUrl(String word) { return API_BASE_URL + "?OC=" + API_OC + "&target=lstrm&type=JSON&query=" + word; } - private String buildKoreanDictApiUrl(String word) { + private String buildApiUrlV2(String word) { return UriComponentsBuilder.fromHttpUrl(KOREAN_DICT_API_BASE_URL) .queryParam("key", API_KEY) .queryParam("req_type", "json") @@ -114,9 +114,9 @@ private String buildKoreanDictApiUrl(String word) { .queryParam("sort", "dict") .queryParam("start", "1") .queryParam("num", "10") - .queryParam("advanced", "y") - .queryParam("type4", "all") - .queryParam("cat", "23") +// .queryParam("advanced", "y") +// .queryParam("type4", "all") +// .queryParam("cat", "23") .build() .toUriString(); } @@ -136,39 +136,72 @@ private String extractDefinitionFromJson(String json) throws JsonProcessingExcep } } - private String extractTop3DefinitionsFromJson(String json) throws JsonProcessingException { + private String extractTop3DefinitionsFromJson(String json, String requestedWord) throws JsonProcessingException { JsonNode rootNode = objectMapper.readTree(json); + JsonNode channelNode = rootNode.path("channel"); - // channel > item 배열에서 아이템들 추출 - JsonNode itemsNode = rootNode.path("channel").path("item"); + // 1. total이 0이면 '찾을수 없는 단어입니다' 리턴 + int total = channelNode.path("total").asInt(0); + if (total == 0) { + return "찾을수 없는 단어입니다"; + } + JsonNode itemsNode = channelNode.path("item"); if (!itemsNode.isArray() || itemsNode.size() == 0) { - throw new RuntimeException("검색 결과가 없습니다."); + return "찾을수 없는 단어입니다"; + } + + // 2. 클라이언트가 요청한 단어와 정확히 일치하는 item만 필터링 + List matchingItems = new ArrayList<>(); + for (JsonNode item : itemsNode) { + String itemWord = item.path("word").asText(); + if (requestedWord.equals(itemWord)) { + matchingItems.add(item); + } + } + + if (matchingItems.isEmpty()) { + return "찾을수 없는 단어입니다"; + } + + // 3. 법률 카테고리 우선순위 적용 + List definitions = extractDefinitionsWithPriority(matchingItems); + + if (definitions.isEmpty()) { + return "찾을수 없는 단어입니다"; } - List definitions = new ArrayList<>(); + // 같은 word면 개수 제한 없이 모든 definition 반환 + return String.join("\n", definitions); + } - // 최대 3개의 definition 추출 - for (int i = 0; i < Math.min(itemsNode.size(), 3); i++) { - JsonNode item = itemsNode.get(i); + private List extractDefinitionsWithPriority(List matchingItems) { + List legalDefinitions = new ArrayList<>(); // 법률 카테고리 + List allDefinitions = new ArrayList<>(); // 모든 카테고리 + + for (JsonNode item : matchingItems) { JsonNode senseNode = item.path("sense"); - if (senseNode.isArray() && senseNode.size() > 0) { - JsonNode firstSense = senseNode.get(0); - String definition = firstSense.path("definition").asText(); + if (senseNode.isArray()) { + for (JsonNode sense : senseNode) { + String definition = sense.path("definition").asText(); + String cat = sense.path("cat").asText(""); + + if (definition != null && !definition.trim().isEmpty()) { + String cleanDefinition = definition.trim(); + allDefinitions.add(cleanDefinition); - if (definition != null && !definition.trim().isEmpty()) { - definitions.add(definition.trim()); + // cat이 "법률"인 경우 별도로 수집 + if ("법률".equals(cat)) { + legalDefinitions.add(cleanDefinition); + } + } } } } - if (definitions.isEmpty()) { - throw new RuntimeException("검색 결과에서 정의를 찾을 수 없습니다."); - } - - // 줄바꿈으로 연결하여 하나의 문자열로 만들기 - return String.join("\n", definitions); + // 법률 카테고리가 있으면 법률만, 없으면 모든 카테고리 반환 + return legalDefinitions.isEmpty() ? allDefinitions : legalDefinitions; } private void saveDefinition(String word, String definition) { 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 c8d0e052..66e9ec99 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 @@ -1,18 +1,17 @@ package com.ai.lawyer.domain.poll.controller; -import com.ai.lawyer.domain.poll.dto.PollCreateDto; -import com.ai.lawyer.domain.poll.dto.PollDto; -import com.ai.lawyer.domain.poll.dto.PollStaticsResponseDto; -import com.ai.lawyer.domain.poll.dto.PollVoteDto; +import com.ai.lawyer.domain.poll.dto.*; import com.ai.lawyer.domain.poll.entity.PollVote; import com.ai.lawyer.domain.poll.entity.PollOptions; import com.ai.lawyer.domain.poll.service.PollService; import com.ai.lawyer.domain.post.dto.PostDetailDto; import com.ai.lawyer.domain.post.service.PostService; import com.ai.lawyer.global.response.ApiResponse; +import com.ai.lawyer.global.util.AuthUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -26,6 +25,7 @@ @RestController @RequestMapping("/api/polls") @RequiredArgsConstructor +@Slf4j public class PollController { private final PollService pollService; @@ -34,17 +34,12 @@ public class PollController { @Operation(summary = "투표 단일 조회") @GetMapping("/{pollId}") public ResponseEntity> getPoll(@PathVariable Long pollId) { - PollDto poll = pollService.getPoll(pollId); + Long memberId = AuthUtil.getCurrentMemberId(); + log.info("PollController getPoll: memberId={}", memberId); + PollDto poll = pollService.getPoll(pollId, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "투표 단일 조회 성공", poll)); } - @Operation(summary = "투표 옵션 목록 조회") - @GetMapping("/{pollId}/options") - public ResponseEntity>> getPollOptions(@PathVariable Long pollId) { - List options = pollService.getPollOptions(pollId); - return ResponseEntity.ok(new ApiResponse<>(200, "투표 옵션 목록 조회 성공", options)); - } - @Operation(summary = "투표하기") @PostMapping("/{pollId}/vote") public ResponseEntity> vote(@PathVariable Long pollId, @RequestParam Long pollItemsId) { @@ -68,9 +63,22 @@ public ResponseEntity> closePoll(@PathVariable Long pollId) { return ResponseEntity.ok(new ApiResponse<>(200, "투표가 종료되었습니다.", null)); } + @Operation(summary = "투표 수정") + @PutMapping("/{pollId}") + public ResponseEntity> updatePoll(@PathVariable Long pollId, @RequestBody PollUpdateDto pollUpdateDto) { + Long currentMemberId = AuthUtil.getCurrentMemberId(); + PollDto updated = pollService.updatePoll(pollId, pollUpdateDto, currentMemberId); + return ResponseEntity.ok(new ApiResponse<>(200, "투표가 수정되었습니다.", updated)); + } + @Operation(summary = "투표 삭제") @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); return ResponseEntity.ok(new ApiResponse<>(200, "투표가 삭제되었습니다.", null)); } @@ -78,14 +86,16 @@ public ResponseEntity> deletePoll(@PathVariable Long pollId) { @Operation(summary = "진행중인 투표 Top 1 조회") @GetMapping("/top/ongoing") public ResponseEntity> getTopOngoingPoll() { - PollDto poll = pollService.getTopPollByStatus(PollDto.PollStatus.ONGOING); + Long memberId = AuthUtil.getCurrentMemberId(); + PollDto poll = pollService.getTopPollByStatus(PollDto.PollStatus.ONGOING, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "진행중인 투표 Top 1 조회 성공", poll)); } @Operation(summary = "종료된 투표 Top 1 조회") @GetMapping("/top/closed") public ResponseEntity> getTopClosedPoll() { - PollDto poll = pollService.getTopPollByStatus(PollDto.PollStatus.CLOSED); + Long memberId = AuthUtil.getCurrentMemberId(); + PollDto poll = pollService.getTopPollByStatus(PollDto.PollStatus.CLOSED, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "종료된 투표 Top 1 조회 성공", poll)); } @@ -112,42 +122,37 @@ public ResponseEntity> createPoll(@RequestBody PollCreateDt return ResponseEntity.ok(new ApiResponse<>(201, "투표가 생성되었습니다.", created)); } - @Operation(summary = "투표 수정") - @PutMapping("/{pollId}") - public ResponseEntity> updatePoll(@PathVariable Long pollId, @RequestBody com.ai.lawyer.domain.poll.dto.PollUpdateDto pollUpdateDto) { - PollDto updated = pollService.updatePoll(pollId, pollUpdateDto); - return ResponseEntity.ok(new ApiResponse<>(200, "투표가 수정되었습니다.", updated)); - } - @Operation(summary = "진행중인 투표 전체 목록 조회") @GetMapping("/ongoing") public ResponseEntity>> getOngoingPolls() { - List polls = pollService.getPollsByStatus(PollDto.PollStatus.ONGOING); + Long memberId = AuthUtil.getCurrentMemberId(); + List polls = pollService.getPollsByStatus(PollDto.PollStatus.ONGOING, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "진행중인 투표 전체 목록 조회 성공", polls)); } @Operation(summary = "종료된 투표 전체 목록 조회") @GetMapping("/closed") public ResponseEntity>> getClosedPolls() { - List polls = pollService.getPollsByStatus(PollDto.PollStatus.CLOSED); + Long memberId = AuthUtil.getCurrentMemberId(); + List polls = pollService.getPollsByStatus(PollDto.PollStatus.CLOSED, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "종료된 투표 전체 목록 조회 성공", polls)); } - @Operation(summary = "종료된 투표 Top N 조회") - @GetMapping("/top/closed-list") //검색조건 : pi/polls/top/closed-list?size=3 - public ResponseEntity>> getTopClosedPolls(@RequestParam(defaultValue = "3") int size) { - List polls = pollService.getTopNPollsByStatus(PollDto.PollStatus.CLOSED, size); - String message = String.format("종료된 투표 Top %d 조회 성공", size); - return ResponseEntity.ok(new ApiResponse<>(200, message, polls)); - } - - @Operation(summary = "진행중인 투표 Top N 조회") - @GetMapping("/top/ongoing-list") //검색조건 : api/polls/top/ongoing-list?size=3 - public ResponseEntity>> getTopOngoingPolls(@RequestParam(defaultValue = "3") int size) { - List polls = pollService.getTopNPollsByStatus(PollDto.PollStatus.ONGOING, size); - String message = String.format("진행중인 투표 Top %d 조회 성공", size); - return ResponseEntity.ok(new ApiResponse<>(200, message, polls)); - } +// @Operation(summary = "종료된 투표 Top N 조회") +// @GetMapping("/top/closed-list") //검색조건 : pi/polls/top/closed-list?size=3 +// public ResponseEntity>> getTopClosedPolls(@RequestParam(defaultValue = "3") int size) { +// List polls = pollService.getTopNPollsByStatus(PollDto.PollStatus.CLOSED, size); +// String message = String.format("종료된 투표 Top %d 조회 성공", size); +// return ResponseEntity.ok(new ApiResponse<>(200, message, polls)); +// } +// +// @Operation(summary = "진행중인 투표 Top N 조회") +// @GetMapping("/top/ongoing-list") //검색조건 : api/polls/top/ongoing-list?size=3 +// public ResponseEntity>> getTopOngoingPolls(@RequestParam(defaultValue = "3") int size) { +// List polls = pollService.getTopNPollsByStatus(PollDto.PollStatus.ONGOING, size); +// String message = String.format("진행중인 투표 Top %d 조회 성공", size); +// return ResponseEntity.ok(new ApiResponse<>(200, message, polls)); +// } @Operation(summary = "index(순번)로 투표하기") @PostMapping("/{pollId}/voting") diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollOptionDto.java b/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollOptionDto.java index 12eb0240..669b2d10 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollOptionDto.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollOptionDto.java @@ -12,4 +12,5 @@ public class PollOptionDto { private Long voteCount; private java.util.List statics; private int pollOptionIndex; + private boolean voted; // 해당 옵션에 투표했는지 여부 } diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollVoteDto.java b/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollVoteDto.java index ece3dad8..3f3ffa3a 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollVoteDto.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/dto/PollVoteDto.java @@ -15,5 +15,5 @@ public class PollVoteDto { private Long pollItemsId; private Long memberId; private Long voteCount; + private String message; } - 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 8cd5b537..4ac94772 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,6 +3,10 @@ import com.ai.lawyer.domain.poll.entity.PollVote; import org.springframework.data.jpa.repository.JpaRepository; +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); } - diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepositoryCustom.java b/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepositoryCustom.java index 80834e8a..d805765f 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepositoryCustom.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepositoryCustom.java @@ -14,4 +14,3 @@ public interface PollVoteRepositoryCustom { List getOptionAgeStatics(Long pollId); List getOptionGenderStatics(Long pollId); } - 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 95b230c4..97d79daa 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 @@ -14,12 +14,12 @@ public interface PollService { // ===== 조회 관련 ===== - PollDto getPoll(Long pollId); - PollDto getPollWithStatistics(Long pollId); + PollDto getPoll(Long pollId, Long memberId); + PollDto getPollWithStatistics(Long pollId, Long memberId); List getPollOptions(Long pollId); - List getPollsByStatus(PollDto.PollStatus status); - PollDto getTopPollByStatus(PollDto.PollStatus status); - List getTopNPollsByStatus(PollDto.PollStatus status, int n); + List getPollsByStatus(PollDto.PollStatus status, Long memberId); + PollDto getTopPollByStatus(PollDto.PollStatus status, Long memberId); + List getTopNPollsByStatus(PollDto.PollStatus status, int n, Long memberId); // ===== 통계 관련 ===== PollStaticsResponseDto getPollStatics(Long pollId); @@ -31,7 +31,7 @@ public interface PollService { // ===== 생성/수정/삭제 관련 ===== PollDto createPoll(PollCreateDto request, Long memberId); - PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto); + PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto, Long memberId); void patchUpdatePoll(Long pollId, PollUpdateDto pollUpdateDto); void closePoll(Long pollId); void deletePoll(Long pollId); 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 8ffb40ca..32ffb013 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 @@ -8,6 +8,7 @@ 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.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +34,7 @@ @Service @Transactional +@Slf4j @RequiredArgsConstructor public class PollServiceImpl implements PollService { @@ -76,7 +78,7 @@ public PollDto createPoll(PollCreateDto request, Long memberId) { .build(); pollOptionsRepository.save(option); } - return convertToDto(savedPoll); + return convertToDto(savedPoll, memberId, false); } catch (ResponseStatusException e) { throw e; } catch (Exception e) { @@ -85,18 +87,14 @@ public PollDto createPoll(PollCreateDto request, Long memberId) { } @Override - public PollDto getPoll(Long pollId) { + public PollDto getPoll(Long pollId, Long memberId) { Poll poll = pollRepository.findById(pollId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다.")); - autoClose(poll); - if (poll.getStatus() == Poll.PollStatus.CLOSED) { - return getPollWithStatistics(pollId); - } - return convertToDto(poll); + return convertToDto(poll, memberId, false); } @Override - public List getPollsByStatus(PollDto.PollStatus status) { + public List getPollsByStatus(PollDto.PollStatus status, Long memberId) { List polls = pollRepository.findAll(); for (Poll poll : polls) { autoClose(poll); @@ -104,8 +102,8 @@ public List getPollsByStatus(PollDto.PollStatus status) { List pollDtos = polls.stream() .filter(p -> p.getStatus().name().equals(status.name())) .map(p -> status == PollDto.PollStatus.CLOSED - ? getPollWithStatistics(p.getPollId()) - : convertToDto(p)) + ? getPollWithStatistics(p.getPollId(), memberId) + : getPoll(p.getPollId(), memberId)) .toList(); return pollDtos; } @@ -125,19 +123,38 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다."); } - // 중복 투표 방지 - /* - if (pollVoteRepository.existsByPoll_PollIdAndMember_MemberId(pollId, memberId)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 이 투표에 참여하셨습니다."); + // 기존 투표 내역 조회 + var existingVoteOpt = pollVoteRepository.findByMember_MemberIdAndPoll_PollId(memberId, pollId); + if (existingVoteOpt.isPresent()) { + PollVote existingVote = existingVoteOpt.get(); + if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다."); + } else { + pollVoteRepository.deleteByMember_MemberIdAndPoll_PollId(memberId, pollId); + PollVote pollVote = PollVote.builder() + .poll(poll) + .pollOptions(pollOptions) + .member(member) + .build(); + PollVote savedVote = pollVoteRepository.save(pollVote); + Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId); + return PollVoteDto.builder() + .pollVoteId(savedVote.getPollVoteId()) + .pollId(pollId) + .pollItemsId(pollItemsId) + .memberId(memberId) + .voteCount(voteCount) + .message("투표 항목을 변경하였습니다.") + .build(); + } } - */ + // 기존 투표 내역이 없으면 정상 투표 PollVote pollVote = PollVote.builder() .poll(poll) .pollOptions(pollOptions) .member(member) .build(); PollVote savedVote = pollVoteRepository.save(pollVote); - // 해당 옵션의 투표 수 계산 Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId); return PollVoteDto.builder() .pollVoteId(savedVote.getPollVoteId()) @@ -145,6 +162,7 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { .pollItemsId(pollItemsId) .memberId(memberId) .voteCount(voteCount) + .message("투표가 완료되었습니다.") .build(); } @@ -247,7 +265,7 @@ public void deletePoll(Long pollId) { } @Override - public PollDto getTopPollByStatus(PollDto.PollStatus status) { + public PollDto getTopPollByStatus(PollDto.PollStatus status, Long memberId) { List result = pollVoteRepository.findTopPollByStatus(Poll.PollStatus.valueOf(status.name())); if (result.isEmpty()) { // 종료된 투표가 없으면 빈 PollDto 반환 @@ -263,11 +281,11 @@ public PollDto getTopPollByStatus(PollDto.PollStatus status) { .build(); } Long pollId = (Long) result.get(0)[0]; - return getPoll(pollId); + return getPoll(pollId, memberId); } @Override - public List getTopNPollsByStatus(PollDto.PollStatus status, int n) { + public List getTopNPollsByStatus(PollDto.PollStatus status, int n, Long memberId) { Pageable pageable = org.springframework.data.domain.PageRequest.of(0, n); List result = pollVoteRepository.findTopNPollByStatus( com.ai.lawyer.domain.poll.entity.Poll.PollStatus.valueOf(status.name()), pageable); @@ -275,8 +293,8 @@ public List getTopNPollsByStatus(PollDto.PollStatus status, int n) { for (Object[] row : result) { Long pollId = (Long) row[0]; pollDtos.add(status == PollDto.PollStatus.CLOSED - ? getPollWithStatistics(pollId) - : getPoll(pollId)); + ? getPollWithStatistics(pollId, memberId) + : getPoll(pollId, memberId)); } return pollDtos; } @@ -297,9 +315,12 @@ public Long getVoteCountByPostId(Long postId) { @Override - public PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto) { + public PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto, Long memberId) { Poll poll = pollRepository.findById(pollId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "수정할 투표를 찾을 수 없습니다.")); + if (!poll.getPost().getMember().getMemberId().equals(memberId)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 수정할 수 있습니다."); + } if (getVoteCountByPollId(pollId) > 0) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표가 진행된 투표는 수정할 수 없습니다."); } @@ -353,7 +374,7 @@ public PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto) { System.out.println("poll에 저장된 reservedCloseAt 값: " + poll.getReservedCloseAt()); } Poll updated = pollRepository.save(poll); - return convertToDto(updated); + return convertToDto(updated, null, false); } @Override @@ -414,21 +435,27 @@ public void patchUpdatePoll(Long pollId, PollUpdateDto pollUpdateDto) { } @Override - public PollDto getPollWithStatistics(Long pollId) { + public PollDto getPollWithStatistics(Long pollId, Long memberId) { Poll poll = pollRepository.findById(pollId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다.")); + return convertToDto(poll, memberId, true); + } + + private PollDto convertToDto(Poll poll, Long memberId, boolean withStatistics) { List options = pollOptionsRepository.findByPoll_PollId(poll.getPollId()); - List optionIds = options.stream().map(PollOptions::getPollItemsId).toList(); + List optionDtos = new ArrayList<>(); Long totalVoteCount = pollVoteRepository.countByPollId(poll.getPollId()); - List optionDtos; - if (poll.getStatus() == Poll.PollStatus.CLOSED && !optionIds.isEmpty()) { - List staticsRaw = pollVoteRepository.countStaticsByPollOptionIds(optionIds); - optionDtos = new ArrayList<>(); - for (int i = 0; i < options.size(); i++) { - PollOptions option = options.get(i); - Long voteCount = pollVoteRepository.countByPollOptionId(option.getPollItemsId()); - List statics = staticsRaw.stream() - .filter(arr -> ((Long)arr[0]).equals(option.getPollItemsId())) + for (int i = 0; i < options.size(); i++) { + PollOptions option = options.get(i); + Long voteCount = pollVoteRepository.countByPollOptionId(option.getPollItemsId()); + boolean voted = false; + if (memberId != null) { + voted = pollVoteRepository.findByMember_MemberIdAndPollOptions_PollItemsId(memberId, option.getPollItemsId()).isPresent(); + } + List statics = null; + if (withStatistics && poll.getStatus() == Poll.PollStatus.CLOSED) { + List staticsRaw = pollVoteRepository.countStaticsByPollOptionIds(List.of(option.getPollItemsId())); + statics = staticsRaw.stream() .map(arr -> { String gender = arr[1] != null ? arr[1].toString() : null; Integer age = arr[2] != null ? ((Number)arr[2]).intValue() : null; @@ -438,55 +465,15 @@ public PollDto getPollWithStatistics(Long pollId) { .ageGroup(ageGroup) .voteCount((Long)arr[3]) .build(); - }) - .toList(); - optionDtos.add(PollOptionDto.builder() - .pollItemsId(option.getPollItemsId()) - .content(option.getOption()) - .voteCount(voteCount) - .statics(statics) - .pollOptionIndex(i + 1) - .build()); - } - } else { - optionDtos = new ArrayList<>(); - for (int i = 0; i < options.size(); i++) { - PollOptions option = options.get(i); - Long voteCount = pollVoteRepository.countByPollOptionId(option.getPollItemsId()); - optionDtos.add(PollOptionDto.builder() - .pollItemsId(option.getPollItemsId()) - .content(option.getOption()) - .voteCount(voteCount) - .statics(null) - .pollOptionIndex(i + 1) - .build()); + }).toList(); } - } - return PollDto.builder() - .pollId(poll.getPollId()) - .postId(poll.getPost() != null ? poll.getPost().getPostId() : null) - .voteTitle(poll.getVoteTitle()) - .status(PollDto.PollStatus.valueOf(poll.getStatus().name())) - .createdAt(poll.getCreatedAt()) - .closedAt(poll.getClosedAt()) - .pollOptions(optionDtos) - .totalVoteCount(totalVoteCount) - .build(); - } - - private PollDto convertToDto(Poll poll) { - List options = pollOptionsRepository.findByPoll_PollId(poll.getPollId()); - List optionDtos = new ArrayList<>(); - Long totalVoteCount = pollVoteRepository.countByPollId(poll.getPollId()); - for (int i = 0; i < options.size(); i++) { - PollOptions option = options.get(i); - Long voteCount = pollVoteRepository.countByPollOptionId(option.getPollItemsId()); optionDtos.add(PollOptionDto.builder() .pollItemsId(option.getPollItemsId()) .content(option.getOption()) .voteCount(voteCount) - .statics(null) + .statics(statics) .pollOptionIndex(i + 1) + .voted(voted) .build()); } LocalDateTime expectedCloseAt = poll.getReservedCloseAt() != null ? poll.getReservedCloseAt() : poll.getCreatedAt().plusDays(7); @@ -568,4 +555,4 @@ public void validatePollCreate(PollForPostDto dto) { public void validatePollCreate(PollCreateDto dto) { validatePollCommon(dto.getVoteTitle(), dto.getPollOptions(), dto.getReservedCloseAt()); } -} \ No newline at end of file +} 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 5078b888..e8bf31dd 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 @@ -1,10 +1,13 @@ package com.ai.lawyer.domain.post.controller; +import com.ai.lawyer.domain.poll.dto.PollDto; import com.ai.lawyer.domain.post.dto.*; import com.ai.lawyer.domain.post.service.PostService; +import com.ai.lawyer.domain.poll.dto.PollDto; import com.ai.lawyer.domain.member.repositories.MemberRepository; import com.ai.lawyer.global.jwt.TokenProvider; import com.ai.lawyer.global.response.ApiResponse; +import com.ai.lawyer.global.util.AuthUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -47,16 +50,17 @@ public ResponseEntity> createPost(@RequestBody PostRequestD return ResponseEntity.ok(new ApiResponse<>(201, "게시글이 등록되었습니다.", created)); } - @PostMapping("/postdev") - public ResponseEntity> createPostDev(@RequestBody PostRequestDto postRequestDto, @RequestParam Long memberId) { - PostDto created = postService.createPost(postRequestDto, memberId); - return ResponseEntity.ok(new ApiResponse<>(201, "[DEV] 게시글이 등록되었습니다.", created)); - } +// @PostMapping("/postdev") +// public ResponseEntity> createPostDev(@RequestBody PostRequestDto postRequestDto, @RequestParam Long memberId) { +// PostDto created = postService.createPost(postRequestDto, memberId); +// return ResponseEntity.ok(new ApiResponse<>(201, "[DEV] 게시글이 등록되었습니다.", created)); +// } @Operation(summary = "게시글 전체 조회") @GetMapping("") public ResponseEntity>> getAllPosts() { - List posts = postService.getAllPosts(); + Long memberId = AuthUtil.getCurrentMemberId(); + List posts = postService.getAllPosts(memberId); return ResponseEntity.ok(new ApiResponse<>(200, "게시글 전체 조회 성공", posts)); } @@ -70,7 +74,8 @@ public ResponseEntity>> getAllSimplePosts() { @Operation(summary = "게시글 단일 조회") @GetMapping("/{postId}") public ResponseEntity> getPostById(@PathVariable Long postId) { - PostDetailDto postDto = postService.getPostById(postId); + Long memberId = AuthUtil.getCurrentMemberId(); + PostDetailDto postDto = postService.getPostDetailById(postId, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "게시글 단일 조회 성공", postDto)); } @@ -78,7 +83,7 @@ public ResponseEntity> getPostById(@PathVariable Long @GetMapping("/member/{memberId}") public ResponseEntity>> getPostsByMember(@PathVariable Long memberId) { List posts = postService.getPostsByMemberId(memberId).stream() - .map(postDto -> postService.getPostDetailById(postDto.getPostId())) + .map(postDto -> postService.getPostDetailById(postDto.getPostId(), AuthUtil.getCurrentMemberId())) .toList(); return ResponseEntity.ok(new ApiResponse<>(200, "회원별 게시글 목록 조회 성공", posts)); } @@ -86,22 +91,43 @@ 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); + Long postOwnerId = postDetail.getPost().getMemberId(); + if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { + return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 수정 가능합니다.", null)); + } postService.updatePost(postId, postUpdateDto); - PostDetailDto updated = postService.getPostDetailById(postId); + PostDetailDto updated = postService.getPostDetailById(postId, currentMemberId); 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); + Long postOwnerId = postDetail.getPost().getMemberId(); + if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { + return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 수정 가능합니다.", null)); + } postService.patchUpdatePost(postId, postUpdateDto); - PostDetailDto updated = postService.getPostDetailById(postId); + PostDetailDto updated = postService.getPostDetailById(postId, currentMemberId); 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); + Long postOwnerId = postDetail.getPost().getMemberId(); + if (!postOwnerId.equals(currentMemberId) && !"ADMIN".equals(currentRole)) { + return ResponseEntity.status(403).body(new ApiResponse<>(403, "본인 또는 관리자만 삭제 가능합니다.", null)); + } postService.deletePost(postId); return ResponseEntity.ok(new ApiResponse<>(200, "게시글이 삭제되었습니다.", null)); } @@ -171,7 +197,8 @@ public ResponseEntity> getPostsPaged( @RequestParam(defaultValue = "10") int size ) { Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - Page posts = postService.getPostsPaged(pageable); + Long memberId = AuthUtil.getCurrentMemberId(); + Page posts = postService.getPostsPaged(pageable, memberId); if (posts == null) { posts = new org.springframework.data.domain.PageImpl<>(java.util.Collections.emptyList(), pageable, 0); } @@ -186,7 +213,8 @@ public ResponseEntity> getOngoingPostsPaged( @RequestParam(defaultValue = "10") int size ) { Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - Page posts = postService.getOngoingPostsPaged(pageable); + Long memberId = AuthUtil.getCurrentMemberId(); + Page posts = postService.getOngoingPostsPaged(pageable, memberId); if (posts == null) { posts = new org.springframework.data.domain.PageImpl<>(java.util.Collections.emptyList(), pageable, 0); } @@ -201,11 +229,46 @@ public ResponseEntity> getClosedPostsPaged( @RequestParam(defaultValue = "10") int size ) { Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - Page posts = postService.getClosedPostsPaged(pageable); + Long memberId = AuthUtil.getCurrentMemberId(); + Page posts = postService.getClosedPostsPaged(pageable, memberId); 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 = "진행중인 투표 Top N 조회") + @GetMapping("/top/ongoingList") + public ResponseEntity>> getTopNOngoingPolls(@RequestParam(defaultValue = "3") int size) { + Long memberId = AuthUtil.getCurrentMemberId(); + List posts = postService.getTopNPollsByStatus(PollDto.PollStatus.ONGOING, size, memberId); + String message = String.format("진행중인 투표 Top %d 조회 성공", size); + return ResponseEntity.ok(new ApiResponse<>(200, message, posts)); + } + + @Operation(summary = "마감된 투표 Top N 조회") + @GetMapping("/top/closedList") + public ResponseEntity>> getTopNClosedPolls(@RequestParam(defaultValue = "3") int size) { + Long memberId = AuthUtil.getCurrentMemberId(); + List posts = postService.getTopNPollsByStatus(PollDto.PollStatus.CLOSED, size, memberId); + String message = String.format("종료된 투표 Top %d 조회 성공", size); + return ResponseEntity.ok(new ApiResponse<>(200, message, posts)); + } + + @Operation(summary = "진행중인 투표 Top 1 조회") + @GetMapping("/top/ongoing") + public ResponseEntity> getTopOngoingPoll() { + Long memberId = AuthUtil.getCurrentMemberId(); + PostDto post = postService.getTopPollByStatus(PollDto.PollStatus.ONGOING, memberId); + return ResponseEntity.ok(new ApiResponse<>(200, "진행중인 투표 Top 1 조회 성공", post)); + } + + @Operation(summary = "마감된 투표 Top 1 조회") + @GetMapping("/top/closed") + public ResponseEntity> getTopClosedPoll() { + Long memberId = AuthUtil.getCurrentMemberId(); + PostDto post = postService.getTopPollByStatus(PollDto.PollStatus.CLOSED, memberId); + return ResponseEntity.ok(new ApiResponse<>(200, "마감된 투표 Top 1 조회 성공", post)); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostDto.java b/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostDto.java index d72b762b..b8161e6e 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostDto.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/dto/PostDto.java @@ -9,7 +9,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder(toBuilder = true) -public class PostDto { +public class PostDto { private Long postId; private Long memberId; 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 67e4dabb..a2ed98b6 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 @@ -7,6 +7,7 @@ import com.ai.lawyer.domain.post.dto.PostUpdateDto; import com.ai.lawyer.domain.post.dto.PostWithPollCreateDto; import com.ai.lawyer.domain.post.dto.PostSimpleDto; +import com.ai.lawyer.domain.poll.dto.PollDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -15,8 +16,8 @@ public interface PostService { // ===== 조회 관련 ===== PostDetailDto getPostById(Long postId); - PostDetailDto getPostDetailById(Long postId); - List getAllPosts(); + PostDetailDto getPostDetailById(Long postId, Long memberId); + List getAllPosts(Long memberId); List getAllSimplePosts(); List getPostsByMemberId(Long memberId); @@ -32,7 +33,11 @@ public interface PostService { List getMyPosts(Long requesterMemberId); // ===== 페이징 관련 ===== - Page getPostsPaged(Pageable pageable); - Page getOngoingPostsPaged(Pageable pageable); - Page getClosedPostsPaged(Pageable pageable); + Page getPostsPaged(Pageable pageable, Long memberId); + Page getOngoingPostsPaged(Pageable pageable, Long memberId); + Page getClosedPostsPaged(Pageable pageable, Long memberId); + + // ===== 투표 Top 관련 ===== + List getTopNPollsByStatus(PollDto.PollStatus status, int n, Long memberId); + PostDto getTopPollByStatus(PollDto.PollStatus status, Long memberId); } \ 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 f20d91da..cd275426 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 @@ -28,6 +28,7 @@ import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -41,7 +42,12 @@ public class PostServiceImpl implements PostService { private final PollVoteRepository pollVoteRepository; private final PollService pollService; - public PostServiceImpl(PostRepository postRepository, MemberRepository memberRepository, PollRepository pollRepository, PollOptionsRepository pollOptionsRepository, PollVoteRepository pollVoteRepository, PollService pollService) { + public PostServiceImpl(PostRepository postRepository, + MemberRepository memberRepository, + PollRepository pollRepository, + PollOptionsRepository pollOptionsRepository, + PollVoteRepository pollVoteRepository, + PollService pollService) { this.postRepository = postRepository; this.memberRepository = memberRepository; this.pollRepository = pollRepository; @@ -66,19 +72,23 @@ public PostDto createPost(PostRequestDto postRequestDto, Long memberId) { .createdAt(LocalDateTime.now()) .build(); Post saved = postRepository.save(post); - return convertToDto(saved); + return convertToDto(saved, memberId); } - @Override - public PostDetailDto getPostById(Long postId) { - return getPostDetailById(postId); + public PostDetailDto getPostDetailById(Long postId, Long memberId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.")); + PostDto postDto = convertToDto(post, memberId); + return PostDetailDto.builder() + .post(postDto) + .build(); } @Override - public PostDetailDto getPostDetailById(Long postId) { + public PostDetailDto getPostById(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.")); - PostDto postDto = convertToDto(post); + PostDto postDto = convertToDto(post, post.getMember().getMemberId()); return PostDetailDto.builder() .post(postDto) .build(); @@ -93,7 +103,7 @@ public List getPostsByMemberId(Long memberId) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 회원의 게시글이 없습니다."); } return posts.stream() - .map(this::convertToDto) + .map(post -> convertToDto(post, memberId)) .collect(Collectors.toList()); } @@ -115,7 +125,7 @@ public PostDto updatePost(Long postId, PostUpdateDto postUpdateDto) { if (post.getPoll() == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "이 게시글에는 투표가 없어 투표 수정이 불가능합니다."); } - pollService.updatePoll(post.getPoll().getPollId(), postUpdateDto.getPoll()); + pollService.updatePoll(post.getPoll().getPollId(), postUpdateDto.getPoll(), post.getMember().getMemberId()); } if (postUpdateDto.getPostName() != null) post.setPostName(postUpdateDto.getPostName()); @@ -124,7 +134,7 @@ public PostDto updatePost(Long postId, PostUpdateDto postUpdateDto) { post.setCreatedAt(java.time.LocalDateTime.now()); // 수정 시 생성일 갱신 Post updated = postRepository.save(post); - return convertToDto(updated); + return convertToDto(updated, post.getMember().getMemberId()); } @Override @@ -136,11 +146,12 @@ public void deletePost(Long postId) { } @Override - public List getAllPosts() { - List posts = postRepository.findAll(); - return posts.stream() - .map(post -> getPostDetailById(post.getPostId())) - .collect(Collectors.toList()); + public List getAllPosts(Long memberId) { + return postRepository.findAll().stream() + .map(post -> PostDetailDto.builder() + .post(convertToDto(post, memberId)) + .build()) + .collect(Collectors.toList()); } public PostDto getMyPostById(Long postId, Long requesterMemberId) { @@ -149,7 +160,7 @@ public PostDto getMyPostById(Long postId, Long requesterMemberId) { if (!post.getMember().getMemberId().equals(requesterMemberId)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인 게시글만 조회할 수 있습니다."); } - return convertToDto(post); + return convertToDto(post, requesterMemberId); } public List getMyPosts(Long requesterMemberId) { @@ -158,7 +169,7 @@ public List getMyPosts(Long requesterMemberId) { List posts = postRepository.findByMember(member); // 본인 게시글이 없으면 빈 리스트 반환 return posts.stream() - .map(this::convertToDto) + .map(post -> convertToDto(post, requesterMemberId)) .collect(Collectors.toList()); } @@ -226,38 +237,38 @@ public PostDetailDto createPostWithPoll(PostWithPollCreateDto dto, Long memberId } savedPost.setPoll(savedPoll); postRepository.save(savedPost); - return getPostDetailById(savedPost.getPostId()); + return getPostDetailById(savedPost.getPostId(), memberId); } @Override public List getAllSimplePosts() { List posts = postRepository.findAll(); return posts.stream() - .map(post -> { - PostSimpleDto.PollInfo pollInfo = null; - if (post.getPoll() != null) { - pollInfo = PostSimpleDto.PollInfo.builder() - .pollId(post.getPoll().getPollId()) - .pollStatus(post.getPoll().getStatus().name()) - .build(); - } - return PostSimpleDto.builder() - .postId(post.getPostId()) - .memberId(post.getMember().getMemberId()) - .poll(pollInfo) - .build(); - }) - .collect(Collectors.toList()); + .map(post -> { + PostSimpleDto.PollInfo pollInfo = null; + if (post.getPoll() != null) { + pollInfo = PostSimpleDto.PollInfo.builder() + .pollId(post.getPoll().getPollId()) + .pollStatus(post.getPoll().getStatus().name()) + .build(); + } + return PostSimpleDto.builder() + .postId(post.getPostId()) + .memberId(post.getMember().getMemberId()) + .poll(pollInfo) + .build(); + }) + .collect(Collectors.toList()); } @Override - public Page getPostsPaged(Pageable pageable) { - return postRepository.findAll(pageable).map(this::convertToDto); + public Page getPostsPaged(Pageable pageable, Long memberId) { + return postRepository.findAll(pageable).map(post -> convertToDto(post, memberId)); } @Override - public Page getOngoingPostsPaged(Pageable pageable) { - Page allPosts = postRepository.findAll(pageable).map(this::convertToDto); + public Page getOngoingPostsPaged(Pageable pageable, Long memberId) { + Page allPosts = postRepository.findAll(pageable).map(post -> convertToDto(post, memberId)); List ongoing = allPosts.stream() .filter(dto -> dto.getPoll() != null && dto.getPoll().getStatus() == PollDto.PollStatus.ONGOING) .collect(Collectors.toList()); @@ -265,30 +276,49 @@ public Page getOngoingPostsPaged(Pageable pageable) { } @Override - public Page getClosedPostsPaged(Pageable pageable) { - Page allPosts = postRepository.findAll(pageable).map(this::convertToDto); + public Page getClosedPostsPaged(Pageable pageable, Long memberId) { + Page allPosts = postRepository.findAll(pageable).map(post -> convertToDto(post, memberId)); 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; + @Override + public List getTopNPollsByStatus(PollDto.PollStatus status, int n, Long memberId) { + return postRepository.findAll().stream() + .map(post -> convertToDto(post, memberId)) + .filter(dto -> dto.getPoll() != null && dto.getPoll().getStatus() == status) + .sorted(Comparator.comparing((PostDto dto) -> dto.getPoll().getTotalVoteCount() == null ? 0 : dto.getPoll().getTotalVoteCount()).reversed()) + .limit(n) + .collect(Collectors.toList()); + } + + @Override + public PostDto getTopPollByStatus(PollDto.PollStatus status, Long memberId) { + return postRepository.findAll().stream() + .map(post -> convertToDto(post, memberId)) + .filter(dto -> dto.getPoll() != null && dto.getPoll().getStatus() == status) + .max(Comparator.comparing((PostDto dto) -> dto.getPoll().getTotalVoteCount() == null ? 0 : dto.getPoll().getTotalVoteCount())) + .orElse(null); + } + + private PostDto convertToDto(Post entity, Long memberId) { + Long postMemberId = null; if (entity.getMember() != null) { - memberId = entity.getMember().getMemberId(); + postMemberId = entity.getMember().getMemberId(); } PollDto pollDto = null; if (entity.getPoll() != null) { if (entity.getPoll().getStatus() == Poll.PollStatus.CLOSED) { - pollDto = pollService.getPollWithStatistics(entity.getPoll().getPollId()); + pollDto = pollService.getPollWithStatistics(entity.getPoll().getPollId(), memberId); } else { - pollDto = pollService.getPoll(entity.getPoll().getPollId()); + pollDto = pollService.getPoll(entity.getPoll().getPollId(), memberId); } } return PostDto.builder() .postId(entity.getPostId()) - .memberId(memberId) + .memberId(postMemberId) .postName(entity.getPostName()) .postContent(entity.getPostContent()) .category(entity.getCategory()) 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 new file mode 100644 index 00000000..b41e07b9 --- /dev/null +++ b/backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java @@ -0,0 +1,43 @@ +package com.ai.lawyer.global.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +public class AuthUtil { + public static Long getCurrentMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + System.out.println("[AuthUtil] principal class: " + principal.getClass().getName() + ", value: " + principal); + if (principal instanceof org.springframework.security.core.userdetails.User user) { + try { + return Long.parseLong(user.getUsername()); + } catch (NumberFormatException e) { + return null; + } + } else if (principal instanceof String str) { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return null; + } + } else if (principal instanceof Long l) { + return l; + } + } + return null; + } + + public static String getCurrentMemberRole() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + return authentication.getAuthorities().stream() + .findFirst() + .map(auth -> auth.getAuthority()) + .orElse(null); + } + +} 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 9e92d267..7425db95 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 @@ -69,26 +69,16 @@ void setUp() { @Test @DisplayName("투표 단일 조회") void t1() throws Exception { - Mockito.when(pollService.getPoll(Mockito.anyLong())).thenReturn(null); + Mockito.when(pollService.getPoll(Mockito.anyLong(), Mockito.anyLong())).thenReturn(null); mockMvc.perform(get("/api/polls/1") .cookie(new Cookie("accessToken", "valid-access-token"))) .andExpect(status().isOk()); } - @Test - @DisplayName("투표 옵션 목록 조회") - void t2() throws Exception { - Mockito.when(pollService.getPollOptions(Mockito.anyLong())).thenReturn(java.util.Collections.emptyList()); - - mockMvc.perform(get("/api/polls/1/options") - .cookie(new Cookie("accessToken", "valid-access-token"))) - .andExpect(status().isOk()); - } - @Test @DisplayName("투표하기") - void t3() throws Exception { + void t2() throws Exception { Mockito.when(pollService.vote(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(null); mockMvc.perform( @@ -100,7 +90,7 @@ void t3() throws Exception { @Test @DisplayName("투표 통계 조회") - void t4() throws Exception { + void t3() throws Exception { Mockito.when(pollService.getPollStatics(Mockito.anyLong())).thenReturn(new PollStaticsResponseDto()); mockMvc.perform(get("/api/polls/1/statics") @@ -110,7 +100,7 @@ void t4() throws Exception { @Test @DisplayName("투표 종료") - void t5() throws Exception { + void t4() throws Exception { Mockito.doNothing().when(pollService).closePoll(Mockito.anyLong()); mockMvc.perform( @@ -121,7 +111,9 @@ void t5() throws Exception { @Test @DisplayName("투표 삭제") - void t6() 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()); mockMvc.perform( @@ -132,8 +124,8 @@ void t6() throws Exception { @Test @DisplayName("진행중인 투표 Top 1 조회") - void t7() throws Exception { - Mockito.when(pollService.getTopPollByStatus(Mockito.any())).thenReturn(null); + void t6() throws Exception { + Mockito.when(pollService.getTopPollByStatus(Mockito.any(), Mockito.anyLong())).thenReturn(null); mockMvc.perform(get("/api/polls/top/ongoing") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -142,8 +134,8 @@ void t7() throws Exception { @Test @DisplayName("종료된 투표 Top 1 조회") - void t8() throws Exception { - Mockito.when(pollService.getTopPollByStatus(Mockito.any())).thenReturn(null); + void t7() throws Exception { + Mockito.when(pollService.getTopPollByStatus(Mockito.any(), Mockito.anyLong())).thenReturn(null); mockMvc.perform(get("/api/polls/top/closed") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -152,7 +144,7 @@ void t8() throws Exception { @Test @DisplayName("투표 생성") - void t9() throws Exception { + void t8() throws Exception { Mockito.when(pollService.createPoll(Mockito.any(), Mockito.anyLong())).thenReturn(null); mockMvc.perform( @@ -165,9 +157,9 @@ void t9() throws Exception { @Test @DisplayName("투표 단일 조회") - void t10() throws Exception { + void t9() throws Exception { PollDto responseDto = PollDto.builder().pollId(1L).voteTitle("테스트 투표").build(); - Mockito.when(pollService.getPoll(Mockito.anyLong())).thenReturn(responseDto); + Mockito.when(pollService.getPoll(Mockito.anyLong(), Mockito.anyLong())).thenReturn(responseDto); mockMvc.perform(get("/api/polls/1") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -178,7 +170,7 @@ void t10() throws Exception { @Test @DisplayName("투표하기") - void t11() throws Exception { + void t10() throws Exception { PollVoteDto responseDto = PollVoteDto.builder().pollId(1L).memberId(1L).build(); Mockito.when(pollService.vote(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(responseDto); 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 c47df427..7d6b8f94 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 @@ -112,7 +112,7 @@ void autoCloseTest() { 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); + PollDto closed = pollService.getPoll(1L, 1L); assertThat(closed.getStatus()).isEqualTo(PollDto.PollStatus.CLOSED); } } 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 2a3cdebb..1f986f6f 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 @@ -29,8 +29,8 @@ void setUp() { @DisplayName("투표 단일 조회") void t1() { PollDto expected = new PollDto(); - Mockito.when(pollService.getPoll(Mockito.anyLong())).thenReturn(expected); - PollDto result = pollService.getPoll(1L); + Mockito.when(pollService.getPoll(Mockito.anyLong(), Mockito.anyLong())).thenReturn(expected); + PollDto result = pollService.getPoll(1L, 1L); assertThat(result).isEqualTo(expected); } @@ -81,8 +81,8 @@ void t6() { @DisplayName("상태별 Top 투표 조회") void t7() { PollDto expected = new PollDto(); - Mockito.when(pollService.getTopPollByStatus(Mockito.any())).thenReturn(expected); - PollDto result = pollService.getTopPollByStatus(PollDto.PollStatus.ONGOING); + Mockito.when(pollService.getTopPollByStatus(Mockito.any(), Mockito.anyLong())).thenReturn(expected); + PollDto result = pollService.getTopPollByStatus(PollDto.PollStatus.ONGOING, 1L); assertThat(result).isEqualTo(expected); } @@ -107,8 +107,8 @@ void t9() { void t10() { PollDto expected = new PollDto(); PollUpdateDto updateDto = new PollUpdateDto(); - Mockito.when(pollService.updatePoll(Mockito.anyLong(), Mockito.any())).thenReturn(expected); - PollDto result = pollService.updatePoll(1L, updateDto); + Mockito.when(pollService.updatePoll(Mockito.anyLong(), Mockito.any(), Mockito.anyLong())).thenReturn(expected); + PollDto result = pollService.updatePoll(1L, updateDto, 1L); assertThat(result).isEqualTo(expected); } @@ -116,8 +116,8 @@ void t10() { @DisplayName("통계 포함 투표 조회") void t11() { PollDto expected = new PollDto(); - Mockito.when(pollService.getPollWithStatistics(Mockito.anyLong())).thenReturn(expected); - PollDto result = pollService.getPollWithStatistics(1L); + Mockito.when(pollService.getPollWithStatistics(Mockito.anyLong(), Mockito.isNull())).thenReturn(expected); + PollDto result = pollService.getPollWithStatistics(1L, null); assertThat(result).isEqualTo(expected); } @@ -144,8 +144,8 @@ void t13() { @DisplayName("상태별 투표 목록 조회") void t14() { java.util.List expected = java.util.Collections.emptyList(); - Mockito.when(pollService.getPollsByStatus(Mockito.any())).thenReturn(expected); - java.util.List result = pollService.getPollsByStatus(PollDto.PollStatus.ONGOING); + Mockito.when(pollService.getPollsByStatus(Mockito.any(), Mockito.anyLong())).thenReturn(expected); + java.util.List result = pollService.getPollsByStatus(PollDto.PollStatus.ONGOING, 1L); assertThat(result).isEqualTo(expected); } @@ -153,8 +153,8 @@ void t14() { @DisplayName("상태별 Top N 투표 목록 조회") void t15() { java.util.List expected = java.util.Collections.emptyList(); - Mockito.when(pollService.getTopNPollsByStatus(Mockito.any(), Mockito.anyInt())).thenReturn(expected); - java.util.List result = pollService.getTopNPollsByStatus(PollDto.PollStatus.ONGOING, 3); + Mockito.when(pollService.getTopNPollsByStatus(Mockito.any(), Mockito.anyInt(), Mockito.anyLong())).thenReturn(expected); + java.util.List result = pollService.getTopNPollsByStatus(PollDto.PollStatus.ONGOING, 3, 1L); assertThat(result).isEqualTo(expected); } 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 cdc5d978..22d0fbcb 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 @@ -1,6 +1,7 @@ package com.ai.lawyer.domain.post.controller; import com.ai.lawyer.domain.post.dto.PostRequestDto; +import com.ai.lawyer.domain.post.dto.PostUpdateDto; import com.ai.lawyer.domain.post.service.PostService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; @@ -92,7 +93,7 @@ void t1() throws Exception { @DisplayName("게시글 전체 조회") void t2() throws Exception { List posts = java.util.Collections.emptyList(); - Mockito.when(postService.getAllPosts()).thenReturn(posts); + Mockito.when(postService.getAllPosts(Mockito.anyLong())).thenReturn(posts); mockMvc.perform(get("/api/posts") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -105,7 +106,7 @@ void t2() throws Exception { void t3() throws Exception { com.ai.lawyer.domain.post.dto.PostDto postDto = com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("테스트 제목").build(); com.ai.lawyer.domain.post.dto.PostDetailDto postDetailDto = com.ai.lawyer.domain.post.dto.PostDetailDto.builder().post(postDto).build(); - Mockito.when(postService.getPostById(Mockito.anyLong())).thenReturn(postDetailDto); + Mockito.when(postService.getPostDetailById(Mockito.anyLong(), Mockito.anyLong())).thenReturn(postDetailDto); mockMvc.perform(get("/api/posts/1") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -119,7 +120,7 @@ void t4() throws Exception { List postDtoList = List.of(com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("테스트 제목").build()); com.ai.lawyer.domain.post.dto.PostDetailDto postDetailDto = com.ai.lawyer.domain.post.dto.PostDetailDto.builder().post(postDtoList.getFirst()).build(); Mockito.when(postService.getPostsByMemberId(Mockito.anyLong())).thenReturn(postDtoList); - Mockito.when(postService.getPostDetailById(Mockito.anyLong())).thenReturn(postDetailDto); + Mockito.when(postService.getPostDetailById(Mockito.anyLong(), Mockito.anyLong())).thenReturn(postDetailDto); mockMvc.perform(get("/api/posts/member/1") .cookie(new Cookie("accessToken", "valid-access-token"))) @@ -130,10 +131,14 @@ void t4() throws Exception { @Test @DisplayName("게시글 수정") void t5() throws Exception { - com.ai.lawyer.domain.post.dto.PostDto postDto = com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("수정 제목").build(); + com.ai.lawyer.domain.post.dto.PostDto postDto = com.ai.lawyer.domain.post.dto.PostDto.builder() + .postId(1L) + .postName("수정 제목") + .memberId(1L) + .build(); com.ai.lawyer.domain.post.dto.PostDetailDto postDetailDto = com.ai.lawyer.domain.post.dto.PostDetailDto.builder().post(postDto).build(); Mockito.doNothing().when(postService).patchUpdatePost(Mockito.anyLong(), Mockito.any()); - Mockito.when(postService.getPostDetailById(Mockito.anyLong())).thenReturn(postDetailDto); + Mockito.when(postService.getPostDetailById(Mockito.eq(1L), Mockito.anyLong())).thenReturn(postDetailDto); com.ai.lawyer.domain.post.dto.PostUpdateDto updateDto = com.ai.lawyer.domain.post.dto.PostUpdateDto.builder().postName("수정 제목").build(); mockMvc.perform(put("/api/posts/1") @@ -147,6 +152,13 @@ void t5() throws Exception { @Test @DisplayName("게시글 삭제") void t6() throws Exception { + com.ai.lawyer.domain.post.dto.PostDto postDto = com.ai.lawyer.domain.post.dto.PostDto.builder() + .postId(1L) + .postName("삭제 제목") + .memberId(1L) + .build(); + com.ai.lawyer.domain.post.dto.PostDetailDto postDetailDto = com.ai.lawyer.domain.post.dto.PostDetailDto.builder().post(postDto).build(); + Mockito.when(postService.getPostDetailById(Mockito.eq(1L), Mockito.anyLong())).thenReturn(postDetailDto); Mockito.doNothing().when(postService).deletePost(Mockito.anyLong()); mockMvc.perform(delete("/api/posts/1") @@ -162,7 +174,7 @@ void t7() 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<>(postList, pageable, 1); - Mockito.when(postService.getPostsPaged(Mockito.any(org.springframework.data.domain.Pageable.class))).thenReturn(page); + Mockito.when(postService.getPostsPaged(Mockito.any(org.springframework.data.domain.Pageable.class), Mockito.anyLong())).thenReturn(page); mockMvc.perform(get("/api/posts/paged") .param("page", "0") diff --git a/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java b/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java index 0c174b26..fea1c4a8 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java @@ -60,8 +60,8 @@ void t3() { @DisplayName("게시글 상세 조회") void t4() { PostDetailDto expected = new PostDetailDto(); - Mockito.when(postService.getPostDetailById(Mockito.anyLong())).thenReturn(expected); - PostDetailDto result = postService.getPostDetailById(1L); + Mockito.when(postService.getPostDetailById(Mockito.anyLong(), Mockito.anyLong())).thenReturn(expected); + PostDetailDto result = postService.getPostDetailById(1L, 1L); assertThat(result).isEqualTo(expected); } @@ -96,8 +96,8 @@ void t7() { @DisplayName("전체 게시글 목록 조회") void t8() { java.util.List expected = java.util.Collections.emptyList(); - Mockito.when(postService.getAllPosts()).thenReturn(expected); - java.util.List result = postService.getAllPosts(); + Mockito.when(postService.getAllPosts(Mockito.anyLong())).thenReturn(expected); + java.util.List result = postService.getAllPosts(1L); assertThat(result).isEqualTo(expected); } @@ -134,9 +134,8 @@ void t12() { java.util.List postList = java.util.List.of(new PostDto()); 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<>(postList, pageable, 1); - Mockito.when(postService.getPostsPaged(pageable)).thenReturn(page); - - var result = postService.getPostsPaged(pageable); + Mockito.when(postService.getPostsPaged(Mockito.eq(pageable), Mockito.eq(1L))).thenReturn(page); + var result = postService.getPostsPaged(pageable, 1L); assertThat(result.getContent()).hasSize(1); assertThat(result.getTotalElements()).isEqualTo(1); assertThat(result.getTotalPages()).isEqualTo(1);