Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
484ef47
chore[initdate]: ddl 타입 재적용
DooHyoJeong Oct 15, 2025
d1a9d4d
chore[infra]: sql 로그 줄이기
DooHyoJeong Oct 15, 2025
2f5541e
Merge pull request #319 from prgrms-web-devcourse-final-project/chore…
DooHyoJeong Oct 15, 2025
c23dee2
Fix[post]:게시글 삭제, 본인 게시물 404 삭제
GarakChoi Oct 15, 2025
bd8a1a2
Merge pull request #321 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
532e5ae
Fix[post]:gender enum
GarakChoi Oct 15, 2025
4c70d56
Merge pull request #322 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
913721b
fix[member]:소셜 로그인으로 진행했을 때 토큰으로 member_id를 조회하는 로직에 대한 버그 수정
asowjdan Oct 15, 2025
843a66c
Merge pull request #323 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
c37aa81
fix[member]:소셜 로그인으로 진행했을 때 토큰으로 member_id를 조회하는 로직에 대한 버그 수정
asowjdan Oct 15, 2025
9faad87
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
d97dfbb
Merge pull request #325 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
cc4e8e1
fix[member]:소셜 로그인으로 진행했을 때 토큰으로 member_id를 조회하는 로직에 대한 버그 수정
asowjdan Oct 15, 2025
e4d5e30
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
81f3403
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
b094b0c
Merge pull request #327 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
9ba2cb1
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
ae19174
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
447913d
Merge pull request #329 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
3181c14
Fix[post]:work
GarakChoi Oct 15, 2025
c74b530
Merge pull request #330 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
49d9c16
fix[poll]:투표하기 예외처리 추가
GarakChoi Oct 15, 2025
1629f52
Merge pull request #332 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
e78675f
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
3e7cc84
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
c36d124
Merge pull request #334 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
ea1ede8
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
ba09634
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
bf83541
fix[post]:투표 예약시간 강제 종료시 현재 시간으로 바뀌게함
GarakChoi Oct 15, 2025
2ced5c0
Merge pull request #336 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
04ca166
Merge pull request #337 from asowjdan/fix/member
DooHyoJeong Oct 15, 2025
190b897
docker: kafka 버전 업
yongho9064 Oct 15, 2025
d65bbcb
Merge pull request #339 from prgrms-web-devcourse-final-project/fix/k…
DooHyoJeong Oct 15, 2025
445f7e2
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
2411f3b
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
6ad58fa
Merge pull request #341 from asowjdan/fix/member
asowjdan Oct 15, 2025
fc4e6a9
work
GarakChoi Oct 15, 2025
2e04a63
Fix[poll]:Long memberId
GarakChoi Oct 15, 2025
a909249
fix[member]:토큰 생성 로직 수정으로 인한 테스트 코드 수정
asowjdan Oct 15, 2025
953a22d
Merge pull request #343 from prgrms-web-devcourse-final-project/feat/…
GarakChoi Oct 15, 2025
292c74c
Merge branch 'develop' into fix/member
asowjdan Oct 15, 2025
f49db6e
Merge pull request #344 from asowjdan/fix/member
asowjdan Oct 15, 2025
c6c291d
work
GarakChoi Oct 15, 2025
12a5d81
Merge pull request #346 from prgrms-web-devcourse-final-project/feat/…
asowjdan Oct 15, 2025
f4d2749
fix[bug]:챗봇 오류 수정 및 중복 투표 방지 로직 오류 수정
asowjdan Oct 15, 2025
d2df9de
Merge pull request #348 from asowjdan/fix/member
Nohheechul Oct 16, 2025
f23c7f9
fix: 권한 에러
yongho9064 Oct 16, 2025
e58093a
Merge pull request #349 from prgrms-web-devcourse-final-project/fix/auth
GarakChoi Oct 16, 2025
c91a161
fix: 채팅 에러
yongho9064 Oct 16, 2025
f43978f
Merge pull request #351 from prgrms-web-devcourse-final-project/fix/c…
yongho9064 Oct 16, 2025
73ee202
fix[chat]: 지저분한 코드 수정
yongho9064 Oct 16, 2025
f838244
Merge pull request #353 from prgrms-web-devcourse-final-project/ref/chat
GarakChoi Oct 16, 2025
d353f6c
fix[kafka]: 수정
yongho9064 Oct 16, 2025
da8ccc7
Merge pull request #354 from prgrms-web-devcourse-final-project/fix/k…
DooHyoJeong Oct 16, 2025
cca6321
chore[chat]: @Async 주석 해제
yongho9064 Oct 16, 2025
4ff56d1
Merge pull request #356 from prgrms-web-devcourse-final-project/feat/…
DooHyoJeong Oct 17, 2025
a307f6b
chore[infra] : kafka 주석
DooHyoJeong Oct 17, 2025
082e4e8
chore[infra] : .test용 env 파일 설정
DooHyoJeong Oct 17, 2025
6d85edb
Merge pull request #358 from prgrms-web-devcourse-final-project/chore…
DooHyoJeong Oct 17, 2025
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: 1 addition & 1 deletion .github/workflows/CI-CD_Pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ jobs:

