Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sofa.linkiving.domain.chat.ai;

import com.sofa.linkiving.domain.chat.dto.request.RagAnswerReq;
import com.sofa.linkiving.domain.chat.dto.response.RagAnswerRes;

public interface AnswerClient {
RagAnswerRes generateAnswer(RagAnswerReq request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.sofa.linkiving.domain.chat.ai;

import java.util.List;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import com.sofa.linkiving.domain.chat.dto.request.RagAnswerReq;
import com.sofa.linkiving.domain.chat.dto.response.RagAnswerRes;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@Primary
public class MockAnswerClient implements AnswerClient {

@Override
public RagAnswerRes generateAnswer(RagAnswerReq request) {
log.info("[Mock AI Request] User: {}, Question: {}, Mode: {}, HistoryCnt: {}",
request.userId(), request.question(), request.mode(), request.history().size());

return new RagAnswerRes(
"임시 답변",
List.of("3", "4"),
List.of(
new RagAnswerRes.ReasoningStep(
"임시 답변 스탭",
List.of("3", "4")
)
),
List.of("3", "4"),
false
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.springframework.validation.annotation.Validated;

import com.sofa.linkiving.domain.chat.dto.request.AnswerCancelReq;
import com.sofa.linkiving.domain.chat.dto.request.AnswerReq;
import com.sofa.linkiving.domain.chat.dto.request.CreateChatReq;
import com.sofa.linkiving.domain.chat.dto.response.ChatsRes;
import com.sofa.linkiving.domain.chat.dto.response.CreateChatRes;
Expand All @@ -20,18 +22,45 @@
@Tag(name = "Chat", description = """
AI 채팅 통합 명세 (HTTP + WebSocket)

### 📡 1. WebSocket 연결 정보 (필수)
답변을 실시간으로 수신하기 위해 **반드시 소켓 연결 및 구독**이 선행되어야 합니다.

### 📡 1. WebSocket 연결 정보
* **Socket Endpoint:** `ws://{domain}/ws/chat`
* **Subscribe Path:** `/topic/chat/{chatId}`
* **Auth Header:** `Authorization: Bearer {accessToken}` (CONNECT 프레임 헤더)
* **Subscribe Path:** `/user/queue/chat` (전역 구독)

### 🚀 2. 동작 흐름
1. **소켓 연결:** 프론트엔드에서 WebSocket 연결 및 `/topic/chat/{chatId}` 구독
2. **질문 전송:** `/app/send/{chatId}` (STOMP)로 질문 전송
3. **답변 수신:** 소켓 구독 채널로 토큰 단위 답변 스트리밍 (`String` 데이터)
4. **완료:** `END_OF_STREAM` 메시지 수신 시 스트리밍 종료
1. **소켓 연결:** 로그인 직후 `/user/queue/chat` 구독
2. **질문 전송:** `/send` 로 요청 전송
- Body: `{ "chatId": 1, "message": "질문" }`
3. **답변 수신:** 구독한 경로로 답변 도착 (chatId 포함됨)
**CASE A: 답변 생성 성공 (success: true)**
- AI의 답변과 참고 링크가 포함됩니다.
```json
{
"success": true,
"chatId": 1,
"messageId": 105,
"content": "질문하신 내용에 대한 AI 답변입니다...",
"step": ["질문 분석", "데이터 검색", "답변 생성"],
"links": [
{ "linkId": 10, "title": "관련 문서 제목", "url": "https://...", "imageUrl": "http://...", "summary": "요약 내용" }
]
}
```

**CASE B: 답변 생성 실패 (success: false)**
- 에러 상황입니다. `content` 필드에 **사용자가 보냈던 원래 질문**이 담겨옵니다.
- 프론트엔드 처리: 이 값을 다시 입력창(Input)에 채워주세요.
```json
{
"success": false,
"chatId": 1,
"messageId": null,
"content": "내 질문 내용",
"step": null,
"links": null
}
```
4. **답변 취소**: `/cancel` 로 요청 전송
- Body: `{ "chatId": 1 }`
""")
public interface ChatApi {
@Operation(summary = "채팅 기록 조회", description = "채팅 기록을 최신순으로 조회합니다. 무한 스크롤 방식으로 제공됩니다.")
Expand All @@ -58,8 +87,7 @@ BaseResponse<CreateChatRes> createChat(
@Operation(summary = "링크 삭제", description = "해당 링크방과 채팅 기록을 전부 Hard Delete 진행합니다.")
BaseResponse<String> deleteChat(Member member, Long chatId);

void sendMessage(@Parameter(description = "채팅방 Id", required = true) Long chatId,
@Parameter(description = "사용자 질문 내용", required = true) String message, Member member);
void sendMessage(AnswerReq req, Member member);

void cancelMessage(@Parameter(description = "채팅방 Id", required = true) Long chatId, Member member);
void cancelMessage(AnswerCancelReq req, Member member);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.sofa.linkiving.domain.chat.controller;

import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -12,6 +11,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.sofa.linkiving.domain.chat.dto.request.AnswerCancelReq;
import com.sofa.linkiving.domain.chat.dto.request.AnswerReq;
import com.sofa.linkiving.domain.chat.dto.request.CreateChatReq;
import com.sofa.linkiving.domain.chat.dto.response.ChatsRes;
import com.sofa.linkiving.domain.chat.dto.response.CreateChatRes;
Expand Down Expand Up @@ -51,15 +52,15 @@ public BaseResponse<String> deleteChat(@AuthMember Member member, @PathVariable
}

@Override
@MessageMapping("/send/{chatId}")
public void sendMessage(@DestinationVariable Long chatId, @Payload String message, @AuthMember Member member) {
chatFacade.generateAnswer(chatId, member, message);
@MessageMapping("/send")
public void sendMessage(@Payload AnswerReq req, @AuthMember Member member) {
chatFacade.generateAnswer(req.chatId(), member, req.message());
}

@Override
@MessageMapping("/cancel/{chatId}")
public void cancelMessage(@DestinationVariable Long chatId, @AuthMember Member member) {
chatFacade.cancelAnswer(chatId, member);
@MessageMapping("/cancel")
public void cancelMessage(@Payload AnswerCancelReq req, @AuthMember Member member) {
chatFacade.cancelAnswer(req.chatId(), member);
}

@Override
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.sofa.linkiving.domain.chat.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

public record AnswerCancelReq(
@Schema(description = "채팅방 ID")
Long chatId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sofa.linkiving.domain.chat.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

public record AnswerReq(
@Schema(description = "채팅방 ID")
Long chatId,
@Schema(description = "유저 질문 내용")
String message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.sofa.linkiving.domain.chat.dto.request;

import java.util.List;

import com.sofa.linkiving.domain.chat.enums.Mode;

public record RagAnswerReq(
Long userId,
String question,
List<String> history,
Mode mode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.sofa.linkiving.domain.chat.dto.response;

import java.util.List;

import com.sofa.linkiving.domain.chat.entity.Message;
import com.sofa.linkiving.domain.link.dto.internal.LinkDto;
import com.sofa.linkiving.domain.link.dto.response.LinkCardRes;

import io.swagger.v3.oas.annotations.media.Schema;

public record AnswerRes(
@Schema(description = "성공 여부")
Boolean success,
@Schema(description = "채팅방 ID")
Long chatId,
@Schema(description = "메세지 ID")
Long messageId,
@Schema(description = "답변 내용")
String content,
@Schema(description = "스텝 목록")
List<String> step,
@Schema(description = "첨부된 링크 목록")
List<LinkCardRes> links
) {
public static AnswerRes of(Long chatId, Message message, List<String> step, List<LinkDto> linkDtos) {
return new AnswerRes(
true,
chatId,
message.getId(),
message.getContent(),
step,
linkDtos.stream().map(LinkCardRes::from).toList()
);
}

public static AnswerRes error(Long chatId, String content) {
return new AnswerRes(
false,
chatId,
null,
content,
null,
null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.sofa.linkiving.domain.chat.dto.response;

import java.util.List;

public record RagAnswerRes(
String answer,
List<String> linkIds,
List<ReasoningStep> reasoningSteps,
List<String> relatedLinks,
boolean isFallback
) {
public record ReasoningStep(
String step,
List<String> linkIds
) {
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/sofa/linkiving/domain/chat/enums/Mode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.sofa.linkiving.domain.chat.enums;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Mode {
DETAILED("detailed"),
CONCISE("concise");

private final String value;

@JsonCreator
public static Mode from(String value) {
for (Mode mode : Mode.values()) {
if (mode.getValue().equalsIgnoreCase(value)) {
return mode;
}
}
return DETAILED;
}

@JsonValue
public String getValue() {
return value;
}
}
Loading