Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/main/java/com/ai/lawyer/BackendApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
@EnableJpaAuditing
@ConfigurationPropertiesScan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
import com.ai.lawyer.domain.chatbot.dto.HistoryDto;
import com.ai.lawyer.domain.chatbot.service.ChatService;
import com.ai.lawyer.domain.chatbot.service.HistoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -21,7 +20,6 @@
public class HistoryController {

private final HistoryService historyService;
private final ChatService chatService;

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

@Operation(summary = "채팅방 삭제")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ public static class ChatResponse {
@Schema(description = "채팅방 ID", example = "1")
private Long roomId;

@Schema(description = "History 방 제목", example = "손해배상 청구 관련 문의")
private String title;

@Schema(description = "AI 챗봇의 응답 메시지", example = "네, 관련 법령과 판례를 바탕으로 답변해 드리겠습니다.")
private String message;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ public class Chat {
@Lob
private String message;

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

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

@CreationTimestamp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class History {
private Long historyId;

@ManyToOne
@JoinColumn(name = "member_id")
@JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER"))
private Member memberId;

@OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

import com.ai.lawyer.domain.chatbot.entity.ChatLaw;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ChatLawRepository extends JpaRepository<ChatLaw, Long> {

/**
* member_id에 해당하는 모든 ChatLaw 삭제 (회원 탈퇴 시 사용)
*/
@Modifying
@Query("DELETE FROM ChatLaw cl WHERE cl.chatId.historyId.memberId.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

import com.ai.lawyer.domain.chatbot.entity.ChatPrecedent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ChatPrecedentRepository extends JpaRepository<ChatPrecedent, Long> {

/**
* member_id에 해당하는 모든 ChatPrecedent 삭제 (회원 탈퇴 시 사용)
*/
@Modifying
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

import com.ai.lawyer.domain.chatbot.entity.Chat;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ChatRepository extends JpaRepository<Chat, Long> {

/**
* member_id에 해당하는 모든 Chat 삭제 (회원 탈퇴 시 사용)
*/
@Modifying
@Query("DELETE FROM Chat c WHERE c.historyId.memberId.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

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

History findByHistoryIdAndMemberId(Long roomId, Member memberId);

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.ai.lawyer.domain.chatbot.service;

import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.KeywordExtractionDto;
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.TitleExtractionDto;
import com.ai.lawyer.domain.chatbot.entity.*;
import com.ai.lawyer.domain.chatbot.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.document.Document;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class AsyncPostChatProcessingService {

private final KeywordService keywordService;
private final HistoryRepository historyRepository;
private final ChatRepository chatRepository;
private final KeywordRankRepository keywordRankRepository;
private final ChatMemoryRepository chatMemoryRepository;
private final ChatPrecedentRepository chatPrecedentRepository;
private final ChatLawRepository chatLawRepository;

@Value("${custom.ai.title-extraction}")
private String titleExtraction;
@Value("{$custom.ai.keyword-extraction}")
private String keywordExtraction;

@Async
@Transactional
public void processHandlerTasks(Long historyId, String userMessage, String fullResponse, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
try {
History history = historyRepository.findById(historyId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채팅방입니다. historyId: " + historyId));

// 1. 메시지 기억 저장
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.chatMemoryRepository(chatMemoryRepository)
.build();

chatMemory.add(String.valueOf(history.getHistoryId()), new AssistantMessage(fullResponse));
chatMemoryRepository.saveAll(String.valueOf(history.getHistoryId()), chatMemory.get(String.valueOf(history.getHistoryId())));

// 2. 채팅방 제목 설정 / 및 필터
setHistoryTitle(userMessage, history, fullResponse);

// 3. 채팅 기록 저장
saveChatWithDocuments(history, MessageType.USER, userMessage, similarCaseDocuments, similarLawDocuments);
saveChatWithDocuments(history, MessageType.ASSISTANT, fullResponse, similarCaseDocuments, similarLawDocuments);

// 4. 키워드 추출 및 랭킹 업데이트
if (!fullResponse.contains("해당 질문은 법률")) {
extractAndUpdateKeywordRanks(userMessage);
}
} catch (Exception e) {
log.error("에러 발생: {}", historyId, e);
}
}

private void setHistoryTitle(String userMessage, History history, String fullResponse) {
String targetText = fullResponse.contains("해당 질문은 법률") ? userMessage : fullResponse;
TitleExtractionDto titleDto = keywordService.keywordExtract(targetText, titleExtraction, TitleExtractionDto.class);
history.setTitle(titleDto.getTitle());
historyRepository.save(history); // @Transactional 어노테이션으로 인해 메소드 종료 시 자동 저장되지만, 명시적으로 호출할 수도 있습니다.
}

private void extractAndUpdateKeywordRanks(String message) {
KeywordExtractionDto keywordResponse = keywordService.keywordExtract(message, keywordExtraction, KeywordExtractionDto.class);
if (keywordResponse == null || keywordResponse.getKeyword() == null) {
return;
}

KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());

if (keywordRank == null) {
keywordRank = KeywordRank.builder()
.keyword(keywordResponse.getKeyword())
.score(1L)
.build();
} else {
keywordRank.setScore(keywordRank.getScore() + 1);
}
keywordRankRepository.save(keywordRank);
}

private void saveChatWithDocuments(History history, MessageType type, String message, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
Chat chat = chatRepository.save(Chat.builder()
.historyId(history)
.type(type)
.message(message)
.build());

// Ai 메시지가 저장될 때 관련 문서 저장
if (type == MessageType.ASSISTANT) {
if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) {
List<ChatPrecedent> chatPrecedents = similarCaseDocuments.stream()
.map(doc -> ChatPrecedent.builder()
.chatId(chat)
.precedentContent(doc.getText())
.caseNumber(doc.getMetadata().get("caseNumber").toString())
.caseName(doc.getMetadata().get("caseName").toString())
.build())
.collect(Collectors.toList());
chatPrecedentRepository.saveAll(chatPrecedents);
}

if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) {
List<ChatLaw> chatLaws = similarLawDocuments.stream()
.map(doc -> ChatLaw.builder()
.chatId(chat)
.content(doc.getText())
.lawName(doc.getMetadata().get("lawName").toString())
.build())
.collect(Collectors.toList());
chatLawRepository.saveAll(chatLaws);
}
}
}
}
Loading