Repository navigation
[Feat/#95] 챗봇 메시지 전송 SSE 중계 #96
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
overhyeon
wants to merge
14
commits into
main
Choose a base branch
from
feat/#95-chat-sse-relay
base: main
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
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
6b7240f
feat: AI 서버 SSE 중계 클라이언트 추가
overhyeon 9471271
feat: 챗봇 대화 저장소 추가
overhyeon 34f8d8a
feat: 챗봇 질문·답변 서비스 추가
overhyeon ed89b26
feat: 챗봇 메시지 전송 API 추가
overhyeon e3a9020
fix: 응답 스트림에 직접 써서 조각이 실제로 흘러가게 함
overhyeon 5e65cad
fix: 중간에 실패한 답변은 저장하지 않음
overhyeon a0326dc
fix: 한 회원당 답변 스트림을 하나만 허용
overhyeon dd5fc22
refactor: AI 서버 클라이언트를 기능/벤더 패키지로 옮기고 설정 키를 정리
tnals0924 5e55cdc
fix: 학생 연결 끊김을 AI 서버 장애로 기록하지 않음
tnals0924 af08235
refactor: AI 서버 클라이언트에서 SSE 파싱과 종료 보장을 분리
tnals0924 1302474
refactor: 챗봇 서비스에서 동시 답변 차단과 답변 수집을 분리
tnals0924 4d389c8
refactor: 챗봇 도메인 객체 생성을 정적 팩토리로 통일
tnals0924 7c88c1a
refactor: SSE 쓰기를 컨트롤러에서 분리
tnals0924 c96121b
fix: 챗봇 API 검증 실패를 ApiResponse로 응답
tnals0924 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
42 changes: 42 additions & 0 deletions
42
api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/welfare/chat/AppChatApi.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,42 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import java.io.IOException; | ||
| import kr.ac.kookmin.stream.api.app.AppApiUser; | ||
| import kr.ac.kookmin.stream.api.app.welfare.chat.request.ChatMessageCreateRequest; | ||
| import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; | ||
| import kr.ac.kookmin.stream.common.CommonErrorCode; | ||
|
|
||
| /** | ||
| * 학생 앱 챗봇 API의 문서 명세. 구현은 {@link AppChatController}가 맡는다. | ||
| * <p> | ||
| * <b>이 API만 {@code ApiResponse} 껍데기를 쓰지 않는다.</b> 답변이 조각으로 나눠 오므로 한 번 감싸는 껍데기를 붙일 자리가 없다. | ||
| * 그래서 에러가 두 갈래로 갈린다. | ||
| * <ul> | ||
| * <li>스트림 시작 <b>전</b>(검증·인증 실패) — 다른 API와 같은 {@code ApiResponse} 에러와 상태 코드</li> | ||
| * <li>스트림 시작 <b>후</b>(AI 서버 실패·타임아웃) — {@code event: error}. 첫 바이트가 나간 뒤에는 상태 코드를 바꿀 수 없다</li> | ||
| * </ul> | ||
| */ | ||
| @Tag(name = "챗봇", description = "학생 앱 챗봇 질문·답변") | ||
| public interface AppChatApi { | ||
|
|
||
| /** | ||
| * 질문을 보내고 답변을 SSE로 받는다. 응답은 {@code text/event-stream}이고 이벤트는 네 종류다. | ||
| * <pre> | ||
| * event: delta data: { "text": "부분 응답" } 0회 이상 반복 | ||
| * event: bubble data: {} 여기서부터 새 말풍선 | ||
| * event: done data: {} 정상 종료 | ||
| * event: error data: { "message": "오류 안내" } 실패 | ||
| * </pre> | ||
| * 조각이 줄바꿈 중간에서 끊길 수 있어 프론트가 빈 줄을 직접 찾지 않도록 서버가 문단 경계마다 {@code bubble}을 보낸다. | ||
| */ | ||
| @Operation(summary = "챗봇 질문 전송 (SSE)", | ||
| description = "질문을 보내고 답변을 조각으로 받는다. 응답은 JSON 한 번이 아니라 " | ||
| + "text/event-stream이며 delta·bubble·done·error 네 이벤트가 온다. " | ||
| + "지난 대화는 서버가 DB에서 꺼내 함께 보내므로 요청에는 이번 질문만 담는다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| void sendMessage(AppApiUser apiUser, ChatMessageCreateRequest request, HttpServletResponse response) | ||
| throws IOException; | ||
| } |
45 changes: 45 additions & 0 deletions
45
api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/welfare/chat/AppChatController.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,45 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat; | ||
|
|
||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import jakarta.validation.Valid; | ||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import kr.ac.kookmin.stream.api.app.AppApiUser; | ||
| import kr.ac.kookmin.stream.api.app.welfare.chat.request.ChatMessageCreateRequest; | ||
| import kr.ac.kookmin.stream.welfare.domain.chat.service.ChatService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| 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.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import tools.jackson.databind.ObjectMapper; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/v1/app/chat") | ||
| @RequiredArgsConstructor | ||
| public class AppChatController implements AppChatApi { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(AppChatController.class); | ||
|
|
||
| private final ChatService chatService; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Override | ||
| @PostMapping(value = "/messages", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
| public void sendMessage( | ||
| AppApiUser apiUser, | ||
| @Valid @RequestBody ChatMessageCreateRequest request, | ||
| HttpServletResponse response | ||
| ) throws IOException { | ||
| ChatSseWriter writer = ChatSseWriter.of(response, objectMapper); | ||
| try { | ||
| chatService.answer(apiUser.userId(), request.message(), writer); | ||
| } catch (UncheckedIOException e) { | ||
| // 학생이 화면을 닫으면 쓰기가 실패한다. 오류가 아니라 정상 종료다 | ||
| log.debug("학생이 연결을 끊어 중계를 멈춥니다. memberId={}", apiUser.userId()); | ||
| } | ||
| } | ||
| } |
72 changes: 72 additions & 0 deletions
72
api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/welfare/chat/ChatSseWriter.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,72 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat; | ||
|
|
||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.io.UncheckedIOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import kr.ac.kookmin.stream.api.app.welfare.chat.response.ChatDeltaResponse; | ||
| import kr.ac.kookmin.stream.api.app.welfare.chat.response.ChatErrorResponse; | ||
| import kr.ac.kookmin.stream.welfare.domain.chat.client.ChatEventListener; | ||
| import lombok.AccessLevel; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.MediaType; | ||
| import tools.jackson.databind.ObjectMapper; | ||
|
|
||
| /** | ||
| * 받은 이벤트를 학생 쪽 연결에 SSE 형식({@code event: 이름}, {@code data: JSON}, 빈 줄)으로 쓰고 조각마다 바로 flush한다. | ||
| * <p> | ||
| * 응답 스트림에 <b>직접</b> 쓴다. {@code StreamingResponseBody}로는 flush가 무시되어 톰캣 버퍼가 찰 때만 조각이 나갔다. | ||
| * 대신 답변이 끝날 때까지 요청 스레드를 쥐므로, 동시 대화가 톰캣 최대 스레드(기본 200)에 가까워지면 {@code SseEmitter}를 다시 시험한다. | ||
| */ | ||
| @RequiredArgsConstructor(access = AccessLevel.PRIVATE) | ||
| final class ChatSseWriter implements ChatEventListener { | ||
|
|
||
| // bubble·done은 data가 빈 객체다. 필드 없는 record는 직렬화 설정에 따라 예외가 날 수 있어 그대로 쓴다 | ||
| private static final String EMPTY_DATA = "{}"; | ||
|
|
||
| private final OutputStream output; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| /** 응답 헤더를 SSE로 정하고 출력 스트림을 연다. */ | ||
| static ChatSseWriter of(HttpServletResponse response, ObjectMapper objectMapper) throws IOException { | ||
| response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE); | ||
| response.setCharacterEncoding(StandardCharsets.UTF_8.name()); | ||
| // 중간 프록시가 응답을 모아두면 조각이 한 번에 도착해 스트리밍이 사라진다. AI 서버도 같은 헤더를 보낸다 | ||
| response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); | ||
| response.setHeader("X-Accel-Buffering", "no"); | ||
| return new ChatSseWriter(response.getOutputStream(), objectMapper); | ||
| } | ||
|
|
||
| @Override | ||
| public void onDelta(String text) { | ||
| write("delta", objectMapper.writeValueAsString(new ChatDeltaResponse(text))); | ||
| } | ||
|
|
||
| @Override | ||
| public void onBubble() { | ||
| write("bubble", EMPTY_DATA); | ||
| } | ||
|
|
||
| @Override | ||
| public void onDone() { | ||
| write("done", EMPTY_DATA); | ||
| } | ||
|
|
||
| @Override | ||
| public void onError(String message) { | ||
| write("error", objectMapper.writeValueAsString(new ChatErrorResponse(message))); | ||
| } | ||
|
|
||
| private void write(String event, String data) { | ||
| try { | ||
| String frame = "event: " + event + "\ndata: " + data + "\n\n"; | ||
| output.write(frame.getBytes(StandardCharsets.UTF_8)); | ||
| output.flush(); | ||
| } catch (IOException e) { | ||
| // 학생이 연결을 끊었다. 받을 사람이 없으니 서비스까지 올려 보내 중계를 멈춘다 | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
...main/java/kr/ac/kookmin/stream/api/app/welfare/chat/request/ChatMessageCreateRequest.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,18 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
| /** | ||
| * 챗봇에 보낼 질문. 지난 대화는 서버가 DB에서 꺼내 붙이므로 이번 질문만 받는다. | ||
| * <p> | ||
| * 길이를 제한하는 이유는 두 가지다. 긴 글을 그대로 넘기면 토큰 비용이 그만큼 늘고, | ||
| * 프롬프트 인젝션을 길게 심을 여지를 줄인다. | ||
| */ | ||
| public record ChatMessageCreateRequest( | ||
| @Schema(description = "학생이 입력한 질문", example = "빌릴게 어떻게 써요?") | ||
| @NotBlank(message = "질문을 입력해주세요.") | ||
| @Size(max = 500, message = "질문은 500자까지 입력할 수 있습니다.") | ||
| String message | ||
| ) {} |
4 changes: 4 additions & 0 deletions
4
...i/src/main/java/kr/ac/kookmin/stream/api/app/welfare/chat/response/ChatDeltaResponse.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,4 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat.response; | ||
|
|
||
| /** SSE {@code delta} 이벤트의 data. 답변 조각 하나. */ | ||
| public record ChatDeltaResponse(String text) {} |
4 changes: 4 additions & 0 deletions
4
...i/src/main/java/kr/ac/kookmin/stream/api/app/welfare/chat/response/ChatErrorResponse.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,4 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.chat.response; | ||
|
|
||
| /** SSE {@code error} 이벤트의 data. 학생에게 보여줄 안내문. */ | ||
| public record ChatErrorResponse(String message) {} |
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
19 changes: 19 additions & 0 deletions
19
...ain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/client/ChatClient.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,19 @@ | ||
| package kr.ac.kookmin.stream.welfare.domain.chat.client; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.welfare.domain.chat.domain.ChatTurn; | ||
|
|
||
| /** | ||
| * 답변을 만들어 주는 AI 서버. 구현체는 {@code infrastructure:client}에 둔다. | ||
| */ | ||
| public interface ChatClient { | ||
|
|
||
| /** | ||
| * 대화 이력을 보내고 답변을 조각으로 받는다. 스트림이 끝날 때까지 돌아오지 않는다. | ||
| * | ||
| * @param memberId 질문한 학생. AI 서버가 요청자를 식별하는 헤더로 쓴다 | ||
| * @param history 이번 질문까지 포함한 대화 전체. AI 서버는 대화를 저장하지 않으므로 요청마다 통째로 보낸다 | ||
| * @param listener 받은 이벤트를 넘길 곳. 계약은 {@link ChatEventListener} 참고 | ||
| */ | ||
| void stream(Long memberId, List<ChatTurn> history, ChatEventListener listener); | ||
| } |
27 changes: 27 additions & 0 deletions
27
...fare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/client/ChatEventListener.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,27 @@ | ||
| package kr.ac.kookmin.stream.welfare.domain.chat.client; | ||
|
|
||
| /** | ||
| * AI 서버가 보내는 답변 이벤트를 받는 쪽. 답변을 모아서 한 번에 넘기지 않고 조각마다 알려, 받는 즉시 학생에게 흘려보낼 수 있게 한다. | ||
| * <p> | ||
| * 마지막에는 {@code onDone} 또는 {@code onError} 중 하나가 정확히 한 번 온다. {@link ChatClient} 구현체가 보장한다. | ||
| * <p> | ||
| * 더 받을 수 없으면(학생이 연결을 끊는 등) 구현체는 unchecked 예외를 던져 중계를 멈출 수 있다. | ||
| * {@link ChatClient} 구현체는 이 예외를 AI 서버 실패로 다루지 않고 그대로 전파한다. | ||
| */ | ||
| public interface ChatEventListener { | ||
|
|
||
| /** 답변 조각. 여러 번 온다. 빈 줄은 들어 있지 않다. */ | ||
| void onDelta(String text); | ||
|
|
||
| /** 여기서부터 새 말풍선. 첫 말풍선 앞과 마지막 말풍선 뒤에는 오지 않는다. */ | ||
| void onBubble(); | ||
|
|
||
| /** 정상 종료. */ | ||
| void onDone(); | ||
|
|
||
| /** | ||
| * 실패. 스트림이 시작된 뒤에는 상태 코드를 바꿀 수 없어 예외 대신 이 통로로 알린다. | ||
| * 상세 원인은 로그에만 남기므로 여기 오는 글은 학생에게 보여줄 안내문이다. | ||
| */ | ||
| void onError(String message); | ||
| } |
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
41 changes: 41 additions & 0 deletions
41
...omain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/domain/ChatTurn.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,41 @@ | ||
| package kr.ac.kookmin.stream.welfare.domain.chat.domain; | ||
|
|
||
| /** | ||
| * AI 서버에 보내는 대화 한 턴. {@link ChatMessage}의 id·memberId는 AI 서버가 알 필요가 없어 역할과 글만 담는다. | ||
| * <p> | ||
| * {@code role} 값은 AI 서버 계약과 {@code chat_messages.sender_type}에 같은 글자로 쓴다. 중간에 변환하면 한쪽만 고쳤을 때 조용히 어긋난다. | ||
| */ | ||
| public record ChatTurn(String role, String text) { | ||
|
|
||
| /** 학생이 쓴 글. */ | ||
| public static final String USER = "user"; | ||
|
|
||
| /** 챗봇이 쓴 글. Gemini 계약이 assistant가 아니라 model이다. */ | ||
| public static final String MODEL = "model"; | ||
|
|
||
| public ChatTurn { | ||
| validateRole(role); | ||
| validateText(text); | ||
| } | ||
|
|
||
| public static ChatTurn from(ChatMessage message) { | ||
| return new ChatTurn(message.getSenderType(), message.getContent()); | ||
| } | ||
|
|
||
| public static ChatTurn user(String text) { | ||
| return new ChatTurn(USER, text); | ||
| } | ||
|
|
||
| private static void validateRole(String role) { | ||
| if (role == null || role.isBlank()) { | ||
| throw new IllegalArgumentException("role이 비어 있습니다."); | ||
| } | ||
| } | ||
|
|
||
| // AI 서버가 빈 글에 422를 돌려준다. 보내기 전에 막아 원인을 여기서 알 수 있게 한다 | ||
| private static void validateText(String text) { | ||
| if (text == null || text.isBlank()) { | ||
| throw new IllegalArgumentException("text가 비어 있습니다."); | ||
| } | ||
| } | ||
| } |
15 changes: 15 additions & 0 deletions
15
.../main/java/kr/ac/kookmin/stream/welfare/domain/chat/repository/ChatMessageRepository.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,15 @@ | ||
| package kr.ac.kookmin.stream.welfare.domain.chat.repository; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.welfare.domain.chat.domain.ChatMessage; | ||
|
|
||
| public interface ChatMessageRepository { | ||
|
|
||
| /** | ||
| * 한 학생의 최근 대화를 <b>최신순</b>으로 가져온다. "최근 N개"를 자르려면 이 방향으로 정렬해야 하므로, | ||
| * 오래된 것부터 필요한 쪽(AI 서버에 보낼 때)은 쓰는 쪽에서 뒤집는다. | ||
| */ | ||
| List<ChatMessage> findRecentByMemberId(Long memberId, int limit); | ||
|
|
||
| ChatMessage save(ChatMessage message); | ||
| } |
14 changes: 14 additions & 0 deletions
14
...n/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/service/ChatService.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 kr.ac.kookmin.stream.welfare.domain.chat.service; | ||
|
|
||
| import kr.ac.kookmin.stream.welfare.domain.chat.client.ChatEventListener; | ||
|
|
||
| public interface ChatService { | ||
|
|
||
| /** | ||
| * 질문을 AI 서버에 보내고 답변을 조각으로 흘려보낸다. 스트림이 끝날 때까지 돌아오지 않는다. | ||
| * 질문과 답변은 {@code chat_messages}에 남긴다. | ||
| * | ||
| * @param listener 받은 이벤트를 넘길 곳. 보통 컨트롤러가 학생 쪽 연결에 그대로 쓴다 | ||
| */ | ||
| void answer(Long memberId, String question, ChatEventListener listener); | ||
| } |
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.
사용자가 개인 데이터를 물어볼 때는 어떻게 처리할지 궁금합니다 (ex 내 행사 신청 내역을 물어봤을때)
현재 chat이 welfare 안에 있어서 다른 도메인의 데이터를 조회하기 어려운데 백엔드에서 데이터를 넘겨줘야 한다면 app-api UseCase에서 필요한 데이터를 조회해 ChatContext record로 만들어 answer에 넘기는 방식으로 수정하면 어떨까요?
ai 서버가 직접 조회하는 구조라면 상관없겠지만 어떤 방식으로 하시는건지 확인하고싶습니다!
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.
좋은 질문 감사합니다! 개인 데이터는 AI 서버가 질문을 보고 필요할 때 백엔드 내부 API를 호출하는 구조로 가려고 합니다.
백엔드가 미리 조회해서
ChatContext로 넘기는 방식을 택하지 않은 이유는 다음과 같습니다.말씀하신 "chat이 welfare 안에 있어서 다른 도메인을 조회하기 어렵다"는 문제는 이 구조에서도 풀립니다. 조회는 chat 서비스가 아니라 AI 서버 전용 내부 API(내부 토큰 검증·내부망 한정)가 맡게 됩니다.
회원 ID는 모델이 정하지 않고, 백엔드가 보낸
X-User-Id헤더 값만 쓰도록 할 예정입니다. 그래야 다른 학생 정보가 조회되지 않습니다.인증 헤더 계약을 먼저 확정해야 해서 이번 PR 범위에서는 빼고 별도 이슈로 진행하겠습니다.