# prod.env 복원
install -d -m 700 /home/ec2-user/configs
printf "%s" "${{ secrets.PROD_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env
printf "%s" "${{ secrets.PROD_TEST_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env
chmod 600 /home/ec2-user/configs/prod.env
test -s /home/ec2-user/configs/prod.env || { echo "prod.env empty"; exit 1; }

Expand Down
1 change: 0 additions & 1 deletion backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.kafka:spring-kafka'
testImplementation 'org.springframework.kafka:spring-kafka-test'

// API Documentation (문서화)
Expand Down
34 changes: 1 addition & 33 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,8 @@ services:
timeout: 5s
retries: 10

zookeeper:
image: confluentinc/cp-zookeeper:7.4.4
container_name: zookeeper
restart: unless-stopped
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

kafka:
image: confluentinc/cp-kafka:7.4.4
container_name: kafka
restart: unless-stopped
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
volumes:
- kafka-data:/var/lib/kafka/data

volumes:
mysql-data:
redis-data:
qdrant-data:
ollama-data:
kafka-data:
ollama-data:
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,45 @@
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatRequest;
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatResponse;
import com.ai.lawyer.domain.chatbot.service.ChatBotService;
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.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@Slf4j
@Tag(name = "ChatBot API", description = "챗봇 관련 API")
@Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chat")
public class ChatBotController {

private final ChatBotService chatBotService;

@Operation(summary = "01. 새로운 채팅", description = "첫 메시지 전송으로 새로운 채팅방을 생성하고 챗봇과 대화를 시작")
@PostMapping("/message")
public ResponseEntity<Flux<ChatResponse>> postNewMessage(
@AuthenticationPrincipal Long memberId,
@RequestBody ChatRequest chatRequest) {
return ResponseEntity.ok(chatBotService.sendMessage(memberId, chatRequest, null));
@PostMapping(value = "/message")
public Flux<ChatResponse> postNewMessage(@RequestBody ChatRequest chatRequest) {

Long memberId = AuthUtil.getAuthenticatedMemberId();

log.info("새로운 채팅 요청: memberId={}", memberId);

return chatBotService.sendMessage(memberId, chatRequest, null);
}

@Operation(summary = "02. 기존 채팅", description = "기존 채팅방에 메시지를 보내고 챗봇과 대화를 이어감")
@PostMapping("{roomId}/message")
public ResponseEntity<Flux<ChatResponse>> postMessage(@AuthenticationPrincipal Long memberId, @RequestBody ChatRequest chatRequest, @PathVariable(value = "roomId", required = false) Long roomId) {
return ResponseEntity.ok(chatBotService.sendMessage(memberId, chatRequest, roomId));
@PostMapping(value = "{roomId}/message")
public Flux<ChatResponse> postMessage(
@RequestBody ChatRequest chatRequest,
@PathVariable(value = "roomId", required = false) Long roomId) {

Long memberId = AuthUtil.getAuthenticatedMemberId();

log.info("기존 채팅 요청: memberId={}, roomId={}", memberId, roomId);

return chatBotService.sendMessage(memberId, chatRequest, roomId);
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Chat> chats;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ public interface ChatLawRepository extends JpaRepository<ChatLaw, Long> {

/**
* 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ public interface ChatPrecedentRepository extends JpaRepository<ChatPrecedent, Lo

/**
* member_id에 해당하는 모든 ChatPrecedent 삭제 (회원 탈퇴 시 사용)
* History.memberId가 Long 타입이므로 직접 비교
*/
@Modifying
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId.memberId = :memberId")
@Query("DELETE FROM ChatPrecedent cp WHERE cp.chatId.historyId.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ public interface ChatRepository extends JpaRepository<Chat, Long> {

/**
* 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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,16 +12,17 @@
@Repository
public interface HistoryRepository extends JpaRepository<History, Long> {

List<History> findAllByMemberId(Member memberId);
// member_id로 직접 조회 (Member, OAuth2Member 모두 지원)
List<History> 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);

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

import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatLawDto;
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatPrecedentDto;
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 com.ai.lawyer.infrastructure.redis.service.ChatCacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
Expand All @@ -13,9 +17,11 @@
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.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -27,6 +33,8 @@
public class AsyncPostChatProcessingService {

private final KeywordService keywordService;
private final ChatCacheService chatCacheService;

private final HistoryRepository historyRepository;
private final ChatRepository chatRepository;
private final KeywordRankRepository keywordRankRepository;
Expand All @@ -39,7 +47,7 @@ public class AsyncPostChatProcessingService {
@Value("{$custom.ai.keyword-extraction}")
private String keywordExtraction;

//@Async
@Async
@Transactional
public void processHandlerTasks(Long historyId, String userMessage, String fullResponse, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
try {
Expand Down Expand Up @@ -98,6 +106,9 @@ private void extractAndUpdateKeywordRanks(String message) {
}

private void saveChatWithDocuments(History history, MessageType type, String message, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
List<ChatPrecedent> chatPrecedents = new ArrayList<>();
List<ChatLaw> chatLaws = new ArrayList<>();

Chat chat = chatRepository.save(Chat.builder()
.historyId(history)
.type(type)
Expand All @@ -107,7 +118,7 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
// Ai 메시지가 저장될 때 관련 문서 저장
if (type == MessageType.ASSISTANT) {
if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) {
List<ChatPrecedent> chatPrecedents = similarCaseDocuments.stream()
chatPrecedents = similarCaseDocuments.stream()
.map(doc -> ChatPrecedent.builder()
.chatId(chat)
.precedentContent(doc.getText())
Expand All @@ -119,7 +130,7 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
}

if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) {
List<ChatLaw> chatLaws = similarLawDocuments.stream()
chatLaws = similarLawDocuments.stream()
.map(doc -> ChatLaw.builder()
.chatId(chat)
.content(doc.getText())
Expand All @@ -129,5 +140,16 @@ private void saveChatWithDocuments(History history, MessageType type, String mes
chatLawRepository.saveAll(chatLaws);
}
}

// Redis 캐시에 DTO 저장
ChatHistoryDto dto = ChatHistoryDto.builder()
.type(type.toString())
.message(message)
.createdAt(chat.getCreatedAt())
.precedent(chatPrecedents.isEmpty() ? null : ChatPrecedentDto.from(chatPrecedents.get(0)))
.law(chatLaws.isEmpty() ? null : ChatLawDto.from(chatLaws.get(0)))
.build();

chatCacheService.cacheChatMessage(history.getHistoryId(), dto);
}
}
Loading