Skip to content

Commit 532bf4e

Browse files
authored
Merge pull request #342 from prgrms-web-devcourse-final-project/develop
배포
2 parents 4771cfd + 6ad58fa commit 532bf4e

File tree

13 files changed

+118
-79
lines changed

13 files changed

+118
-79
lines changed

backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.ai.lawyer.domain.chatbot.entity;
22

3-
import com.ai.lawyer.domain.member.entity.Member;
43
import jakarta.persistence.*;
54
import lombok.AllArgsConstructor;
65
import lombok.Builder;
@@ -24,9 +23,10 @@ public class History {
2423
@GeneratedValue(strategy = GenerationType.IDENTITY)
2524
private Long historyId;
2625

27-
@ManyToOne
28-
@JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER"))
29-
private Member memberId;
26+
// Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거 (ConstraintMode.NO_CONSTRAINT)
27+
// member_id를 직접 저장하고, 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장
28+
@Column(name = "member_id")
29+
private Long memberId;
3030

3131
@OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true)
3232
private List<Chat> chats;

backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatLawRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ public interface ChatLawRepository extends JpaRepository<ChatLaw, Long> {
1212

1313
/**
1414
* member_id에 해당하는 모든 ChatLaw 삭제 (회원 탈퇴 시 사용)
15+
* History.memberId가 Long 타입이므로 직접 비교
1516
*/
1617
@Modifying
17-
@Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId.memberId = :memberId")
18+
@Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId = :memberId")
1819
void deleteByMemberIdValue(@Param("memberId") Long memberId);
1920
}

backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatPrecedentRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ public interface ChatPrecedentRepository extends JpaRepository<ChatPrecedent, Lo
1010

1111
/**
1212
* member_id에 해당하는 모든 ChatPrecedent 삭제 (회원 탈퇴 시 사용)
13+
* History.memberId가 Long 타입이므로 직접 비교
1314
*/
1415
@Modifying
15-
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId.memberId = :memberId")
16+
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId = :memberId")
1617
void deleteByMemberIdValue(@Param("memberId") Long memberId);
1718
}

backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/ChatRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ public interface ChatRepository extends JpaRepository<Chat, Long> {
1212

1313
/**
1414
* member_id에 해당하는 모든 Chat 삭제 (회원 탈퇴 시 사용)
15+
* History.memberId가 Long 타입이므로 직접 비교
1516
*/
1617
@Modifying
17-
@Query("DELETE FROM Chat c WHERE c.historyId.memberId.memberId = :memberId")
18+
@Query("DELETE FROM Chat c WHERE c.historyId.memberId = :memberId")
1819
void deleteByMemberIdValue(@Param("memberId") Long memberId);
1920
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.ai.lawyer.domain.chatbot.repository;
22

33
import com.ai.lawyer.domain.chatbot.entity.History;
4-
import com.ai.lawyer.domain.member.entity.Member;
54
import org.springframework.data.jpa.repository.JpaRepository;
65
import org.springframework.data.jpa.repository.Modifying;
76
import org.springframework.data.jpa.repository.Query;
@@ -13,16 +12,17 @@
1312
@Repository
1413
public interface HistoryRepository extends JpaRepository<History, Long> {
1514

16-
List<History> findAllByMemberId(Member memberId);
15+
// member_id로 직접 조회 (Member, OAuth2Member 모두 지원)
16+
List<History> findAllByMemberId(Long memberId);
1717

18-
History findByHistoryIdAndMemberId(Long roomId, Member memberId);
18+
History findByHistoryIdAndMemberId(Long roomId, Long memberId);
1919

2020
/**
2121
* member_id로 채팅 히스토리 삭제 (회원 탈퇴 시 사용)
2222
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
2323
*/
2424
@Modifying
25-
@Query("DELETE FROM History h WHERE h.memberId.memberId = :memberId")
25+
@Query("DELETE FROM History h WHERE h.memberId = :memberId")
2626
void deleteByMemberIdValue(@Param("memberId") Long memberId);
2727

2828
}

backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.springframework.stereotype.Service;
2929
import org.springframework.transaction.annotation.Transactional;
3030
import reactor.core.publisher.Flux;
31+
import reactor.core.publisher.Mono;
32+
import reactor.core.scheduler.Schedulers;
3133

3234
import java.util.HashMap;
3335
import java.util.List;
@@ -60,68 +62,78 @@ public class ChatBotService {
6062
@Transactional
6163
public Flux<ChatResponse> sendMessage(Long memberId, ChatRequest chatRequestDto, Long roomId) {
6264

63-
Member member = memberRepository.findById(memberId)
64-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
65+
// Mono.fromCallable()과 subscribeOn()을 사용하여 블로킹 작업을 별도 스레드에서 실행
66+
// boundedElastic 스케줄러는 블로킹 I/O 작업에 최적화된 스레드 풀 사용
67+
return Mono.fromCallable(() -> {
68+
// 멤버 조회 (블로킹)
69+
Member member = memberRepository.findById(memberId)
70+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
6571

66-
// 벡터 검색 (판례, 법령)
67-
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례");
68-
List<Document> similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령");
72+
// 벡터 검색 (판례, 법령) (블로킹)
73+
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례");
74+
List<Document> similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령");
6975

70-
String caseContext = formatting(similarCaseDocuments);
71-
String lawContext = formatting(similarLawDocuments);
76+
String caseContext = formatting(similarCaseDocuments);
77+
String lawContext = formatting(similarLawDocuments);
7278

73-
// 채팅방 조회 또는 생성
74-
History history = getOrCreateRoom(member, roomId);
79+
// 채팅방 조회 또는 생성 (블로킹)
80+
History history = getOrCreateRoom(member, roomId);
7581

76-
// 메시지 기억 관리 (User 메시지 추가)
77-
ChatMemory chatMemory = saveChatMemory(chatRequestDto, history);
82+
// 메시지 기억 관리 (User 메시지 추가)
83+
ChatMemory chatMemory = saveChatMemory(chatRequestDto, history);
7884

79-
// 프롬프트 생성
80-
Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history);
85+
// 프롬프트 생성
86+
Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history);
8187

82-
// LLM 스트리밍 호출 및 클라이언트에게 즉시 응답
83-
return chatClient.prompt(prompt)
88+
// 준비된 데이터를 담은 컨텍스트 객체 반환
89+
return new PreparedChatContext(prompt, history, similarCaseDocuments, similarLawDocuments);
90+
})
91+
.subscribeOn(Schedulers.boundedElastic()) // 블로킹 작업을 별도 스레드에서 실행
92+
.flatMapMany(context -> {
93+
// LLM 스트리밍 호출 및 클라이언트에게 즉시 응답
94+
return chatClient.prompt(context.prompt)
8495
.stream()
8596
.content()
8697
.collectList()
8798
.map(fullResponseList -> String.join("", fullResponseList))
8899
.doOnNext(fullResponse -> {
89100

90101
// Document를 DTO로 변환
91-
List<DocumentDto> caseDtos = similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList());
92-
List<DocumentDto> lawDtos = similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList());
102+
List<DocumentDto> caseDtos = context.similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList());
103+
List<DocumentDto> lawDtos = context.similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList());
93104

94105
// Kafka로 보낼 이벤트 객체
95106
ChatPostProcessEvent event = new ChatPostProcessEvent(
96-
history.getHistoryId(),
107+
context.history.getHistoryId(),
97108
chatRequestDto.getMessage(),
98109
fullResponse,
99110
caseDtos,
100111
lawDtos
101112
);
102-
113+
103114
// Kafka 이벤트 발행
104115
kafkaTemplate.send(POST_PROCESSING_TOPIC, event);
105116

106117
})
107-
.map(fullResponse -> createChatResponse(history, fullResponse, similarCaseDocuments, similarLawDocuments))
118+
.map(fullResponse -> createChatResponse(context.history, fullResponse, context.similarCaseDocuments, context.similarLawDocuments))
108119
.flux()
109120
.onErrorResume(throwable -> {
110-
log.error("스트리밍 처리 중 에러 발생 (historyId: {})", history.getHistoryId(), throwable);
111-
return Flux.just(handleError(history));
121+
log.error("스트리밍 처리 중 에러 발생 (historyId: {})", context.history.getHistoryId(), throwable);
122+
return Flux.just(handleError(context.history));
112123
});
124+
});
113125
}
114126

115127
private ChatResponse createChatResponse(History history, String fullResponse, List<Document> cases, List<Document> laws) {
116128
ChatPrecedentDto precedentDto = null;
117129
if (cases != null && !cases.isEmpty()) {
118-
Document firstCase = cases.get(0);
130+
Document firstCase = cases.getFirst();
119131
precedentDto = ChatPrecedentDto.from(firstCase);
120132
}
121133

122134
ChatLawDto lawDto = null;
123135
if (laws != null && !laws.isEmpty()) {
124-
Document firstLaw = laws.get(0);
136+
Document firstLaw = laws.getFirst();
125137
lawDto = ChatLawDto.from(firstLaw);
126138
}
127139

@@ -159,7 +171,7 @@ private History getOrCreateRoom(Member member, Long roomId) {
159171
if (roomId != null) {
160172
return historyService.getHistory(roomId);
161173
} else {
162-
return historyRepository.save(History.builder().memberId(member).build());
174+
return historyRepository.save(History.builder().memberId(member.getMemberId()).build());
163175
}
164176
}
165177

@@ -178,4 +190,24 @@ private ChatResponse handleError(History history) {
178190
.message("죄송합니다. 서비스 처리 중 오류가 발생했습니다. 요청을 다시 전송해 주세요.")
179191
.build();
180192
}
193+
194+
/**
195+
* 블로킹 작업에서 준비된 데이터를 담는 컨텍스트 클래스
196+
* 리액티브 체인에서 데이터를 전달하기 위한 내부 클래스
197+
*/
198+
private static class PreparedChatContext {
199+
final Prompt prompt;
200+
final History history;
201+
final List<Document> similarCaseDocuments;
202+
final List<Document> similarLawDocuments;
203+
204+
PreparedChatContext(Prompt prompt, History history,
205+
List<Document> similarCaseDocuments,
206+
List<Document> similarLawDocuments) {
207+
this.prompt = prompt;
208+
this.history = history;
209+
this.similarCaseDocuments = similarCaseDocuments;
210+
this.similarLawDocuments = similarLawDocuments;
211+
}
212+
}
181213
}

