-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: 책모 챗봇 구현 #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shinwokkang
wants to merge
8
commits into
develop
Choose a base branch
from
feat/298/chatbot
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat: 책모 챗봇 구현 #299
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b588a0f
feat(chatbot): 챗봇 세션/메시지 스키마 및 모듈 뼈대 추가
shinwokkang 1b0a081
feat(chatbot): PII 마스킹 서비스 추가
shinwokkang 51fe1de
feat(chatbot): Gemini API 클라이언트 및 시스템 프롬프트 추가
shinwokkang a76f51a
fix(chatbot): ChatMessage.maskedContent 컬럼 타입 스키마 불일치 수정
shinwokkang 4e3eee7
feat(chatbot): 대화 오케스트레이션, 모델 라우팅, 미해결 태깅 추가
shinwokkang faf8f0f
feat(chatbot): 컨트롤러 및 핸드오프 API(Swagger) 추가
shinwokkang 1117364
fix(chatbot): 비밀번호 마스킹 정규식이 정상 문장을 오탐하는 문제 수정
shinwokkang ef30c25
feat(chatbot): 프롬프트 유출 방어 및 모니터링 지표 추가
shinwokkang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
src/main/java/checkmo/chatbot/internal/config/ChatbotRestTemplateConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package checkmo.chatbot.internal.config; | ||
|
|
||
| import checkmo.chatbot.internal.config.properties.ChatbotProperties; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.http.client.SimpleClientHttpRequestFactory; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class ChatbotRestTemplateConfig { | ||
|
|
||
| private final ChatbotProperties chatbotProperties; | ||
|
|
||
| /** | ||
| * book 모듈의 기본 {@code restTemplate} 빈(Aladin 전용 타임아웃/XML 컨버터 설정)과 분리된 | ||
| * 챗봇(Gemini) 전용 RestTemplate. 빈 이름을 파라미터/필드명과 일치시켜 이름 기준으로 주입되도록 한다. | ||
| * | ||
| * 주의: classpath에 jackson-dataformat-xml(Aladin 연동용)이 있어 RestTemplate 기본 컨버터 목록에 | ||
| * XML 컨버터가 JSON 컨버터보다 먼저 등록된다. 호출부(GeminiApiService)에서 Content-Type을 | ||
| * application/json으로 명시하지 않으면 요청 바디가 XML로 직렬화되어 버리니 반드시 명시할 것. | ||
| */ | ||
| @Bean | ||
| public RestTemplate chatbotRestTemplate() { | ||
| SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); | ||
| factory.setConnectTimeout(chatbotProperties.getTimeoutMs()); | ||
| factory.setReadTimeout(chatbotProperties.getTimeoutMs()); | ||
|
|
||
| return new RestTemplate(factory); | ||
| } | ||
| } |
54 changes: 54 additions & 0 deletions
54
src/main/java/checkmo/chatbot/internal/config/properties/ChatbotProperties.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package checkmo.chatbot.internal.config.properties; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Positive; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @Component | ||
| @ConfigurationProperties(prefix = "chatbot.gemini") | ||
| public class ChatbotProperties { | ||
|
|
||
| @NotBlank(message = "Gemini API 키는 필수입니다") | ||
| private String apiKey; | ||
|
|
||
| @NotBlank(message = "Gemini API 기본 URL은 필수입니다") | ||
| private String baseUrl = "https://generativelanguage.googleapis.com"; | ||
|
|
||
| @NotBlank(message = "Gemini API 버전은 필수입니다") | ||
| private String apiVersion = "v1beta"; | ||
|
|
||
| @Positive(message = "타임아웃은 양수여야 합니다") | ||
| private int timeoutMs = 15000; | ||
|
|
||
| @Valid | ||
| private Model defaultModel = new Model(); | ||
|
|
||
| @Valid | ||
| private Model escalationModel = new Model(); | ||
|
|
||
| @Valid | ||
| private Handoff handoff = new Handoff(); | ||
|
|
||
| @Getter | ||
| @Setter | ||
| public static class Model { | ||
| @NotBlank(message = "모델명은 필수입니다") | ||
| private String name; | ||
| } | ||
|
|
||
| @Getter | ||
| @Setter | ||
| public static class Handoff { | ||
| @NotBlank(message = "고객센터 URL은 필수입니다") | ||
| private String supportUrl; | ||
|
|
||
| @NotBlank(message = "문의 폼 URL은 필수입니다") | ||
| private String inquiryFormUrl; | ||
| } | ||
| } | ||
22 changes: 22 additions & 0 deletions
22
src/main/java/checkmo/chatbot/internal/converter/ChatbotConverter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package checkmo.chatbot.internal.converter; | ||
|
|
||
| import checkmo.chatbot.internal.service.ChatReply; | ||
| import checkmo.chatbot.web.dto.ChatbotResponseDTO; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class ChatbotConverter { | ||
|
|
||
| public static ChatbotResponseDTO.Reply toReplyResponse(ChatReply chatReply) { | ||
| return ChatbotResponseDTO.Reply.builder() | ||
| .sessionToken(chatReply.sessionToken()) | ||
| .replyText(chatReply.replyText()) | ||
| .escalated(chatReply.escalated()) | ||
| .modelUsed(chatReply.modelUsed()) | ||
| .handoffSuggested(chatReply.handoffSuggested()) | ||
| .supportUrl(chatReply.supportUrl()) | ||
| .inquiryFormUrl(chatReply.inquiryFormUrl()) | ||
| .build(); | ||
| } | ||
| } |
79 changes: 79 additions & 0 deletions
79
src/main/java/checkmo/chatbot/internal/entity/ChatMessage.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package checkmo.chatbot.internal.entity; | ||
|
|
||
| import checkmo.common.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Entity | ||
| public class ChatMessage extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false) | ||
| private Long chatSessionId; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 20) | ||
| private ChatRole role; | ||
|
|
||
| // 마스킹된 텍스트만 저장한다. 원문(PII 포함 가능)은 어떤 컬럼에도 저장하지 않는다. | ||
| // @Lob만 쓰면 기본 length(255) 때문에 Hibernate가 TINYTEXT를 기대해 TEXT 컬럼과 스키마 검증이 어긋난다. | ||
| @Column(nullable = false, columnDefinition = "TEXT") | ||
| private String maskedContent; | ||
|
|
||
| @Column(length = 100) | ||
| private String modelUsed; // ASSISTANT 메시지에만 사용 | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean escalated; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean botUncertain; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean userNegativeReaction; | ||
|
|
||
| private ChatMessage( | ||
| Long chatSessionId, | ||
| ChatRole role, | ||
| String maskedContent, | ||
| String modelUsed, | ||
| boolean escalated, | ||
| boolean botUncertain, | ||
| boolean userNegativeReaction | ||
| ) { | ||
| this.chatSessionId = chatSessionId; | ||
| this.role = role; | ||
| this.maskedContent = maskedContent; | ||
| this.modelUsed = modelUsed; | ||
| this.escalated = escalated; | ||
| this.botUncertain = botUncertain; | ||
| this.userNegativeReaction = userNegativeReaction; | ||
| } | ||
|
|
||
| public static ChatMessage userMessage(Long chatSessionId, String maskedContent, boolean userNegativeReaction) { | ||
| return new ChatMessage(chatSessionId, ChatRole.USER, maskedContent, null, false, false, userNegativeReaction); | ||
| } | ||
|
|
||
| public static ChatMessage assistantMessage( | ||
| Long chatSessionId, | ||
| String maskedContent, | ||
| String modelUsed, | ||
| boolean escalated, | ||
| boolean botUncertain | ||
| ) { | ||
| return new ChatMessage(chatSessionId, ChatRole.ASSISTANT, maskedContent, modelUsed, escalated, botUncertain, false); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package checkmo.chatbot.internal.entity; | ||
|
|
||
| public enum ChatRole { | ||
| USER, | ||
| ASSISTANT | ||
| } |
53 changes: 53 additions & 0 deletions
53
src/main/java/checkmo/chatbot/internal/entity/ChatSession.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package checkmo.chatbot.internal.entity; | ||
|
|
||
| import checkmo.common.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import java.time.LocalDateTime; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Entity | ||
| public class ChatSession extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column | ||
| private Long memberId; // 비로그인 사용자는 null | ||
|
|
||
| @Column(nullable = false, unique = true, length = 64) | ||
| private String sessionToken; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean unresolved; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime lastActivityAt; | ||
|
|
||
| private ChatSession(Long memberId, String sessionToken) { | ||
| this.memberId = memberId; | ||
| this.sessionToken = sessionToken; | ||
| this.unresolved = false; | ||
| this.lastActivityAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public static ChatSession start(Long memberId, String sessionToken) { | ||
| return new ChatSession(memberId, sessionToken); | ||
| } | ||
|
|
||
| public void recordActivity() { | ||
| this.lastActivityAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public void flagUnresolved() { | ||
| this.unresolved = true; | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
src/main/java/checkmo/chatbot/internal/exception/ChatbotErrorStatus.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package checkmo.chatbot.internal.exception; | ||
|
|
||
| import checkmo.common.apiPayload.code.BaseErrorCode; | ||
| import checkmo.common.apiPayload.code.ErrorReasonDTO; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum ChatbotErrorStatus implements BaseErrorCode { | ||
|
|
||
| GEMINI_API_CALL_FAILED(HttpStatus.BAD_GATEWAY, "CHATBOT_502", "챗봇 응답 생성에 실패했습니다."), | ||
| GEMINI_EMPTY_RESPONSE(HttpStatus.BAD_GATEWAY, "CHATBOT_503", "챗봇 응답이 비어 있습니다."), | ||
| SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "CHATBOT_404", "존재하지 않는 챗봇 세션입니다."); | ||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; | ||
| private final String message; | ||
|
|
||
| @Override | ||
| public ErrorReasonDTO getReason() { | ||
| return ErrorReasonDTO.builder() | ||
| .code(code) | ||
| .message(message) | ||
| .isSuccess(false) | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| public ErrorReasonDTO getReasonHttpStatus() { | ||
| return ErrorReasonDTO.builder() | ||
| .httpStatus(httpStatus) | ||
| .code(code) | ||
| .message(message) | ||
| .isSuccess(false) | ||
| .build(); | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
src/main/java/checkmo/chatbot/internal/exception/ChatbotException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package checkmo.chatbot.internal.exception; | ||
|
|
||
| import checkmo.common.apiPayload.exception.GeneralException; | ||
|
|
||
| public class ChatbotException extends GeneralException { | ||
|
|
||
| public ChatbotException(ChatbotErrorStatus status) { | ||
| super(status); | ||
| } | ||
|
|
||
| public ChatbotException(ChatbotErrorStatus status, Throwable cause) { | ||
| super(status, cause); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: checkmo2025/BE
Length of output: 5862
🏁 Script executed:
Repository: checkmo2025/BE
Length of output: 8390
🌐 Web query:
Spring Boot 3.5@ConfigurationPropertiesBean Validation requires@validateddocumentation💡 Result:
In Spring Boot 3.5, using Bean Validation (JSR 380) annotations (such as
@NotBlank,@NotNull,@Size) on a@ConfigurationPropertiesbean requires the class to be annotated with@Validatedto trigger the validation process [1]. Key requirements for validating@ConfigurationPropertiesbeans: 1. Dependency: You must have a JSR-303/JSR-380 implementation, such as Hibernate Validator, on your classpath. This is typically provided by including the spring-boot-starter-validation dependency [2][1][3]. 2. Enabling Validation: Adding@Validatedto your@ConfigurationPropertiesclass is mandatory [1]. Without this annotation, the constraint annotations placed on the fields will be ignored by the binding process [1]. 3. Nested Objects: To cascade validation to nested objects, you must annotate the nested field with@Valid[1][3]. Note that in some versions of Spring Boot, validation of nested objects might occur even without@Validdue to specific implementation details in ConfigurationPropertiesBinder, though using@Validis considered the standard practice to align with the Bean Validation specification [4][3]. This approach allows your application to fail fast at startup if configuration properties do not meet the defined constraints, preventing runtime errors later [1][3]. Ensure that your@ConfigurationPropertiesbean is correctly registered as a bean, for example, by using@EnableConfigurationPropertiesor component scanning [1][5].Citations:
Add
@ValidatedtoChatbotPropertiesso constraints run during binding.Spring Boot validates a
@ConfigurationPropertiesclass only when it is annotated with@Validated, andspring-boot-starter-validationprovides the implementation. Without it,@NotBlankonapiKey,baseUrl,apiVersion,timeoutMs, and nested@Validfields are skipped, so invalid Gemini config can bind successfully and fail later inGeminiApiService.🛡️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents