Skip to content

Commit 0c06b26

Browse files
authored
채팅 기능 구현 및 알림 기능 수정 (#57)
2 parents 39296a7 + 9dc980e commit 0c06b26

51 files changed

Lines changed: 499 additions & 68 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package MathCaptain.weakness.domain.Chat.controller;
2+
3+
import MathCaptain.weakness.domain.Chat.dto.request.ChatRequest;
4+
import MathCaptain.weakness.domain.Chat.dto.response.ChatResponse;
5+
import MathCaptain.weakness.domain.Chat.service.ChatService;
6+
import MathCaptain.weakness.domain.User.entity.Users;
7+
import MathCaptain.weakness.global.annotation.LoginUser;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.messaging.handler.annotation.MessageMapping;
10+
import org.springframework.messaging.simp.SimpMessagingTemplate;
11+
import org.springframework.web.bind.annotation.RestController;
12+
import java.util.List;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
public class ChatController {
17+
18+
private final SimpMessagingTemplate template;
19+
20+
private final ChatService chatService;
21+
22+
@MessageMapping("/chat.send") // 클라이언트 → /app/chat.send
23+
public void handleChat(ChatRequest request, @LoginUser Users loginUser) {
24+
// 1) 유저 메시지 저장 + 클라이언트에게 에코
25+
ChatResponse chatResponse = chatService.saveUserMessage(loginUser, request);
26+
template.convertAndSend("/sub/" + loginUser.getUserId(), chatResponse);
27+
28+
// 2) LLM 호출 & 응답 저장 + 브로드캐스트 (동기 방식)
29+
List<ChatResponse> llmResponses = chatService.askAI(loginUser, request);
30+
llmResponses.forEach(aiResp ->
31+
template.convertAndSend("/sub/" + loginUser.getUserId(), aiResp)
32+
);
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package MathCaptain.weakness.domain.Chat.dto.request;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
@Getter
7+
@NoArgsConstructor
8+
public class ChatRequest {
9+
10+
private String message;
11+
12+
private ChatRequest(String message) {
13+
this.message = message;
14+
}
15+
16+
public static ChatRequest of(String message) {
17+
return new ChatRequest(message);
18+
}
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package MathCaptain.weakness.domain.Chat.dto.request;
2+
3+
import MathCaptain.weakness.domain.Chat.entity.Chat;
4+
import MathCaptain.weakness.domain.User.entity.Users;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
public class LLMRequest {
13+
14+
private Long userId;
15+
16+
private String message;
17+
18+
private List<Chat> history;
19+
20+
private LLMRequest(Long userId, String message, List<Chat> history) {
21+
this.userId = userId;
22+
this.message = message;
23+
this.history = history;
24+
}
25+
26+
public static LLMRequest of(Users loginUser, ChatRequest request, List<Chat> history) {
27+
return new LLMRequest(loginUser.getUserId(), request.getMessage(), history);
28+
}
29+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package MathCaptain.weakness.domain.Chat.dto.response;
2+
3+
import MathCaptain.weakness.domain.Chat.entity.Chat;
4+
import MathCaptain.weakness.domain.common.enums.ChatRole;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
public class ChatResponse {
13+
14+
private Long userId;
15+
16+
private ChatRole role;
17+
18+
private String message;
19+
20+
private LocalDateTime sendTime;
21+
22+
private ChatResponse(Long userId, ChatRole role, String message, LocalDateTime sendTime) {
23+
this.userId = userId;
24+
this.role = role;
25+
this.message = message;
26+
this.sendTime = sendTime;
27+
}
28+
29+
public static ChatResponse of(Chat chat) {
30+
return new ChatResponse(
31+
chat.getUserId(),
32+
chat.getRole(),
33+
chat.getMessage(),
34+
chat.getSendTime()
35+
);
36+
}
37+
}

MathCaptain/weakness/src/main/java/MathCaptain/weakness/domain/Chat/entity/Chat.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package MathCaptain.weakness.domain.Chat.entity;
22

3-
import MathCaptain.weakness.domain.Chat.enums.ChatRole;
3+
import MathCaptain.weakness.domain.Chat.dto.request.ChatRequest;
4+
import MathCaptain.weakness.domain.Chat.dto.response.ChatResponse;
5+
import MathCaptain.weakness.domain.User.entity.Users;
6+
import MathCaptain.weakness.domain.common.enums.ChatRole;
47
import jakarta.persistence.*;
58
import lombok.AccessLevel;
9+
import lombok.Builder;
610
import lombok.Getter;
711
import lombok.NoArgsConstructor;
812
import org.springframework.data.annotation.CreatedDate;
@@ -30,4 +34,23 @@ public class Chat {
3034
@CreatedDate
3135
@Column(updatable = false)
3236
private LocalDateTime sendTime;
37+
38+
@Builder
39+
private Chat(Long userId, ChatRole role, String message) {
40+
this.userId = userId;
41+
this.role = role;
42+
this.message = message;
43+
}
44+
45+
public static Chat of(Users user, ChatRequest request, ChatRole role) {
46+
return new Chat(user.getUserId(), role, request.getMessage());
47+
}
48+
49+
public static Chat of(ChatResponse response) {
50+
return Chat.builder()
51+
.userId(response.getUserId())
52+
.role(ChatRole.ASSISTANT)
53+
.message(response.getMessage())
54+
.build();
55+
}
3356
}

MathCaptain/weakness/src/main/java/MathCaptain/weakness/domain/Chat/repository/ChatRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
66

7+
import java.util.List;
8+
79
@Repository
810
public interface ChatRepository extends JpaRepository<Chat, Long> {
11+
List<Chat> findAllByUserIdOrderBySendTimeAsc(Long userId);
912
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package MathCaptain.weakness.domain.Chat.service;
2+
3+
import MathCaptain.weakness.domain.Chat.dto.request.ChatRequest;
4+
import MathCaptain.weakness.domain.Chat.dto.response.ChatResponse;
5+
import MathCaptain.weakness.domain.Chat.entity.Chat;
6+
import MathCaptain.weakness.domain.Chat.repository.ChatRepository;
7+
import MathCaptain.weakness.domain.User.entity.Users;
8+
import MathCaptain.weakness.domain.common.enums.ChatRole;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
13+
import java.util.List;
14+
import java.util.UUID;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
@Transactional(readOnly = true)
19+
public class ChatService {
20+
21+
public static final ChatRole USER = ChatRole.USER;
22+
private final ChatRepository chatRepository;
23+
private final LLMClient llm;
24+
25+
@Transactional
26+
public ChatResponse saveUserMessage(Users loginUser, ChatRequest request) {
27+
Chat message = Chat.of(loginUser, request, USER);
28+
Chat saved = chatRepository.save(message);
29+
return ChatResponse.of(saved);
30+
}
31+
32+
public List<ChatResponse> askAI(Users loginUser, ChatRequest request) {
33+
List<Chat> history = chatRepository.findAllByUserIdOrderBySendTimeAsc(loginUser.getUserId());
34+
List<Chat> aiChats = llm.call(loginUser, history, request);
35+
return aiChats.stream()
36+
.map(this::storeAndTransform)
37+
.toList();
38+
}
39+
40+
private ChatResponse storeAndTransform(Chat message) {
41+
Chat saved = chatRepository.save(message);
42+
return ChatResponse.of(saved);
43+
}
44+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package MathCaptain.weakness.domain.Chat.service;
2+
3+
import MathCaptain.weakness.domain.Chat.dto.request.ChatRequest;
4+
import MathCaptain.weakness.domain.Chat.dto.request.LLMRequest;
5+
import MathCaptain.weakness.domain.Chat.dto.response.ChatResponse;
6+
import MathCaptain.weakness.domain.Chat.entity.Chat;
7+
import MathCaptain.weakness.domain.User.entity.Users;
8+
import groovy.util.logging.Slf4j;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.web.client.HttpClientErrorException;
14+
import org.springframework.web.client.HttpServerErrorException;
15+
import org.springframework.web.client.ResourceAccessException;
16+
import org.springframework.web.client.RestTemplate;
17+
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
@lombok.extern.slf4j.Slf4j
24+
@Slf4j
25+
@Component
26+
@RequiredArgsConstructor
27+
public class LLMClient {
28+
29+
private final RestTemplate restTemplate;
30+
31+
@Value("${llm.server.url}")
32+
private String baseUrl;
33+
34+
public List<Chat> call(Users loginUser, List<Chat> history, ChatRequest request) {
35+
try {
36+
LLMRequest llmRequest = LLMRequest.of(loginUser, request, history);
37+
38+
ResponseEntity<ChatResponse[]> response = restTemplate.postForEntity(
39+
// TODO : 엔드포인트 수정
40+
baseUrl + "/v1/chat",
41+
llmRequest,
42+
// 응답을 ChatResponse[] (배열)로 역직렬화 (수정 필요)
43+
ChatResponse[].class
44+
);
45+
46+
ChatResponse[] body = response.getBody();
47+
if (body == null) {
48+
return Collections.emptyList();
49+
}
50+
return Arrays.stream(body)
51+
.map(Chat::of)
52+
.collect(Collectors.toList());
53+
54+
} catch (ResourceAccessException timeoutEx) {
55+
log.error("LLM 서버 응답 지연", timeoutEx);
56+
return List.of();
57+
} catch (HttpClientErrorException | HttpServerErrorException httpEx) {
58+
// 4XX/5XX 응답
59+
log.error("LLM 호출 실패: {}", httpEx.getStatusCode(), httpEx);
60+
return List.of();
61+
}
62+
}
63+
}

MathCaptain/weakness/src/main/java/MathCaptain/weakness/domain/Group/controller/GroupJoinController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package MathCaptain.weakness.domain.Group.controller;
22

33
import MathCaptain.weakness.domain.Group.dto.request.GroupJoinRequest;
4-
import MathCaptain.weakness.domain.Group.enums.RequestStatus;
4+
import MathCaptain.weakness.domain.common.enums.RequestStatus;
55
import MathCaptain.weakness.domain.Group.service.GroupJoinService;
66
import MathCaptain.weakness.domain.Group.service.RelationService;
77
import MathCaptain.weakness.domain.Notification.service.NotificationService;

MathCaptain/weakness/src/main/java/MathCaptain/weakness/domain/Group/dto/request/GroupCreateRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package MathCaptain.weakness.domain.Group.dto.request;
22

3-
import MathCaptain.weakness.domain.Group.enums.CategoryStatus;
3+
import MathCaptain.weakness.domain.common.enums.CategoryStatus;
44
import jakarta.validation.constraints.NotNull;
55
import jakarta.validation.constraints.Size;
66
import lombok.*;

0 commit comments

Comments
 (0)