diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java index fb0f1a2..b646cdf 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java @@ -1,6 +1,5 @@ package com.ai.lawyer.domain.chatbot.entity; -import com.ai.lawyer.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -24,9 +23,10 @@ public class History { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long historyId; - @ManyToOne - @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER")) - private Member memberId; + // Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거 (ConstraintMode.NO_CONSTRAINT) + // member_id를 직접 저장하고, 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장 + @Column(name = "member_id") + private Long memberId; @OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true) private List chats; diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatLawRepository.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatLawRepository.java index bea2a9a..029c9c9 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatLawRepository.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatLawRepository.java @@ -12,8 +12,9 @@ public interface ChatLawRepository extends JpaRepository { /** * member_id에 해당하는 모든 ChatLaw 삭제 (회원 탈퇴 시 사용) + * History.memberId가 Long 타입이므로 직접 비교 */ @Modifying - @Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId.memberId = :memberId") + @Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId = :memberId") void deleteByMemberIdValue(@Param("memberId") Long memberId); } diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatPrecedentRepository.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatPrecedentRepository.java index 820456d..dc80392 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatPrecedentRepository.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatPrecedentRepository.java @@ -10,8 +10,9 @@ public interface ChatPrecedentRepository extends JpaRepository { /** * member_id에 해당하는 모든 Chat 삭제 (회원 탈퇴 시 사용) + * History.memberId가 Long 타입이므로 직접 비교 */ @Modifying - @Query("DELETE FROM Chat c WHERE c.historyId.memberId.memberId = :memberId") + @Query("DELETE FROM Chat c WHERE c.historyId.memberId = :memberId") void deleteByMemberIdValue(@Param("memberId") Long memberId); } diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/HistoryRepository.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/HistoryRepository.java index ec094d4..2b4f402 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/HistoryRepository.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/HistoryRepository.java @@ -1,7 +1,6 @@ package com.ai.lawyer.domain.chatbot.repository; import com.ai.lawyer.domain.chatbot.entity.History; -import com.ai.lawyer.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -13,16 +12,17 @@ @Repository public interface HistoryRepository extends JpaRepository { - List findAllByMemberId(Member memberId); + // member_id로 직접 조회 (Member, OAuth2Member 모두 지원) + List findAllByMemberId(Long memberId); - History findByHistoryIdAndMemberId(Long roomId, Member memberId); + History findByHistoryIdAndMemberId(Long roomId, Long memberId); /** * member_id로 채팅 히스토리 삭제 (회원 탈퇴 시 사용) * Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제 */ @Modifying - @Query("DELETE FROM History h WHERE h.memberId.memberId = :memberId") + @Query("DELETE FROM History h WHERE h.memberId = :memberId") void deleteByMemberIdValue(@Param("memberId") Long memberId); } \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java index f1bb75e..fec7196 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java @@ -28,6 +28,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import java.util.HashMap; import java.util.List; @@ -60,27 +62,36 @@ public class ChatBotService { @Transactional public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, Long roomId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + // Mono.fromCallable()과 subscribeOn()을 사용하여 블로킹 작업을 별도 스레드에서 실행 + // boundedElastic 스케줄러는 블로킹 I/O 작업에 최적화된 스레드 풀 사용 + return Mono.fromCallable(() -> { + // 멤버 조회 (블로킹) + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); - // 벡터 검색 (판례, 법령) - List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); - List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); + // 벡터 검색 (판례, 법령) (블로킹) + List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); + List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); - String caseContext = formatting(similarCaseDocuments); - String lawContext = formatting(similarLawDocuments); + String caseContext = formatting(similarCaseDocuments); + String lawContext = formatting(similarLawDocuments); - // 채팅방 조회 또는 생성 - History history = getOrCreateRoom(member, roomId); + // 채팅방 조회 또는 생성 (블로킹) + History history = getOrCreateRoom(member, roomId); - // 메시지 기억 관리 (User 메시지 추가) - ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); + // 메시지 기억 관리 (User 메시지 추가) + ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); - // 프롬프트 생성 - Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); + // 프롬프트 생성 + Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); - // LLM 스트리밍 호출 및 클라이언트에게 즉시 응답 - return chatClient.prompt(prompt) + // 준비된 데이터를 담은 컨텍스트 객체 반환 + return new PreparedChatContext(prompt, history, similarCaseDocuments, similarLawDocuments); + }) + .subscribeOn(Schedulers.boundedElastic()) // 블로킹 작업을 별도 스레드에서 실행 + .flatMapMany(context -> { + // LLM 스트리밍 호출 및 클라이언트에게 즉시 응답 + return chatClient.prompt(context.prompt) .stream() .content() .collectList() @@ -88,40 +99,41 @@ public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, .doOnNext(fullResponse -> { // Document를 DTO로 변환 - List caseDtos = similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); - List lawDtos = similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + List caseDtos = context.similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + List lawDtos = context.similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); // Kafka로 보낼 이벤트 객체 ChatPostProcessEvent event = new ChatPostProcessEvent( - history.getHistoryId(), + context.history.getHistoryId(), chatRequestDto.getMessage(), fullResponse, caseDtos, lawDtos ); - + // Kafka 이벤트 발행 kafkaTemplate.send(POST_PROCESSING_TOPIC, event); }) - .map(fullResponse -> createChatResponse(history, fullResponse, similarCaseDocuments, similarLawDocuments)) + .map(fullResponse -> createChatResponse(context.history, fullResponse, context.similarCaseDocuments, context.similarLawDocuments)) .flux() .onErrorResume(throwable -> { - log.error("스트리밍 처리 중 에러 발생 (historyId: {})", history.getHistoryId(), throwable); - return Flux.just(handleError(history)); + log.error("스트리밍 처리 중 에러 발생 (historyId: {})", context.history.getHistoryId(), throwable); + return Flux.just(handleError(context.history)); }); + }); } private ChatResponse createChatResponse(History history, String fullResponse, List cases, List laws) { ChatPrecedentDto precedentDto = null; if (cases != null && !cases.isEmpty()) { - Document firstCase = cases.get(0); + Document firstCase = cases.getFirst(); precedentDto = ChatPrecedentDto.from(firstCase); } ChatLawDto lawDto = null; if (laws != null && !laws.isEmpty()) { - Document firstLaw = laws.get(0); + Document firstLaw = laws.getFirst(); lawDto = ChatLawDto.from(firstLaw); } @@ -159,7 +171,7 @@ private History getOrCreateRoom(Member member, Long roomId) { if (roomId != null) { return historyService.getHistory(roomId); } else { - return historyRepository.save(History.builder().memberId(member).build()); + return historyRepository.save(History.builder().memberId(member.getMemberId()).build()); } } @@ -178,4 +190,24 @@ private ChatResponse handleError(History history) { .message("죄송합니다. 서비스 처리 중 오류가 발생했습니다. 요청을 다시 전송해 주세요.") .build(); } + + /** + * 블로킹 작업에서 준비된 데이터를 담는 컨텍스트 클래스 + * 리액티브 체인에서 데이터를 전달하기 위한 내부 클래스 + */ + private static class PreparedChatContext { + final Prompt prompt; + final History history; + final List similarCaseDocuments; + final List similarLawDocuments; + + PreparedChatContext(Prompt prompt, History history, + List similarCaseDocuments, + List similarLawDocuments) { + this.prompt = prompt; + this.history = history; + this.similarCaseDocuments = similarCaseDocuments; + this.similarLawDocuments = similarLawDocuments; + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java index e1d9e1b..db14eb2 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java @@ -6,7 +6,6 @@ import com.ai.lawyer.domain.chatbot.entity.History; import com.ai.lawyer.domain.chatbot.exception.HistoryNotFoundException; import com.ai.lawyer.domain.chatbot.repository.HistoryRepository; -import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.member.repositories.MemberRepository; import com.ai.lawyer.infrastructure.redis.service.ChatCacheService; import lombok.RequiredArgsConstructor; @@ -28,11 +27,12 @@ public class HistoryService { public List getHistoryTitle(Long memberId) { - Member member = memberRepository.findById(memberId).orElseThrow( + // 회원 존재 여부 확인 + memberRepository.findById(memberId).orElseThrow( () -> new IllegalArgumentException("존재하지 않는 회원입니다.") ); - List rooms = historyRepository.findAllByMemberId(member); + List rooms = historyRepository.findAllByMemberId(memberId); List roomDtos = new ArrayList<>(); for (History room : rooms) @@ -45,11 +45,12 @@ public String deleteHistory(Long memberId, Long roomId) { getHistory(roomId); - Member member = memberRepository.findById(memberId).orElseThrow( + // 회원 존재 여부 확인 + memberRepository.findById(memberId).orElseThrow( () -> new IllegalArgumentException("존재하지 않는 회원입니다.") ); - History room = historyRepository.findByHistoryIdAndMemberId(roomId, member); + History room = historyRepository.findByHistoryIdAndMemberId(roomId, memberId); historyRepository.delete(room); chatCacheService.clearChatHistory(roomId); @@ -61,7 +62,8 @@ public String deleteHistory(Long memberId, Long roomId) { @Transactional(readOnly = true) public ResponseEntity> getChatHistory(Long memberId, Long roomId) { - Member member = memberRepository.findById(memberId).orElseThrow( + // 회원 존재 여부 확인 + memberRepository.findById(memberId).orElseThrow( () -> new IllegalArgumentException("존재하지 않는 회원입니다.") ); @@ -72,7 +74,7 @@ public ResponseEntity> getChatHistory(Long memberId, Long r } // 2. DB에서 조회 후 캐시에 저장 - History history = historyRepository.findByHistoryIdAndMemberId(roomId, member); + History history = historyRepository.findByHistoryIdAndMemberId(roomId, memberId); List chats = history.getChats(); // 엔티티 -> DTO 변환 diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java b/backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java index a7c7e75..82bd620 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java @@ -21,8 +21,11 @@ public class PollVote { @JoinColumn(name = "poll_id", nullable = false, foreignKey = @ForeignKey(name = "FK_POLLVOTE_POLL")) private Poll poll; + // Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거 + // 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장 + // foreignKey 제약조건 비활성화 (ConstraintMode.NO_CONSTRAINT) @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false, foreignKey = @ForeignKey(name = "FK_POLLVOTE_MEMBER")) + @JoinColumn(name = "member_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Member member; @ManyToOne(fetch = FetchType.LAZY) 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 a4c34cc..7965743 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,12 +4,10 @@ import com.ai.lawyer.domain.poll.entity.*; import com.ai.lawyer.domain.poll.repository.*; import com.ai.lawyer.domain.member.entity.Member; -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; @@ -242,7 +240,7 @@ public void closePoll(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)) { + if (poll.getPost() == null || !poll.getPost().getMemberId().equals(memberId)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 삭제할 수 있습니다."); } // 1. 이 Poll을 참조하는 Post가 있으면 연결 해제 @@ -312,7 +310,7 @@ public Long getVoteCountByPostId(Long postId) { 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)) { + if (!poll.getPost().getMemberId().equals(memberId)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 수정할 수 있습니다."); } if (getVoteCountByPollId(pollId) > 0) { diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java b/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java index 74b2f23..7469f4c 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java @@ -1,6 +1,5 @@ package com.ai.lawyer.domain.post.entity; -import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.poll.entity.Poll; import jakarta.persistence.*; import lombok.*; @@ -21,9 +20,10 @@ public class Post { @Column(name = "post_id") private Long postId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = true, foreignKey = @ForeignKey(name = "FK_POST_MEMBER")) - private Member member; + // Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거 (ConstraintMode.NO_CONSTRAINT) + // member_id를 직접 저장하고, 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장 + @Column(name = "member_id") + private Long memberId; @Column(name = "post_name", length = 100, nullable = false) private String postName; diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/repository/PostRepository.java b/backend/src/main/java/com/ai/lawyer/domain/post/repository/PostRepository.java index db085a2..afe741b 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/repository/PostRepository.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/repository/PostRepository.java @@ -1,6 +1,5 @@ package com.ai.lawyer.domain.post.repository; -import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.post.entity.Post; import com.ai.lawyer.domain.poll.entity.Poll.PollStatus; import org.springframework.data.domain.Page; @@ -15,17 +14,18 @@ @Repository public interface PostRepository extends JpaRepository { - List findByMember(Member member); + // member_id로 직접 조회 (Member, OAuth2Member 모두 지원) + List findByMemberId(Long memberId); /** * member_id로 게시글 삭제 (회원 탈퇴 시 사용) * Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제 */ @Modifying - @Query("DELETE FROM Post p WHERE p.member.memberId = :memberId") + @Query("DELETE FROM Post p WHERE p.memberId = :memberId") void deleteByMemberIdValue(@Param("memberId") Long memberId); - Page findByMember(Member member, Pageable pageable); + Page findByMemberId(Long memberId, Pageable pageable); Page findByPoll_Status(PollStatus status, Pageable pageable); Page findByPoll_StatusAndPoll_PollIdIn(PollStatus status, List pollIds, Pageable pageable); Page findByPoll_PollIdIn(List pollIds, Pageable pageable); 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 2b007cb..5aaa32c 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 @@ -1,6 +1,5 @@ package com.ai.lawyer.domain.post.service; -import com.ai.lawyer.domain.member.entity.Member; import com.ai.lawyer.domain.poll.dto.PollDto; import com.ai.lawyer.domain.poll.dto.PollDto.PollStatus; import com.ai.lawyer.domain.post.dto.PostDto; @@ -21,7 +20,6 @@ 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; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -64,9 +62,11 @@ public PostDto createPost(PostRequestDto postRequestDto, Long memberId) { postRequestDto.getPostContent() == null || postRequestDto.getPostContent().trim().isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "게시글 제목과 내용은 필수입니다."); } - Member member = AuthUtil.getMemberOrThrow(memberId); + // 회원 존재 여부 확인 (Member 또는 OAuth2Member) + AuthUtil.getMemberOrThrow(memberId); + Post post = Post.builder() - .member(member) + .memberId(memberId) .postName(postRequestDto.getPostName()) .postContent(postRequestDto.getPostContent()) .category(postRequestDto.getCategory()) @@ -90,7 +90,7 @@ public PostDetailDto getPostDetailById(Long postId, Long memberId) { public PostDetailDto getPostById(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.")); - PostDto postDto = convertToDto(post, post.getMember().getMemberId()); + PostDto postDto = convertToDto(post, post.getMemberId()); return PostDetailDto.builder() .post(postDto) .build(); @@ -98,8 +98,9 @@ public PostDetailDto getPostById(Long postId) { @Override public List getPostsByMemberId(Long memberId) { - Member member = AuthUtil.getMemberOrThrow(memberId); - List posts = postRepository.findByMember(member); + // 회원 존재 여부 확인 + AuthUtil.getMemberOrThrow(memberId); + List posts = postRepository.findByMemberId(memberId); // if (posts.isEmpty()) { // throw new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 회원의 게시글이 없습니다."); // } @@ -127,7 +128,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(), post.getMember().getMemberId()); + pollService.updatePoll(post.getPoll().getPollId(), postUpdateDto.getPoll(), post.getMemberId()); } if (postUpdateDto.getPostName() != null) post.setPostName(postUpdateDto.getPostName()); @@ -135,7 +136,7 @@ public PostDto updatePost(Long postId, PostUpdateDto postUpdateDto) { if (postUpdateDto.getCategory() != null) post.setCategory(postUpdateDto.getCategory()); post.setUpdatedAt(LocalDateTime.now()); // 추가 postRepository.save(post); - return convertToDto(post, post.getMember().getMemberId()); + return convertToDto(post, post.getMemberId()); } @Override @@ -165,15 +166,16 @@ public List getAllPosts(Long memberId) { public PostDto getMyPostById(Long postId, Long requesterMemberId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.")); - if (!post.getMember().getMemberId().equals(requesterMemberId)) { + if (!post.getMemberId().equals(requesterMemberId)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인 게시글만 조회할 수 있습니다."); } return convertToDto(post, requesterMemberId); } public List getMyPosts(Long requesterMemberId) { - Member member = AuthUtil.getMemberOrThrow(requesterMemberId); - List posts = postRepository.findByMember(member); + // 회원 존재 여부 확인 + AuthUtil.getMemberOrThrow(requesterMemberId); + List posts = postRepository.findByMemberId(requesterMemberId); return posts.stream() .sorted(Comparator.comparing(Post::getUpdatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) .map(post -> convertToDto(post, requesterMemberId)) @@ -182,8 +184,9 @@ public List getMyPosts(Long requesterMemberId) { @Override public Page getMyPostspaged(Pageable pageable, Long requesterMemberId) { - Member member = AuthUtil.getMemberOrThrow(requesterMemberId); - Page posts = postRepository.findByMember(member, pageable); + // 회원 존재 여부 확인 + AuthUtil.getMemberOrThrow(requesterMemberId); + Page posts = postRepository.findByMemberId(requesterMemberId, pageable); return posts.map(post -> convertToDto(post, requesterMemberId)); } @@ -218,9 +221,10 @@ public PostDetailDto createPostWithPoll(PostWithPollCreateDto dto, Long memberId } var pollDto = dto.getPoll(); pollService.validatePollCreate(pollDto); - Member member = AuthUtil.getMemberOrThrow(memberId); + // 회원 존재 여부 확인 + AuthUtil.getMemberOrThrow(memberId); Post post = Post.builder() - .member(member) + .memberId(memberId) .postName(postDto.getPostName()) .postContent(postDto.getPostContent()) .category(postDto.getCategory()) @@ -263,7 +267,7 @@ public List getAllSimplePosts() { } return PostSimpleDto.builder() .postId(post.getPostId()) - .memberId(post.getMember().getMemberId()) + .memberId(post.getMemberId()) .poll(pollInfo) .build(); }) @@ -339,10 +343,7 @@ public List getTopNPollsByStatus(PollStatus status, int n, Long memberI } private PostDto convertToDto(Post entity, Long memberId) { - Long postMemberId = null; - if (entity.getMember() != null) { - postMemberId = entity.getMember().getMemberId(); - } + Long postMemberId = entity.getMemberId(); PollDto pollDto = null; if (entity.getPoll() != null) { if (entity.getPoll().getStatus() == Poll.PollStatus.CLOSED) { 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 7d6b8f9..79c2d58 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 @@ -63,7 +63,7 @@ void autoCloseTest() { post.setPostContent("테스트 내용"); post.setCategory("테스트"); post.setCreatedAt(LocalDateTime.now()); - post.setMember(member); + post.setMemberId(member.getMemberId()); post.setPoll(null); lenient().when(postRepository.save(any(Post.class))).thenReturn(post); @@ -80,7 +80,7 @@ void autoCloseTest() { postWithPoll.setPostContent("테스트 내용"); postWithPoll.setCategory("테스트"); postWithPoll.setCreatedAt(post.getCreatedAt()); - postWithPoll.setMember(member); + postWithPoll.setMemberId(member.getMemberId()); postWithPoll.setPoll(poll); lenient().when(postRepository.save(argThat(p -> p.getPoll() != null))).thenReturn(postWithPoll);