Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
19d400f
feat[dependencies]: Qdrant, OpenAI, JDBC 기반 채팅 메모리 저장소용 Spring AI 추가
yongho9064 Sep 26, 2025
587dfac
feat[config]: AI 관련 Bean 설정 추가 (ChatMemoryRepository, OpenAI ChatClie…
yongho9064 Sep 26, 2025
da1c8da
feat[chat]: 회원 채팅 기록 조회 기능(ChatService) 추가
yongho9064 Sep 26, 2025
f29cae1
feat[chat]: ChatMemory 엔티티 및 복합키 추가
yongho9064 Sep 26, 2025
f83501e
feat[chat]: ChatBotService 추가 - AI 메시지 처리, 벡터 검색, 채팅 기억, 키워드/제목 추출 포함
yongho9064 Sep 26, 2025
3cb984e
feat[exception]: 글로벌 예외 처리 추가
yongho9064 Sep 26, 2025
d0b0c4d
feat[chat]: 채팅방 조회, 삭제
yongho9064 Sep 26, 2025
9054ed7
feat[chat]: 상위 키워드 조회 기능 추가
yongho9064 Sep 26, 2025
a04776d
feat[vector]: 벡터 스토어 카운팅 검사
yongho9064 Sep 26, 2025
f7c66af
feat[docs]: SpringDocConfig에 챗봇 관련 API 추가
yongho9064 Sep 26, 2025
b0e47ac
chore[chat]: ChatMemory 주석 처리
yongho9064 Sep 26, 2025
b51f685
feat[ai/config]: AI 시스템 메시지 및 제목/키워드 추출 프롬프트 설정 추가
yongho9064 Sep 26, 2025
e92a63c
feat[vector]: 법령 및 판례 데이터를 Qdrant 벡터 스토어에 로드
yongho9064 Sep 26, 2025
a697611
chore[docker]: Qdrant 서비스 Docker Compose 설정 추가
yongho9064 Sep 26, 2025
8a81963
chore[config]: Spring 설정 yml 업데이트
yongho9064 Sep 26, 2025
dc4eaf7
feat[db]: SPRING_AI_CHAT_MEMORY 테이블 생성 (MySQL)
yongho9064 Sep 26, 2025
69771f7
feat[dependencies]: Qdrant, OpenAI, JDBC 기반 채팅 메모리 저장소용 Spring AI 추가
yongho9064 Sep 26, 2025
3454512
feat[config]: AI 관련 Bean 설정 추가 (ChatMemoryRepository, OpenAI ChatClie…
yongho9064 Sep 26, 2025
fa3d196
feat[chat]: 회원 채팅 기록 조회 기능(ChatService) 추가
yongho9064 Sep 26, 2025
299d1c5
feat[chat]: ChatMemory 엔티티 및 복합키 추가
yongho9064 Sep 26, 2025
811426b
feat[chat]: ChatBotService 추가 - AI 메시지 처리, 벡터 검색, 채팅 기억, 키워드/제목 추출 포함
yongho9064 Sep 26, 2025
e243b2f
feat[exception]: 글로벌 예외 처리 추가
yongho9064 Sep 26, 2025
e261965
feat[chat]: 채팅방 조회, 삭제
yongho9064 Sep 26, 2025
a4fd974
feat[chat]: 상위 키워드 조회 기능 추가
yongho9064 Sep 26, 2025
18ca31f
feat[vector]: 벡터 스토어 카운팅 검사
yongho9064 Sep 26, 2025
ca314e0
feat[docs]: SpringDocConfig에 챗봇 관련 API 추가
yongho9064 Sep 26, 2025
c2b6397
chore[chat]: ChatMemory 주석 처리
yongho9064 Sep 26, 2025
1d91c65
feat[ai/config]: AI 시스템 메시지 및 제목/키워드 추출 프롬프트 설정 추가
yongho9064 Sep 26, 2025
eb9b851
feat[vector]: 법령 및 판례 데이터를 Qdrant 벡터 스토어에 로드
yongho9064 Sep 26, 2025
ddf7458
chore[docker]: Qdrant 서비스 Docker Compose 설정 추가
yongho9064 Sep 26, 2025
fc71ca5
chore[config]: Spring 설정 yml 업데이트
yongho9064 Sep 26, 2025
c856ffb
feat[db]: SPRING_AI_CHAT_MEMORY 테이블 생성 (MySQL)
yongho9064 Sep 26, 2025
9bbef97
Merge remote-tracking branch 'origin/feat/ai' into feat/ai
yongho9064 Sep 26, 2025
35c1741
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
262fd6a
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
ad6afc6
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
cd18db1
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
e022560
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
86e4814
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
461c011
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
3dbdff4
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
b1d4842
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
be32cb0
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 2025
c79b34c
fix:[ci/cd]: 버그 수정
yongho9064 Sep 26, 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
20 changes: 19 additions & 1 deletion .github/workflows/CI-CD_Pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ jobs:
env:
REDIS_PASSWORD: ""

# ✅ Qdrant 서비스 추가
qdrant:
image: qdrant/qdrant:v1.3.1
ports:
- 6333:6333
- 6334:6334

steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
Expand All @@ -62,6 +69,13 @@ jobs:
timeout 10s bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/localhost/6379; do sleep 1; done'
echo "Redis is ready!"

# ✅ Qdrant 연결 테스트
- name: Wait for Qdrant
run: |
echo "Waiting for Qdrant to be ready..."
timeout 40s bash -c 'until curl -sSf http://localhost:6333/collections >/dev/null; do sleep 1; done'
echo "Qdrant is ready!"

# ✅ application-test.yml에서 사용하는 모든 환경변수를 .env 파일에 생성
- name: Create test .env file
working-directory: backend
Expand All @@ -85,6 +99,10 @@ jobs:
TEST_REDIS_PORT=6379
TEST_REDIS_PASSWORD=

# Qdrant
TEST_QDRANT_HOST=localhost
TEST_QDRANT_PORT=6333

# CI/CD 환경에서는 Embedded Redis 끄기
SPRING_DATA_REDIS_EMBEDDED=false

Expand Down Expand Up @@ -228,7 +246,7 @@ jobs:
set -xe
echo "===== 현재 실행 중인 컨테이너 ====="
docker ps -a || true

echo "===== 기존 컨테이너 종료 & 제거 ====="
docker stop app1 2>/dev/null || true
docker rm app1 2>/dev/null || true
Expand Down
14 changes: 14 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.25"
id("com.google.devtools.ksp") version "1.9.25-1.0.20"
}
ext {
springAiVersion = "1.0.2"
}

group = 'com.ai.lawyer'
version = '0.0.1-SNAPSHOT'
Expand Down Expand Up @@ -65,6 +68,12 @@ dependencies {
implementation("io.github.openfeign.querydsl:querydsl-jpa:7.0")
ksp("io.github.openfeign.querydsl:querydsl-ksp-codegen:7.0")

// AI
implementation 'org.springframework.ai:spring-ai-starter-vector-store-qdrant'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'

// Testing (테스트)
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand All @@ -76,6 +85,11 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging"
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion"
}
}

tasks.named('test') {
useJUnitPlatform()
Expand Down
16 changes: 15 additions & 1 deletion backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ services:
timeout: 5s
retries: 10

qdrant:
image: qdrant/qdrant:v1.13.4
container_name: qdrant-new
restart: unless-stopped
ports:
- "6333:6333" # HTTP API
- "6334:6334" # gRPC API
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:6333/healthz" ]
interval: 10s
timeout: 5s
retries: 10

volumes:
mysql-data:
redis-data:
redis-data:
qdrant-data:
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ai.lawyer.domain.chatbot.controller;

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 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 reactor.core.publisher.Flux;

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

private final ChatBotService chatBotService;

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

@Operation(summary = "기존 채팅", 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));
}

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

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;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "History API", description = "채팅 방 API")
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/chat/history")
public class HistoryController {

private final HistoryService historyService;
private final ChatService chatService;

@Operation(summary = "채팅방 제목 목록 조회")
@GetMapping("/")
public ResponseEntity<List<HistoryDto>> getHistoryTitles(@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok(historyService.getHistoryTitle(memberId));
}

@Operation(summary = "채팅 조회")
@GetMapping("/{historyId}")
public ResponseEntity<List<ChatHistoryDto>> getChatHistory(@AuthenticationPrincipal Long memberId, @PathVariable("historyId") Long roomId) {
return chatService.getChatHistory(memberId, roomId);
}

@Operation(summary = "채팅방 삭제")
@DeleteMapping("/{historyId}")
public ResponseEntity<String> deleteHistory(@AuthenticationPrincipal Long memberId, @PathVariable("historyId") Long roomId) {
return ResponseEntity.ok(historyService.deleteHistory(memberId, roomId));
}

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

import com.ai.lawyer.domain.chatbot.entity.KeywordRank;
import com.ai.lawyer.domain.chatbot.service.KeywordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Tag(name = "Keyword API", description = "키워드 API")
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/chat/keyword")
public class KeywordController {

private final KeywordService keywordService;

@Operation(summary = "1~5위 키워드 랭킹 조회")
@GetMapping("/ranks")
public ResponseEntity<List<KeywordRank>> getKeywordRanks() {
return ResponseEntity.ok(keywordService.getTop5KeywordRanks());
}

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

import com.ai.lawyer.domain.chatbot.entity.Chat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.springframework.ai.document.Document;

import java.time.LocalDateTime;
import java.util.List;

@Schema(description = "채팅 관련 DTO")
public class ChatDto {

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "채팅 요청 DTO")
public static class ChatRequest {

@Schema(description = "사용자가 입력한 메시지", example = "보험 회사에서 손해배상 청구를 거절당했어요. 어떻게 해야 하나요?")
private String message;

}

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "채팅 응답 DTO")
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;

@Schema(description = "응답 생성에 참고한 유사 판례 정보 목록")
private List<Document> similarCases;

@Schema(description = "응답 생성에 참고한 유사 법령 정보 목록")
private List<Document> similarLaws;
}

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "특정 채팅방의 대화 내역 DTO")
public static class ChatHistoryDto {

@Schema(description = "AI 인지 USER 인지", example = "USER")
private String type;

@Schema(description = "메시지 내용", example = "안녕하세요~~")
private String message;

@Schema(description = "생성 시간")
private LocalDateTime createdAt;

public static ChatHistoryDto from(Chat chat) {
return ChatHistoryDto.builder()
.type(chat.getType().toString())
.message(chat.getMessage())
.createdAt(chat.getCreatedAt())
.build();
}
}

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

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

public class ExtractionDto {

@Data
@AllArgsConstructor
@NoArgsConstructor
public static class TitleExtractionDto {
private String title;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public static class KeywordExtractionDto {
private List<String> keyword;
}

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

import com.ai.lawyer.domain.chatbot.entity.History;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "히스토리 DTO - 채팅방")
public class HistoryDto {

@Schema(description = "방 ID", example = "1")
private Long historyRoomId;

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

@Schema(description = "생성 시간")
private LocalDateTime createdAt;

@Schema(description = "업데이트 시간")
private LocalDateTime updatedAt;

public static HistoryDto from(History room) {
return HistoryDto.builder()
.historyRoomId(room.getHistoryId())
.title(room.getTitle())
.createdAt(room.getCreatedAt())
.updatedAt(room.getUpdatedAt())
.build();
}
}
Loading