Skip to content

Commit ff6a79a

Browse files
authored
merge: 챗봇 도메인
[feat] Gemini 챗봇 도메인 생성 #71
2 parents 9362f98 + f0092d8 commit ff6a79a

File tree

14 files changed

+394
-1
lines changed

14 files changed

+394
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ db_dev.trace.db
4343
### Environment Variables ###
4444
.env
4545

46+
### Claude AI ###
47+
CLAUDE.md
48+
.claude/

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
2929
implementation("org.springframework.boot:spring-boot-starter-validation")
3030
implementation("org.springframework.boot:spring-boot-starter-web")
31+
implementation("org.springframework.boot:spring-boot-starter-webflux")
3132
implementation("io.github.cdimascio:java-dotenv:5.2.2")
3233
implementation("org.springframework.boot:spring-boot-starter-security")
3334
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.back.domain.chatbot.controller;
2+
3+
import com.back.domain.chatbot.dto.ChatRequestDto;
4+
import com.back.domain.chatbot.dto.ChatResponseDto;
5+
import com.back.domain.chatbot.entity.ChatConversation;
6+
import com.back.domain.chatbot.service.ChatbotService;
7+
import com.back.global.rsData.RsData;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
import java.util.List;
15+
16+
@RestController
17+
@RequestMapping("/api/chatbot")
18+
@RequiredArgsConstructor
19+
@Slf4j
20+
public class ChatbotController {
21+
22+
private final ChatbotService chatbotService;
23+
24+
@PostMapping("/chat")
25+
public ResponseEntity<RsData<ChatResponseDto>> sendMessage(@Valid @RequestBody ChatRequestDto requestDto) {
26+
try {
27+
ChatResponseDto response = chatbotService.sendMessage(requestDto);
28+
return ResponseEntity.ok(RsData.successOf(response));
29+
} catch (Exception e) {
30+
log.error("채팅 메시지 처리 중 오류 발생: ", e);
31+
return ResponseEntity.internalServerError()
32+
.body(RsData.failOf("서버 오류가 발생했습니다."));
33+
}
34+
}
35+
36+
@GetMapping("/history/{sessionId}")
37+
public ResponseEntity<RsData<List<ChatConversation>>> getChatHistory(@PathVariable String sessionId) {
38+
try {
39+
List<ChatConversation> history = chatbotService.getChatHistory(sessionId);
40+
return ResponseEntity.ok(RsData.successOf(history));
41+
} catch (Exception e) {
42+
log.error("채팅 기록 조회 중 오류 발생: ", e);
43+
return ResponseEntity.internalServerError()
44+
.body(RsData.failOf("서버 오류가 발생했습니다."));
45+
}
46+
}
47+
48+
@GetMapping("/history/user/{userId}/session/{sessionId}")
49+
public ResponseEntity<RsData<List<ChatConversation>>> getUserChatHistory(
50+
@PathVariable Long userId,
51+
@PathVariable String sessionId) {
52+
try {
53+
List<ChatConversation> history = chatbotService.getUserChatHistory(userId, sessionId);
54+
return ResponseEntity.ok(RsData.successOf(history));
55+
} catch (Exception e) {
56+
log.error("사용자 채팅 기록 조회 중 오류 발생: ", e);
57+
return ResponseEntity.internalServerError()
58+
.body(RsData.failOf("서버 오류가 발생했습니다."));
59+
}
60+
}
61+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.back.domain.chatbot.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
8+
@Getter
9+
@Setter
10+
@NoArgsConstructor
11+
public class ChatRequestDto {
12+
13+
@NotBlank(message = "메시지는 필수입니다.")
14+
private String message;
15+
16+
private String sessionId;
17+
18+
private Long userId;
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.back.domain.chatbot.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class ChatResponseDto {
15+
16+
private String response;
17+
private String sessionId;
18+
private LocalDateTime timestamp;
19+
20+
public ChatResponseDto(String response, String sessionId) {
21+
this.response = response;
22+
this.sessionId = sessionId;
23+
this.timestamp = LocalDateTime.now();
24+
}
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.domain.chatbot.dto;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Setter
10+
public class GeminiRequestDto {
11+
12+
private List<Content> contents;
13+
14+
@Getter
15+
@Setter
16+
public static class Content {
17+
private List<Part> parts;
18+
19+
public Content(String text) {
20+
this.parts = List.of(new Part(text));
21+
}
22+
}
23+
24+
@Getter
25+
@Setter
26+
public static class Part {
27+
private String text;
28+
29+
public Part(String text) {
30+
this.text = text;
31+
}
32+
}
33+
34+
public GeminiRequestDto(String message) {
35+
this.contents = List.of(new Content(message));
36+
}
37+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.back.domain.chatbot.dto;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Setter
10+
public class GeminiResponseDto {
11+
12+
private List<Candidate> candidates;
13+
14+
@Getter
15+
@Setter
16+
public static class Candidate {
17+
private Content content;
18+
}
19+
20+
@Getter
21+
@Setter
22+
public static class Content {
23+
private List<Part> parts;
24+
}
25+
26+
@Getter
27+
@Setter
28+
public static class Part {
29+
private String text;
30+
}
31+
32+
public String getGeneratedText() {
33+
if (candidates != null && !candidates.isEmpty() &&
34+
candidates.get(0).content != null &&
35+
candidates.get(0).content.parts != null &&
36+
!candidates.get(0).content.parts.isEmpty()) {
37+
return candidates.get(0).content.parts.get(0).text;
38+
}
39+
return "응답을 생성할 수 없습니다.";
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.back.domain.chatbot.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
6+
import java.time.LocalDateTime;
7+
8+
import static jakarta.persistence.GenerationType.IDENTITY;
9+
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Builder
15+
@ToString
16+
@Entity
17+
public class ChatConversation {
18+
@Id
19+
@GeneratedValue(strategy = IDENTITY)
20+
private Long id;
21+
22+
private Long userId;
23+
24+
@Column(columnDefinition = "TEXT")
25+
private String userMessage;
26+
27+
@Column(columnDefinition = "TEXT")
28+
private String botResponse;
29+
30+
private String sessionId;
31+
32+
private LocalDateTime createdAt;
33+
34+
@PrePersist
35+
protected void onCreate() {
36+
createdAt = LocalDateTime.now();
37+
}
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.back.domain.chatbot.repository;
2+
3+
import com.back.domain.chatbot.entity.ChatConversation;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.stereotype.Repository;
8+
9+
import java.util.List;
10+
11+
@Repository
12+
public interface ChatConversationRepository extends JpaRepository<ChatConversation, Long> {
13+
14+
List<ChatConversation> findBySessionIdOrderByCreatedAtAsc(String sessionId);
15+
16+
Page<ChatConversation> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
17+
18+
List<ChatConversation> findByUserIdAndSessionIdOrderByCreatedAtAsc(Long userId, String sessionId);
19+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.back.domain.chatbot.service;
2+
3+
import com.back.domain.chatbot.dto.ChatRequestDto;
4+
import com.back.domain.chatbot.dto.ChatResponseDto;
5+
import com.back.domain.chatbot.entity.ChatConversation;
6+
import com.back.domain.chatbot.repository.ChatConversationRepository;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import java.util.List;
13+
import java.util.UUID;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
@Slf4j
18+
public class ChatbotService {
19+
20+
private final GeminiApiService geminiApiService;
21+
private final ChatConversationRepository chatConversationRepository;
22+
23+
@Transactional
24+
public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
25+
String sessionId = requestDto.getSessionId();
26+
if (sessionId == null || sessionId.isEmpty()) {
27+
sessionId = UUID.randomUUID().toString();
28+
}
29+
30+
try {
31+
String contextualMessage = buildContextualMessage(requestDto.getMessage(), sessionId);
32+
33+
String botResponse = geminiApiService.generateResponse(contextualMessage).block();
34+
35+
ChatConversation conversation = ChatConversation.builder()
36+
.userId(requestDto.getUserId())
37+
.userMessage(requestDto.getMessage())
38+
.botResponse(botResponse)
39+
.sessionId(sessionId)
40+
.build();
41+
42+
chatConversationRepository.save(conversation);
43+
44+
return new ChatResponseDto(botResponse, sessionId);
45+
46+
} catch (Exception e) {
47+
log.error("채팅 응답 생성 중 오류 발생: ", e);
48+
return new ChatResponseDto("죄송합니다. 오류가 발생했습니다. 다시 시도해주세요.", sessionId);
49+
}
50+
}
51+
52+
private String buildContextualMessage(String userMessage, String sessionId) {
53+
List<ChatConversation> recentConversations = chatConversationRepository
54+
.findBySessionIdOrderByCreatedAtAsc(sessionId);
55+
56+
if (recentConversations.isEmpty()) {
57+
return "당신은 칵테일 전문 챗봇입니다. 칵테일에 관련된 질문에 친근하고 도움이 되는 답변을 해주세요. 질문: " + userMessage;
58+
}
59+
60+
StringBuilder contextBuilder = new StringBuilder();
61+
contextBuilder.append("당신은 칵테일 전문 챗봇입니다. 다음은 이전 대화 내용입니다:\n\n");
62+
63+
int maxHistory = Math.min(recentConversations.size(), 5);
64+
for (int i = Math.max(0, recentConversations.size() - maxHistory); i < recentConversations.size(); i++) {
65+
ChatConversation conv = recentConversations.get(i);
66+
contextBuilder.append("사용자: ").append(conv.getUserMessage()).append("\n");
67+
contextBuilder.append("챗봇: ").append(conv.getBotResponse()).append("\n\n");
68+
}
69+
70+
contextBuilder.append("새로운 질문: ").append(userMessage);
71+
contextBuilder.append("\n\n이전 대화 맥락을 고려하여 친근하고 도움이 되는 답변을 해주세요.");
72+
73+
return contextBuilder.toString();
74+
}
75+
76+
@Transactional(readOnly = true)
77+
public List<ChatConversation> getChatHistory(String sessionId) {
78+
return chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
79+
}
80+
81+
@Transactional(readOnly = true)
82+
public List<ChatConversation> getUserChatHistory(Long userId, String sessionId) {
83+
return chatConversationRepository.findByUserIdAndSessionIdOrderByCreatedAtAsc(userId, sessionId);
84+
}
85+
}

0 commit comments

Comments
 (0)