Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8ffbbb4
feat: GPT 기반 코드 추가
Nano2998 Aug 4, 2025
1cc9f2c
feat: GPT 기반 코드 추가
Nano2998 Aug 4, 2025
fd264a4
refactor: Security Config ChatBot 해제
Nano2998 Aug 4, 2025
e19c802
Merge branch 'feat/ai-chat' of github.com:near-Contact-Reminder/backe…
Nano2998 Aug 4, 2025
f347036
feat: GPT 기반 챗봇 기능 추가
slg1119 Aug 5, 2025
e950acd
Merge branch 'feat/ai-chat' of github.com:near-Contact-Reminder/backe…
Nano2998 Aug 7, 2025
d1e4f26
feat: GPT 기반 챗봇 기능 추가
slg1119 Aug 5, 2025
2be5b51
Merge branch 'feat/ai-chat' of github.com:near-Contact-Reminder/backe…
Nano2998 Aug 11, 2025
e063f64
fix : Session 저장 오류 수정
Nano2998 Aug 13, 2025
2206c4a
style: 코드 스타일 수정
slg1119 Oct 3, 2025
63465b0
feat: OpenAi DTO 생성
seungwoo-project Oct 31, 2025
90e0bef
feat: OpenAi Client / Config 추가
seungwoo-project Oct 31, 2025
f45f44a
refactor: OpenAi DTO KaKao DTO와 일관되게 리팩토링
seungwoo-project Oct 31, 2025
35d39c2
feat: OpenFeign 도입으로 스프링 openai 라이브러리 제거
seungwoo-project Oct 31, 2025
790de6a
refactor: OpenAi Client / Config 리팩토링
seungwoo-project Oct 31, 2025
4bddd94
feat: OpenFeign 적용
seungwoo-project Oct 31, 2025
fa5e5f6
fix: 원하는 필드만 받도록 수정
seungwoo-project Oct 31, 2025
3b99bc2
fix: 연결된 대화 프롬프트서비스 변경
seungwoo-project Oct 31, 2025
cf247a6
remove: GptConfig.java 삭제
seungwoo-project Nov 1, 2025
147afa4
remove: spring-ai의존성 제거
seungwoo-project Nov 1, 2025
edfbd61
refactor: OpenAiApi호출 메소드로 추출
seungwoo-project Nov 1, 2025
7364f0d
refactor: OpenAiFeignConfig 삭제 및 ChatService로 통합
seungwoo-project Nov 1, 2025
558d468
refacotr: OpenAi -> Chat으로 파일명, 변수명 변경 / OpenAI 통신 로직 분리
seungwoo-project Nov 1, 2025
f758c24
feat: Test코드 작성
seungwoo-project Nov 1, 2025
d018219
Merge pull request #32 from seungwoo-project/feature/chatclient
seungwoo-project Nov 4, 2025
939094b
refactor: 하드코딩된 프롬프트를 DB 관리 방식으로 변경
seungwoo-project Nov 20, 2025
da3c804
Merge branch 'dev' into feat/ai-chat
slg1119 Nov 26, 2025
053fed4
Merge branch 'dev' into feat/ai-chat
slg1119 Dec 5, 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
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(./gradlew test:*)",
"Bash(./gradlew:*)"
],
"deny": []
}
}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
HELP.md
.gradle
.env
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
Expand Down Expand Up @@ -46,4 +47,7 @@ src/main/resources/application-social.yml
src/main/resources/static/AuthKey.p8

### AWS Settings ###
src/main/resources/application-aws.yml
src/main/resources/application-aws.yml