backend/src/main/java/com/ai/lawyer/domain/chatbot/service/HistoryService.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import com.ai.lawyer.domain.chatbot.entity.History;
77
import com.ai.lawyer.domain.chatbot.exception.HistoryNotFoundException;
88
import com.ai.lawyer.domain.chatbot.repository.HistoryRepository;
9-
import com.ai.lawyer.domain.member.entity.Member;
109
import com.ai.lawyer.domain.member.repositories.MemberRepository;
1110
import com.ai.lawyer.infrastructure.redis.service.ChatCacheService;
1211
import lombok.RequiredArgsConstructor;
@@ -28,11 +27,12 @@ public class HistoryService {
2827

2928
public List<HistoryDto> getHistoryTitle(Long memberId) {
3029

31-
Member member = memberRepository.findById(memberId).orElseThrow(
30+
// 회원 존재 여부 확인
31+
memberRepository.findById(memberId).orElseThrow(
3232
() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
3333
);
3434

35-
List<History> rooms = historyRepository.findAllByMemberId(member);
35+
List<History> rooms = historyRepository.findAllByMemberId(memberId);
3636
List<HistoryDto> roomDtos = new ArrayList<>();
3737

3838
for (History room : rooms)
@@ -45,11 +45,12 @@ public String deleteHistory(Long memberId, Long roomId) {
4545

4646
getHistory(roomId);
4747

48-
Member member = memberRepository.findById(memberId).orElseThrow(
48+
// 회원 존재 여부 확인
49+
memberRepository.findById(memberId).orElseThrow(
4950
() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
5051
);
5152

52-
History room = historyRepository.findByHistoryIdAndMemberId(roomId, member);
53+
History room = historyRepository.findByHistoryIdAndMemberId(roomId, memberId);
5354

5455
historyRepository.delete(room);
5556
chatCacheService.clearChatHistory(roomId);
@@ -61,7 +62,8 @@ public String deleteHistory(Long memberId, Long roomId) {
6162
@Transactional(readOnly = true)
6263
public ResponseEntity<List<ChatHistoryDto>> getChatHistory(Long memberId, Long roomId) {
6364

64-
Member member = memberRepository.findById(memberId).orElseThrow(
65+
// 회원 존재 여부 확인
66+
memberRepository.findById(memberId).orElseThrow(
6567
() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
6668
);
6769

@@ -72,7 +74,7 @@ public ResponseEntity<List<ChatHistoryDto>> getChatHistory(Long memberId, Long r
7274
}
7375

7476
// 2. DB에서 조회 후 캐시에 저장
75-
History history = historyRepository.findByHistoryIdAndMemberId(roomId, member);
77+
History history = historyRepository.findByHistoryIdAndMemberId(roomId, memberId);
7678
List<Chat> chats = history.getChats();
7779

7880
// 엔티티 -> DTO 변환

backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ public class PollVote {
2121
@JoinColumn(name = "poll_id", nullable = false, foreignKey = @ForeignKey(name = "FK_POLLVOTE_POLL"))
2222
private Poll poll;
2323

24+
// Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거
25+
// 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장
26+
// foreignKey 제약조건 비활성화 (ConstraintMode.NO_CONSTRAINT)
2427
@ManyToOne(fetch = FetchType.LAZY)
25-
@JoinColumn(name = "member_id", nullable = false, foreignKey = @ForeignKey(name = "FK_POLLVOTE_MEMBER"))
28+
@JoinColumn(name = "member_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
2629
private Member member;
2730

2831
@ManyToOne(fetch = FetchType.LAZY)

backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
import com.ai.lawyer.domain.poll.entity.*;
55
import com.ai.lawyer.domain.poll.repository.*;
66
import com.ai.lawyer.domain.member.entity.Member;
7-
import com.ai.lawyer.domain.post.dto.PostDto;
87
import com.ai.lawyer.domain.post.entity.Post;
98
import com.ai.lawyer.domain.post.repository.PostRepository;
109
import lombok.RequiredArgsConstructor;
1110
import lombok.extern.slf4j.Slf4j;
12-
import org.springframework.data.domain.Page;
1311
import org.springframework.http.HttpStatus;
1412
import org.springframework.stereotype.Service;
1513
import org.springframework.transaction.annotation.Transactional;
@@ -242,7 +240,7 @@ public void closePoll(Long pollId) {
242240
public void deletePoll(Long pollId, Long memberId) {
243241
Poll poll = pollRepository.findById(pollId)
244242
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다."));
245-
if (poll.getPost() == null || !poll.getPost().getMember().getMemberId().equals(memberId)) {
243+
if (poll.getPost() == null || !poll.getPost().getMemberId().equals(memberId)) {
246244
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 삭제할 수 있습니다.");
247245
}
248246
// 1. 이 Poll을 참조하는 Post가 있으면 연결 해제
@@ -312,7 +310,7 @@ public Long getVoteCountByPostId(Long postId) {
312310
public PollDto updatePoll(Long pollId, PollUpdateDto pollUpdateDto, Long memberId) {
313311
Poll poll = pollRepository.findById(pollId)
314312
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "수정할 투표를 찾을 수 없습니다."));
315-
if (!poll.getPost().getMember().getMemberId().equals(memberId)) {
313+
if (!poll.getPost().getMemberId().equals(memberId)) {
316314
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "본인만 투표를 수정할 수 있습니다.");
317315
}
318316
if (getVoteCountByPollId(pollId) > 0) {

backend/src/main/java/com/ai/lawyer/domain/post/entity/Post.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.ai.lawyer.domain.post.entity;
22

3-
import com.ai.lawyer.domain.member.entity.Member;
43
import com.ai.lawyer.domain.poll.entity.Poll;
54
import jakarta.persistence.*;
65
import lombok.*;
@@ -21,9 +20,10 @@ public class Post {
2120
@Column(name = "post_id")
2221
private Long postId;
2322

24-
@ManyToOne(fetch = FetchType.LAZY)
25-
@JoinColumn(name = "member_id", nullable = true, foreignKey = @ForeignKey(name = "FK_POST_MEMBER"))
26-
private Member member;
23+
// Member와 OAuth2Member 모두 지원하기 위해 FK 제약 조건 제거 (ConstraintMode.NO_CONSTRAINT)
24+
// member_id를 직접 저장하고, 애플리케이션 레벨에서 AuthUtil로 참조 무결성 보장
25+
@Column(name = "member_id")
26+
private Long memberId;
2727

2828
@Column(name = "post_name", length = 100, nullable = false)
2929
private String postName;

0 commit comments

Comments
 (0)