Skip to content

Commit 2e56a66

Browse files
authored
Chat Gpt 로그인 연동 (#42)
* refactor: View - 해설 창 크기 조절 - VH 적용 * feat: 로그인시에만 챗봇 사용 가능하도록 구현 * refactor: 챗봇에 메시지 전송시 메시지를 리스트 형태로 전달하도록 수정 - 이전 대화 기억을 위함 * feat: 챗봇 사용 횟수 제한 기능 구현 * feat: 챗봇 사용 횟수 초기화 기능 구현 -@scheduled로 매 정각마다 초기화 * refactor: 챗봇 관련 로그인 부분과 캐시 조회 부분이 분리되도록 수정 * feat: View - 글자 굵게 표시 기능 추가 * feat: View - 이미지 표시 기능 추가 * refactor: View - 해설 창 배경색 변경 * style: 코딩 컨벤션 적용 * refactor: 대화 목록 저장시 ChatCacheService를 사용하여 저장하도록 수정 * refactor: 캐시 저장 구현 방식 수정 - 어노테이션 기반 -> CacheManager 직접 조작 - @Cacheable어노테이션의 프록시 기반으로 인한 캐시 조작 어려움 * style: 코딩 컨벤션 적용 * refactor: View - 챗봇 말풍선 UI 수정 * docs: 사이트맵 갱신 적용 * refactor: 유저당 메시지 기억 횟수를 ChatManageService에서 관리하도록 수정
1 parent 3fc23b6 commit 2e56a66

27 files changed

+358
-121
lines changed

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ dependencies {
186186
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
187187
implementation 'org.springframework.security:spring-security-oauth2-jose'
188188
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
189+
190+
/*
191+
캐싱 관련
192+
*/
193+
implementation 'org.springframework.boot:spring-boot-starter-cache'
194+
implementation 'com.github.ben-manes.caffeine:caffeine'
195+
189196
}
190197

191198
tasks.named('test') {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.server.computerscience.chatbot.config;
2+
3+
import java.util.concurrent.TimeUnit;
4+
5+
import org.springframework.cache.CacheManager;
6+
import org.springframework.cache.annotation.EnableCaching;
7+
import org.springframework.cache.caffeine.CaffeineCacheManager;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
import com.github.benmanes.caffeine.cache.Caffeine;
12+
13+
@Configuration
14+
@EnableCaching
15+
public class CacheConfig {
16+
@Bean
17+
public CacheManager cacheManager() {
18+
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
19+
cacheManager.setCaffeine(Caffeine.newBuilder()
20+
.expireAfterWrite(1, TimeUnit.HOURS) // 1시간 후 만료
21+
.maximumSize(1000)); // 최대 1000개의 항목 유지
22+
return cacheManager;
23+
}
24+
}

src/main/java/com/server/computerscience/chatbot/controller/ChatbotController.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.server.computerscience.chatbot.controller;
22

33
import org.springframework.http.ResponseEntity;
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5+
import org.springframework.security.oauth2.core.user.OAuth2User;
46
import org.springframework.web.bind.annotation.PostMapping;
57
import org.springframework.web.bind.annotation.RequestBody;
68
import org.springframework.web.bind.annotation.RestController;
@@ -20,8 +22,9 @@ public class ChatbotController {
2022

2123
@PostMapping("/chat/text")
2224
public ResponseEntity<ChatBotResponseDto> chat(
23-
@RequestBody ChatBotRequestDto chatBotRequestDto
25+
@RequestBody ChatBotRequestDto chatBotRequestDto,
26+
@AuthenticationPrincipal OAuth2User user
2427
) {
25-
return ResponseEntity.ok(ChatBotResponseDto.from(chatbotService.chat(chatBotRequestDto)));
28+
return ResponseEntity.ok(ChatBotResponseDto.from(chatbotService.talkToAssistant(chatBotRequestDto, user)));
2629
}
2730
}

src/main/java/com/server/computerscience/chatbot/domain/ChatGptContentType.java renamed to src/main/java/com/server/computerscience/chatbot/domain/ChatContentType.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import lombok.Getter;
44

55
@Getter
6-
public enum ChatGptContentType {
6+
public enum ChatContentType {
77
TEXT("text");
88
private final String lower;
99

10-
ChatGptContentType(String lower) {
10+
ChatContentType(String lower) {
1111
this.lower = lower;
1212
}
1313
}

src/main/java/com/server/computerscience/chatbot/domain/ChatGptRole.java renamed to src/main/java/com/server/computerscience/chatbot/domain/ChatRole.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import lombok.Getter;
44

55
@Getter
6-
public enum ChatGptRole {
7-
SYSTEM("system"), USER("user");
6+
public enum ChatRole {
7+
SYSTEM("system"), USER("user"), ASSISTANT("assistant");
88
private final String lower;
99

10-
ChatGptRole(String lower) {
10+
ChatRole(String lower) {
1111
this.lower = lower;
1212
}
1313
}

src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptContentDto.java renamed to src/main/java/com/server/computerscience/chatbot/dto/request/ChatContentDto.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.server.computerscience.chatbot.dto.request;
22

3-
import com.server.computerscience.chatbot.domain.ChatGptContentType;
3+
import com.server.computerscience.chatbot.domain.ChatContentType;
44

55
import lombok.AllArgsConstructor;
66
import lombok.Builder;
@@ -13,12 +13,12 @@
1313
@NoArgsConstructor
1414
@AllArgsConstructor
1515
@Builder
16-
public class ChatGptContentDto {
16+
public class ChatContentDto {
1717
private String type;
1818
private String text;
1919

20-
public static ChatGptContentDto from(ChatGptContentType type, String text) {
21-
return ChatGptContentDto.builder()
20+
public static ChatContentDto from(ChatContentType type, String text) {
21+
return ChatContentDto.builder()
2222
.text(text)
2323
.type(type.getLower())
2424
.build();

src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptMessageDto.java

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptRestRequestDto.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.server.computerscience.chatbot.dto.request;
22

33
import java.util.List;
4-
import java.util.stream.Collectors;
54

65
import lombok.AllArgsConstructor;
76
import lombok.Builder;
@@ -16,14 +15,12 @@
1615
@Builder
1716
public class ChatGptRestRequestDto {
1817
private String model;
19-
private List<ChatGptMessageDto> messages;
18+
private List<ChatMessageDto> messages;
2019

21-
public static ChatGptRestRequestDto from(String model, List<String> prompts) {
20+
public static ChatGptRestRequestDto from(String model, List<ChatMessageDto> chatMessages) {
2221
return ChatGptRestRequestDto.builder()
2322
.model(model)
24-
.messages(prompts.stream()
25-
.map(ChatGptMessageDto::fromUserText)
26-
.collect(Collectors.toList()))
23+
.messages(chatMessages)
2724
.build();
2825
}
2926
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.server.computerscience.chatbot.dto.request;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
6+
import com.server.computerscience.chatbot.domain.ChatContentType;
7+
import com.server.computerscience.chatbot.domain.ChatRole;
8+
9+
import lombok.AllArgsConstructor;
10+
import lombok.Builder;
11+
import lombok.Getter;
12+
import lombok.NoArgsConstructor;
13+
import lombok.ToString;
14+
15+
@Getter
16+
@ToString
17+
@NoArgsConstructor
18+
@AllArgsConstructor
19+
@Builder
20+
public class ChatMessageDto {
21+
private String role;
22+
private List<ChatContentDto> content;
23+
24+
public static ChatMessageDto from(String text, ChatRole role) {
25+
return ChatMessageDto.builder()
26+
.role(role.getLower())
27+
.content(Collections.singletonList(ChatContentDto.from(ChatContentType.TEXT, text)))
28+
.build();
29+
}
30+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.server.computerscience.chatbot.service;
22

3+
import org.springframework.security.oauth2.core.user.OAuth2User;
4+
35
import com.server.computerscience.chatbot.dto.request.ChatBotRequestDto;
46

57
public interface ChatbotService {
6-
String chat(ChatBotRequestDto chatBotRequestDto);
8+
String talkToAssistant(ChatBotRequestDto chatBotRequestDto, OAuth2User user);
79
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.server.computerscience.chatbot.service.implement;
2+
3+
import java.util.LinkedList;
4+
import java.util.List;
5+
6+
import org.springframework.cache.Cache;
7+
import org.springframework.cache.CacheManager;
8+
import org.springframework.cache.annotation.CacheEvict;
9+
import org.springframework.cache.annotation.CachePut;
10+
import org.springframework.scheduling.annotation.Scheduled;
11+
import org.springframework.stereotype.Service;
12+
13+
import com.server.computerscience.chatbot.dto.request.ChatMessageDto;
14+
15+
import lombok.RequiredArgsConstructor;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class ChatCacheService {
20+
private static final String CHAT_MESSAGE = "chatMessages";
21+
private static final String CHAT_USED_CHANCE = "chatRemainChance";
22+
23+
private final CacheManager cacheManager;
24+
25+
/**
26+
* 이미 저장되어있는 대화 목록이 있을 경우, 아니라면 새로운 목록을 반환
27+
*/
28+
public List<ChatMessageDto> getChatMessages(String userId) {
29+
Cache cache = cacheManager.getCache(CHAT_MESSAGE);
30+
if (cache != null) {
31+
List<ChatMessageDto> chatMessages = cache.get(userId, List.class);
32+
return chatMessages != null ? chatMessages : new LinkedList<>();
33+
}
34+
return new LinkedList<>();
35+
}
36+
37+
@CachePut(value = CHAT_MESSAGE, key = "#userId")
38+
public List<ChatMessageDto> saveChatMessage(String userId, ChatMessageDto chatMessageDto, int maxMessageSize) {
39+
List<ChatMessageDto> chatMessages = getChatMessages(userId); // 캐시에서 가져오기
40+
chatMessages.add(chatMessageDto);
41+
if (chatMessages.size() > maxMessageSize) {
42+
chatMessages.remove(0);
43+
}
44+
return chatMessages;
45+
}
46+
47+
// 남은 이용 횟수를 가져오는 메서드
48+
public Integer getUsedChance(String userId) {
49+
Cache cache = cacheManager.getCache(CHAT_USED_CHANCE);
50+
if (cache != null) {
51+
Integer usedChance = cache.get(userId, Integer.class);
52+
return usedChance != null ? usedChance : 0; // 캐시에서 가져온 값이 없으면 0 반환
53+
}
54+
return 0; // 기본값
55+
}
56+
57+
// 사용 횟수를 증가시키는 메서드
58+
@CachePut(value = CHAT_USED_CHANCE, key = "#userId")
59+
public int increaseUsedChance(String userId) {
60+
int currentUsedChance = getUsedChance(userId);
61+
currentUsedChance = currentUsedChance + 1;
62+
return currentUsedChance;
63+
}
64+
65+
/**
66+
* 매 시간마다 모든 사용자에 대한 사용 횟수 캐시를 삭제하는 메서드
67+
*/
68+
@Scheduled(cron = "0 0 * * * *") // 매 시간 정각에 실행
69+
@CacheEvict(value = CHAT_USED_CHANCE, allEntries = true)
70+
public void clearAllUsedChances() {
71+
// 모든 사용자에 대한 사용 횟수 캐시를 삭제합니다.
72+
System.out.println("모든 사용자에 대한 사용 횟수 캐시를 삭제했습니다.");
73+
}
74+
}

src/main/java/com/server/computerscience/chatbot/service/implement/ChatGptService.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
package com.server.computerscience.chatbot.service.implement;
22

3-
import java.util.Collections;
3+
import java.util.List;
44

55
import org.springframework.stereotype.Service;
66
import org.springframework.web.client.RestTemplate;
77

8-
import com.server.computerscience.chatbot.dto.request.ChatBotRequestDto;
98
import com.server.computerscience.chatbot.dto.request.ChatGptRestRequestDto;
9+
import com.server.computerscience.chatbot.dto.request.ChatMessageDto;
1010
import com.server.computerscience.chatbot.dto.response.ChatGptResponseDto;
11-
import com.server.computerscience.chatbot.service.ChatbotService;
1211

1312
import lombok.RequiredArgsConstructor;
1413

1514
@Service
1615
@RequiredArgsConstructor
17-
public class ChatGptService implements ChatbotService {
16+
public class ChatGptService {
1817
private final String model = "gpt-4o-mini";
1918
private final String payingApiUrl = "https://api.openai.com/v1/chat/completions";
2019
private final RestTemplate restTemplate;
2120

22-
@Override
23-
public String chat(ChatBotRequestDto chatBotRequestDto) {
21+
public String chat(List<ChatMessageDto> chatMessages) {
2422
ChatGptRestRequestDto chatGptRestRequestDto = ChatGptRestRequestDto.from(model,
25-
Collections.singletonList(chatBotRequestDto.getPrompt()));
23+
chatMessages);
2624
ChatGptResponseDto chatGptResponseDto = restTemplate.postForObject(
2725
payingApiUrl,
2826
chatGptRestRequestDto,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.server.computerscience.chatbot.service.implement;
2+
3+
import java.util.List;
4+
5+
import org.springframework.stereotype.Service;
6+
7+
import com.server.computerscience.chatbot.domain.ChatRole;
8+
import com.server.computerscience.chatbot.dto.request.ChatBotRequestDto;
9+
import com.server.computerscience.chatbot.dto.request.ChatMessageDto;
10+
11+
import lombok.RequiredArgsConstructor;
12+
13+
@Service
14+
@RequiredArgsConstructor
15+
public class ChatManageService {
16+
private static final int MAX_CHAT_CHANCE = 30;
17+
private static final int MAX_MESSAGES_SIZE = 15;
18+
private final String NO_MORE_CHANCE = "채팅 기회를 모두 소모하셨습니다. 1시간마다 초기화됩니다.";
19+
private final ChatCacheService chatCacheService;
20+
private final ChatGptService chatGptService;
21+
22+
public String respond(String userId, ChatBotRequestDto chatBotRequestDto) {
23+
if (chatCacheService.getUsedChance(userId) >= MAX_CHAT_CHANCE) {
24+
return NO_MORE_CHANCE;
25+
}
26+
/**
27+
* User가 가지고 있는 이전 대화 기록을 가져온다.
28+
*/
29+
List<ChatMessageDto> chatMessages = beforeRespond(userId,
30+
chatBotRequestDto);
31+
/**
32+
* 챗봇에게 받은 답변 또한 이전 대화 기록에 넣는다.
33+
*/
34+
String answer = chatGptService.chat(chatMessages);
35+
afterRespond(userId, answer, chatMessages);
36+
return answer;
37+
}
38+
39+
private List<ChatMessageDto> beforeRespond(String userId, ChatBotRequestDto chatBotRequestDto) {
40+
ChatMessageDto chatMessageFromUser = ChatMessageDto.from(chatBotRequestDto.getPrompt(), ChatRole.USER);
41+
return chatCacheService.saveChatMessage(userId, chatMessageFromUser, MAX_MESSAGES_SIZE);
42+
}
43+
44+
private void afterRespond(String userId, String answer, List<ChatMessageDto> chatMessages) {
45+
ChatMessageDto chatMessageFromAssistant = ChatMessageDto.from(answer, ChatRole.ASSISTANT);
46+
chatCacheService.saveChatMessage(userId, chatMessageFromAssistant, MAX_MESSAGES_SIZE);
47+
chatCacheService.increaseUsedChance(userId);
48+
}
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.server.computerscience.chatbot.service.implement;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.security.oauth2.core.user.OAuth2User;
5+
import org.springframework.stereotype.Service;
6+
7+
import com.server.computerscience.chatbot.dto.request.ChatBotRequestDto;
8+
import com.server.computerscience.chatbot.service.ChatbotService;
9+
10+
import lombok.RequiredArgsConstructor;
11+
12+
@Service
13+
@RequiredArgsConstructor
14+
public class LoginChatBotService implements ChatbotService {
15+
private final ChatManageService chatManageService;
16+
private final String NOT_LOGIN = "로그인이 필요합니다.";
17+
@Value("${spring.security.oauth2.client.provider.cognito.user-name-attribute}")
18+
private String userIdentifier;
19+
20+
@Override
21+
public String talkToAssistant(ChatBotRequestDto chatBotRequestDto, OAuth2User user) {
22+
if (user == null) {
23+
return NOT_LOGIN;
24+
}
25+
String userId = (String)user.getAttributes().get(userIdentifier);
26+
return chatManageService.respond(userId, chatBotRequestDto);
27+
}
28+
}

0 commit comments

Comments
 (0)