### AI Settings ###
src/main/resources/application-ai.yml
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ repositories {
ext {
set('snippetsDir', file("build/generated-snippets"))
set('springCloudVersion', "2024.0.0")
springAiVersion = "1.0.0"
}

dependencies {
Expand Down Expand Up @@ -64,6 +65,7 @@ dependencies {
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion"
}
}

Expand Down
25 changes: 25 additions & 0 deletions src/main/java/kr/swyp/backend/chatbot/client/ChatClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.swyp.backend.chatbot.client;

import kr.swyp.backend.chatbot.client.dto.ChatDto.ChatRequest;
import kr.swyp.backend.chatbot.client.dto.ChatDto.ChatResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(
name = "openai-client",
url = "https://api.openai.com"
)
public interface ChatClient {

@PostMapping(value = "/v1/chat/completions",
consumes = MediaType.APPLICATION_JSON_VALUE, // Content-Type 고정
produces = MediaType.APPLICATION_JSON_VALUE)
ChatResponse createChatCompletion(
@RequestHeader("Authorization") String authorization,
@RequestBody ChatRequest request
);

}
65 changes: 65 additions & 0 deletions src/main/java/kr/swyp/backend/chatbot/client/dto/ChatDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package kr.swyp.backend.chatbot.client.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class ChatDto {

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ChatRequest {

private String model;
private List<Message> messages;
private Double temperature;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Message {

private String role;
private String content;
}
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@Builder
public static class ChatResponse {

private String id;
private List<Choice> choices;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Choice {

private Message message;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Message {

private String role;
private String content;
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package kr.swyp.backend.chatbot.controller;

import jakarta.validation.constraints.NotBlank;
import java.util.List;
import kr.swyp.backend.chatbot.dto.ChatDto.ChatHistoryDto;
import kr.swyp.backend.chatbot.dto.ChatDto.ChatRequestDto;
import kr.swyp.backend.chatbot.dto.ChatDto.ChatResponseDto;
import kr.swyp.backend.chatbot.dto.ChatDto.ChatSessionDto;
import kr.swyp.backend.chatbot.dto.ChatDto.ConversationRequestDto;
import kr.swyp.backend.chatbot.dto.ChatDto.ConversationResponseDto;
import kr.swyp.backend.chatbot.service.ChatService;
import kr.swyp.backend.member.dto.MemberDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatController {

private final ChatService chatService;

@PostMapping("/ask")
public ChatResponseDto ask(@RequestBody ChatRequestDto question,
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.ask(memberDetails.getMemberId(), question);
}

@GetMapping("/history")
public List<ChatHistoryDto> getChatHistory(
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.getChatHistory(memberDetails.getMemberId());
}

// 새로운 채팅 세션 시작
@PostMapping("/sessions/start")
public ChatSessionDto startNewSession(
@RequestParam @NotBlank String initialMessage,
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.startNewSession(memberDetails.getMemberId(), initialMessage);
}

// 기존 세션에서 대화 계속
@PostMapping("/sessions/continue")
public ConversationResponseDto continueConversation(
@RequestBody ConversationRequestDto request,
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.continueConversation(memberDetails.getMemberId(), request);
}

// 사용자의 채팅 세션 목록 조회
@GetMapping("/sessions")
public List<ChatSessionDto> getUserSessions(
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.getUserSessions(memberDetails.getMemberId());
}

// 특정 세션의 대화 기록 조회
@GetMapping("/sessions/{sessionId}/history")
public List<ConversationResponseDto> getSessionHistory(
@PathVariable String sessionId,
@AuthenticationPrincipal MemberDetails memberDetails) {
return chatService.getSessionHistory(memberDetails.getMemberId(), sessionId);
}
}
58 changes: 58 additions & 0 deletions src/main/java/kr/swyp/backend/chatbot/domain/ChatHistory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package kr.swyp.backend.chatbot.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import kr.swyp.backend.common.domain.BaseEntity;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "CHAT_HISTORY")
public class ChatHistory extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;

@Column(name = "MEMBER_ID")
@Comment("유저 아이디")
private UUID memberId;

@Column(name = "TARGET")
@Comment("대상")
private String target;

@Column(name = "TOPIC")
@Comment("주제")
private String topic;

@Column(name = "QUESTION", columnDefinition = "TEXT")
@Comment("질문")
private String question;

@Column(name = "RESPONSE", columnDefinition = "TEXT")
@Comment("AI 응답")
private String response;

@Column(name = "SESSION_ID")
@Comment("채팅 세션 ID")
private String sessionId;

@Column(name = "MESSAGE_TYPE")
@Comment("메시지 타입 (USER/BOT)")
private String messageType;
Comment on lines +55 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

messageType 필드가 String으로 선언되어 있습니다. "USER", "BOT"과 같이 정해진 값만 들어가는 경우, Enum 타입을 사용하는 것이 타입 안정성을 높이고 실수를 방지하는 데 도움이 됩니다. MessageType이라는 Enum을 만들고, 필드 타입을 MessageType으로 변경한 뒤 @Enumerated(EnumType.STRING) 어노테이션을 추가하여 DB에 문자열로 저장하도록 하는 것을 권장합니다.

@Enumerated(EnumType.STRING)
@Column(name = "MESSAGE_TYPE")
@Comment("메시지 타입 (USER/BOT)")
private MessageType messageType;

이렇게 변경하면 ChatServiceImpl 등에서 문자열로 비교하던 로직도 타입 안전하게 변경할 수 있습니다.

}
41 changes: 41 additions & 0 deletions src/main/java/kr/swyp/backend/chatbot/domain/ChatPrompt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kr.swyp.backend.chatbot.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import kr.swyp.backend.common.domain.BaseEntity;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Entity
@Table(name = "CHAT_PROMPT")
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatPrompt extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "prompt_key", nullable = false, unique = true)
@Comment("프롬프트 식별자")
private String promptKey;

@Column(name = "prompt_content", nullable = false, columnDefinition = "TEXT")
@Comment("프롬프트 텍스트")
private String promptContent;

@Column(name = "is_active", nullable = false)
@Comment("활성 상태")
private Boolean isActive = true;

}
47 changes: 47 additions & 0 deletions src/main/java/kr/swyp/backend/chatbot/domain/ChatSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package kr.swyp.backend.chatbot.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import kr.swyp.backend.common.domain.BaseEntity;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "CHAT_SESSION")
public class ChatSession extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;

@Column(name = "SESSION_ID", unique = true)
@Comment("세션 고유 식별자")
private String sessionId;

@Column(name = "MEMBER_ID")
@Comment("사용자 아이디")
private UUID memberId;

@Column(name = "TITLE")
@Comment("채팅 세션 제목")
private String title;

@Column(name = "IS_ACTIVE")
@Comment("활성 상태")
@Builder.Default
private Boolean isActive = true;
}
Loading