Skip to content
Merged
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
8 changes: 8 additions & 0 deletions chat_service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ java {

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
// Spring AI
implementation platform('org.springframework.ai:spring-ai-bom:1.0.0-M4')
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'
// Spring WebFlux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// WebSocket
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.synapse.chat_service.config;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
* 비동기 처리 설정
* AI 모델 호출의 대용량 트래픽을 고려한 스레드 풀 구성
*/
@Configuration
@EnableAsync
public class AsyncConfig {

/**
* AI 서비스 전용 스레드 풀
* 대규모 동시 요청을 처리할 수 있도록 설정
*/
@Bean(name = "aiTaskExecutor")
public Executor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(50); // 최대 스레드 수
executor.setQueueCapacity(100); // 대기 큐 크기
executor.setThreadNamePrefix("AI-"); // 스레드 이름 접두사
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;

import com.synapse.chat_service.filter.CustomAuthenticationFilter;

Expand All @@ -40,19 +39,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)

.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable())
.addFilterBefore(customAuthenticationFilter, BearerTokenAuthenticationFilter.class)

.authorizeHttpRequests(auth -> auth
.requestMatchers("/chat-test.html").permitAll()
// WebSocket 핸드셰이크 경로 ("/ws/**") - 인증된 사용자만 허용
.requestMatchers("/ws/**").authenticated()
.requestMatchers("/ws/**").permitAll()
// 채팅 관련 API ("/api/v1/messages/**", "/api/v1/ai-chat/**") - 인증된 사용자만 허용
.requestMatchers("/api/v1/messages/**", "/api/v1/ai-chat/**").authenticated()
// CSRF 토큰 발급 API ("/api/v1/csrf-token") - 인증된 사용자만 허용
.requestMatchers("/api/v1/csrf-token").authenticated()
.requestMatchers("/api/internal/**").access(AuthorizationManagers.allOf(
AuthorityAuthorizationManager.hasAuthority("SCOPE_api.internal"),
AuthorityAuthorizationManager.hasAuthority("SCOPE_chat:read")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
// 클라이언트에서 메시지를 받을 때 사용할 prefix
config.setApplicationDestinationPrefixes("/app");
// 클라이언트가 구독할 때 사용할 prefix (AI 응답 수신용)
config.enableSimpleBroker("/assistant")
config.enableSimpleBroker("/topic", "/queue")
.setTaskScheduler(heartbeatScheduler())
.setHeartbeatValue(new long[] {10000, 10000});
config.setUserDestinationPrefix("/user");
}

@Override
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.synapse.chat_service.controller;

import com.synapse.chat_service.dto.request.ChatMessageRequest;
import com.synapse.chat_service.dto.response.ChatHistoryResponse;
import com.synapse.chat_service.dto.response.MessageResponse;
import com.synapse.chat_service.service.MessageService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -20,58 +21,20 @@ public class AiChatController {

private final MessageService messageService;

@GetMapping("/history")
public ResponseEntity<List<MessageResponse.Simple>> getMyAiChatHistory(
@RequestHeader("X-User-Id") Long userId
) {
List<MessageResponse.Simple> response = messageService.getMessagesByUserId(userId);
return ResponseEntity.ok(response);
}

@GetMapping("/history/paging")
public ResponseEntity<Page<MessageResponse.Simple>> getMyAiChatHistoryWithPaging(
@RequestHeader("X-User-Id") Long userId,
@PageableDefault(size = 50, sort = "createdDate", direction = Sort.Direction.ASC) Pageable pageable
@GetMapping("/conversation/list")
public ResponseEntity<List<MessageResponse.ConversationInfo>> getMyConversationList(
@AuthenticationPrincipal UUID userId
) {
Page<MessageResponse.Simple> response = messageService.getMessagesByUserIdWithPaging(userId, pageable);
List<MessageResponse.ConversationInfo> response = messageService.getConversationListByUserId(userId);
return ResponseEntity.ok(response);
}

@GetMapping("/history/recent")
public ResponseEntity<Page<MessageResponse.Simple>> getMyAiChatHistoryRecentFirst(
@RequestHeader("X-User-Id") Long userId,
@PageableDefault(size = 50, sort = "createdDate", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<MessageResponse.Simple> response = messageService.getMessagesRecentFirst(userId, pageable);
return ResponseEntity.ok(response);
}

@GetMapping("/search")
public ResponseEntity<List<MessageResponse.Simple>> searchMyAiChatHistory(
@RequestHeader("X-User-Id") Long userId,
@RequestParam String keyword
) {
List<MessageResponse.Simple> response = messageService.searchMessages(userId, keyword);
return ResponseEntity.ok(response);
}

@GetMapping("/stats")
public ResponseEntity<AiChatStatsResponse> getMyAiChatStats(
@RequestHeader("X-User-Id") Long userId
public ResponseEntity<ChatHistoryResponse> getMyAiChatHistoryRecentFirst(
@AuthenticationPrincipal UUID userId,
@Valid ChatMessageRequest request
) {
long messageCount = messageService.getMessageCountByUserId(userId);
UUID conversationId = messageService.getConversationId(userId);

AiChatStatsResponse response = new AiChatStatsResponse(
conversationId,
messageCount
);

ChatHistoryResponse response = messageService.getMessagesRecentFirst(userId, request.size(), request.cursor());
return ResponseEntity.ok(response);
}

public record AiChatStatsResponse(
UUID conversationId,
long totalMessageCount
) {}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,26 @@
package com.synapse.chat_service.controller;

import java.util.UUID;

import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;

import com.synapse.chat_service.dto.request.MessageRequest;
import com.synapse.chat_service.dto.response.MessageResponse;
import com.synapse.chat_service.service.MessageService;
import jakarta.validation.Valid;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/messages")
@Controller
@RequiredArgsConstructor
public class MessageController {

private final MessageService messageService;

@PostMapping
public ResponseEntity<MessageResponse.Detail> createMessage(
@Valid @RequestBody MessageRequest.Create request
) {
MessageResponse.Detail response = messageService.createMessage(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@GetMapping("/{messageId}")
public ResponseEntity<MessageResponse.Detail> getMessage(
@PathVariable Long messageId
) {
MessageResponse.Detail response = messageService.getMessage(messageId);
return ResponseEntity.ok(response);
}

@DeleteMapping("/{messageId}")
public ResponseEntity<Void> deleteMessage(@PathVariable Long messageId) {
messageService.deleteMessage(messageId);
return ResponseEntity.noContent().build();

@MessageMapping("/chat")
public void handleChatMessage(MessageRequest.Chat chatMessage, @Header("simpUser") Authentication authentication) {
UUID userId = UUID.fromString(authentication.getName());
messageService.processAndRespondToMessage(userId, chatMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.synapse.chat_service.domain.entity;

import java.util.UUID;

import com.synapse.chat_service.domain.common.BaseTimeEntity;
import com.synapse.chat_service.domain.entity.enums.SubscriptionType;

import jakarta.persistence.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -25,25 +26,23 @@ public class ChatUsage extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "user_id", nullable = false, unique = true)
@NotNull
private Long userId;
@Column(name = "user_id", nullable = false, unique = true, columnDefinition = "uuid")
private UUID userId;

@Enumerated(EnumType.STRING)
@Column(name = "subscription_type", nullable = false)
@NotNull
private SubscriptionType subscriptionType;

@Column(name = "message_count", nullable = false)
@Min(0)
@Column(name = "message_count", nullable = false)
private Integer messageCount = 0;

@Column(name = "message_limit", nullable = false)
@Min(0)
@Column(name = "message_limit", nullable = false)
private Integer messageLimit;

@Builder
public ChatUsage(Long userId, SubscriptionType subscriptionType, Integer messageLimit) {
public ChatUsage(UUID userId, SubscriptionType subscriptionType, Integer messageLimit) {
this.userId = userId;
this.subscriptionType = subscriptionType;
this.messageLimit = messageLimit;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.synapse.chat_service.domain.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -29,15 +28,14 @@ public class Conversation extends BaseTimeEntity {
@Column(name = "conversation_id", columnDefinition = "UUID")
private UUID id;

@NotNull
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "user_id", nullable = false, unique = true, columnDefinition = "uuid")
private UUID userId;

@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Message> messages = new ArrayList<>();

@Builder
public Conversation(Long userId) {
public Conversation(UUID userId) {
this.userId = userId;
}
}
Loading
Loading