Skip to content

Commit 3570d17

Browse files
authored
Merge pull request #267 from prgrms-web-devcourse-final-project/develop
배포
2 parents c3b854a + d5d60d3 commit 3570d17

28 files changed

+655
-291
lines changed

backend/src/main/java/com/ai/lawyer/BackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
66
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7+
import org.springframework.scheduling.annotation.EnableAsync;
78

9+
@EnableAsync
810
@SpringBootApplication
911
@EnableJpaAuditing
1012
@ConfigurationPropertiesScan

backend/src/main/java/com/ai/lawyer/domain/chatbot/controller/HistoryController.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
44
import com.ai.lawyer.domain.chatbot.dto.HistoryDto;
5-
import com.ai.lawyer.domain.chatbot.service.ChatService;
65
import com.ai.lawyer.domain.chatbot.service.HistoryService;
76
import io.swagger.v3.oas.annotations.Operation;
87
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -21,7 +20,6 @@
2120
public class HistoryController {
2221

2322
private final HistoryService historyService;
24-
private final ChatService chatService;
2523

2624
@Operation(summary = "채팅방 제목 목록 조회")
2725
@GetMapping("/")
@@ -32,7 +30,7 @@ public ResponseEntity<List<HistoryDto>> getHistoryTitles(@AuthenticationPrincipa
3230
@Operation(summary = "채팅 조회")
3331
@GetMapping("/{historyId}")
3432
public ResponseEntity<List<ChatHistoryDto>> getChatHistory(@AuthenticationPrincipal Long memberId, @PathVariable("historyId") Long roomId) {
35-
return chatService.getChatHistory(memberId, roomId);
33+
return historyService.getChatHistory(memberId, roomId);
3634
}
3735

3836
@Operation(summary = "채팅방 삭제")

backend/src/main/java/com/ai/lawyer/domain/chatbot/dto/ChatDto.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public static class ChatResponse {
3434
@Schema(description = "채팅방 ID", example = "1")
3535
private Long roomId;
3636

37-
@Schema(description = "History 방 제목", example = "손해배상 청구 관련 문의")
38-
private String title;
39-
4037
@Schema(description = "AI 챗봇의 응답 메시지", example = "네, 관련 법령과 판례를 바탕으로 답변해 드리겠습니다.")
4138
private String message;
4239

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ public class Chat {
3333
@Lob
3434
private String message;
3535

36-
@OneToMany(mappedBy = "chatId")
36+
@OneToMany(mappedBy = "chatId", cascade = CascadeType.ALL, orphanRemoval = true)
3737
private List<ChatPrecedent> chatPrecedents;
3838

39-
@OneToMany(mappedBy = "chatId")
39+
@OneToMany(mappedBy = "chatId", cascade = CascadeType.ALL, orphanRemoval = true)
4040
private List<ChatLaw> chatLaws;
4141

4242
@CreationTimestamp

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class History {
2525
private Long historyId;
2626

2727
@ManyToOne
28-
@JoinColumn(name = "member_id")
28+
@JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER"))
2929
private Member memberId;
3030

3131
@OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22

33
import com.ai.lawyer.domain.chatbot.entity.ChatLaw;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58
import org.springframework.stereotype.Repository;
69

710
@Repository
811
public interface ChatLawRepository extends JpaRepository<ChatLaw, Long> {
12+
13+
/**
14+
* member_id에 해당하는 모든 ChatLaw 삭제 (회원 탈퇴 시 사용)
15+
*/
16+
@Modifying
17+
@Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId.memberId = :memberId")
18+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
919
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
import com.ai.lawyer.domain.chatbot.entity.ChatPrecedent;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58

69
public interface ChatPrecedentRepository extends JpaRepository<ChatPrecedent, Long> {
10+
11+
/**
12+
* member_id에 해당하는 모든 ChatPrecedent 삭제 (회원 탈퇴 시 사용)
13+
*/
14+
@Modifying
15+
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId.memberId = :memberId")
16+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
717
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22

33
import com.ai.lawyer.domain.chatbot.entity.Chat;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58
import org.springframework.stereotype.Repository;
69

710
@Repository
811
public interface ChatRepository extends JpaRepository<Chat, Long> {
12+
13+
/**
14+
* member_id에 해당하는 모든 Chat 삭제 (회원 탈퇴 시 사용)
15+
*/
16+
@Modifying
17+
@Query("DELETE FROM Chat c WHERE c.historyId.memberId.memberId = :memberId")
18+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
919
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import com.ai.lawyer.domain.chatbot.entity.History;
44
import com.ai.lawyer.domain.member.entity.Member;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
69
import org.springframework.stereotype.Repository;
710

811
import java.util.List;
@@ -14,4 +17,12 @@ public interface HistoryRepository extends JpaRepository<History, Long> {
1417

1518
History findByHistoryIdAndMemberId(Long roomId, Member memberId);
1619

20+
/**
21+
* member_id로 채팅 히스토리 삭제 (회원 탈퇴 시 사용)
22+
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
23+
*/
24+
@Modifying
25+
@Query("DELETE FROM History h WHERE h.memberId.memberId = :memberId")
26+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
27+
1728
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.ai.lawyer.domain.chatbot.service;
2+
3+
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.KeywordExtractionDto;
4+
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.TitleExtractionDto;
5+
import com.ai.lawyer.domain.chatbot.entity.*;
6+
import com.ai.lawyer.domain.chatbot.repository.*;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.ai.chat.memory.ChatMemory;
10+
import org.springframework.ai.chat.memory.ChatMemoryRepository;
11+
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
12+
import org.springframework.ai.chat.messages.AssistantMessage;
13+
import org.springframework.ai.chat.messages.MessageType;
14+
import org.springframework.ai.document.Document;
15+
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.scheduling.annotation.Async;
17+
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.annotation.Transactional;
19+
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
@Slf4j
24+
@Service
25+
@RequiredArgsConstructor
26+
public class AsyncPostChatProcessingService {
27+
28+
private final KeywordService keywordService;
29+
private final HistoryRepository historyRepository;
30+
private final ChatRepository chatRepository;
31+
private final KeywordRankRepository keywordRankRepository;
32+
private final ChatMemoryRepository chatMemoryRepository;
33+
private final ChatPrecedentRepository chatPrecedentRepository;
34+
private final ChatLawRepository chatLawRepository;
35+
36+
@Value("${custom.ai.title-extraction}")
37+
private String titleExtraction;
38+
@Value("{$custom.ai.keyword-extraction}")
39+
private String keywordExtraction;
40+
41+
@Async
42+
@Transactional
43+
public void processHandlerTasks(Long historyId, String userMessage, String fullResponse, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
44+
try {
45+
History history = historyRepository.findById(historyId)
46+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채팅방입니다. historyId: " + historyId));
47+
48+
// 1. 메시지 기억 저장
49+
ChatMemory chatMemory = MessageWindowChatMemory.builder()
50+
.maxMessages(10)
51+
.chatMemoryRepository(chatMemoryRepository)
52+
.build();
53+
54+
chatMemory.add(String.valueOf(history.getHistoryId()), new AssistantMessage(fullResponse));
55+
chatMemoryRepository.saveAll(String.valueOf(history.getHistoryId()), chatMemory.get(String.valueOf(history.getHistoryId())));
56+
57+
// 2. 채팅방 제목 설정 / 및 필터
58+
setHistoryTitle(userMessage, history, fullResponse);
59+
60+
// 3. 채팅 기록 저장
61+
saveChatWithDocuments(history, MessageType.USER, userMessage, similarCaseDocuments, similarLawDocuments);
62+
saveChatWithDocuments(history, MessageType.ASSISTANT, fullResponse, similarCaseDocuments, similarLawDocuments);
63+
64+
// 4. 키워드 추출 및 랭킹 업데이트
65+
if (!fullResponse.contains("해당 질문은 법률")) {
66+
extractAndUpdateKeywordRanks(userMessage);
67+
}
68+
} catch (Exception e) {
69+
log.error("에러 발생: {}", historyId, e);
70+
}
71+
}
72+
73+
private void setHistoryTitle(String userMessage, History history, String fullResponse) {
74+
String targetText = fullResponse.contains("해당 질문은 법률") ? userMessage : fullResponse;
75+
TitleExtractionDto titleDto = keywordService.keywordExtract(targetText, titleExtraction, TitleExtractionDto.class);
76+
history.setTitle(titleDto.getTitle());
77+
historyRepository.save(history); // @Transactional 어노테이션으로 인해 메소드 종료 시 자동 저장되지만, 명시적으로 호출할 수도 있습니다.
78+
}
79+
80+
private void extractAndUpdateKeywordRanks(String message) {
81+
KeywordExtractionDto keywordResponse = keywordService.keywordExtract(message, keywordExtraction, KeywordExtractionDto.class);
82+
if (keywordResponse == null || keywordResponse.getKeyword() == null) {
83+
return;
84+
}
85+
86+
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());
87+
88+
if (keywordRank == null) {
89+
keywordRank = KeywordRank.builder()
90+
.keyword(keywordResponse.getKeyword())
91+
.score(1L)
92+
.build();
93+
} else {
94+
keywordRank.setScore(keywordRank.getScore() + 1);
95+
}
96+
keywordRankRepository.save(keywordRank);
97+
}
98+
99+
private void saveChatWithDocuments(History history, MessageType type, String message, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
100+
Chat chat = chatRepository.save(Chat.builder()
101+
.historyId(history)
102+
.type(type)
103+
.message(message)
104+
.build());
105+
106+
// Ai 메시지가 저장될 때 관련 문서 저장
107+
if (type == MessageType.ASSISTANT) {
108+
if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) {
109+
List<ChatPrecedent> chatPrecedents = similarCaseDocuments.stream()
110+
.map(doc -> ChatPrecedent.builder()
111+
.chatId(chat)
112+
.precedentContent(doc.getText())
113+
.caseNumber(doc.getMetadata().get("caseNumber").toString())
114+
.caseName(doc.getMetadata().get("caseName").toString())
115+
.build())
116+
.collect(Collectors.toList());
117+
chatPrecedentRepository.saveAll(chatPrecedents);
118+
}
119+
120+
if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) {
121+
List<ChatLaw> chatLaws = similarLawDocuments.stream()
122+
.map(doc -> ChatLaw.builder()
123+
.chatId(chat)
124+
.content(doc.getText())
125+
.lawName(doc.getMetadata().get("lawName").toString())
126+
.build())
127+
.collect(Collectors.toList());
128+
chatLawRepository.saveAll(chatLaws);
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)