MVC구조로 변경

This commit is contained in:
mskim 2025-12-10 11:09:08 +09:00
parent 5947340574
commit dc8015a3bf
31 changed files with 2258 additions and 38 deletions

863
API명세서.md Normal file
View File

@ -0,0 +1,863 @@
# Dewey MCP 서버 API 명세서
## 개요
Dewey MCP (Model Context Protocol) 서버는 JSON-RPC 2.0 프로토콜을 기반으로 AI 장기기억 시스템의 기능을 제공합니다.
- **프로토콜 버전**: 2024-11-05
- **서버 이름**: Dewey Memory Server
- **서버 버전**: 0.0.1-SNAPSHOT
- **Base URL**: `http://localhost:8080/mcp`
- **아키텍처**: MVC (Model-View-Controller) 패턴
---
## 아키텍처
### MVC 구조
```
Controller (McpController)
API Service (McpService)
Domain Service (MemoryService)
Repository (MemoryRepository)
Database (PostgreSQL / Redis)
```
### 계층별 역할
- **Controller**: HTTP 요청 처리 및 라우팅
- **API Service**: Controller와 Domain Service 사이의 어댑터
- **Domain Service**: 비즈니스 로직 처리
- **Repository**: 데이터 접근 계층
- **Model**: 도메인 엔티티 및 DTO
---
## 엔드포인트
### POST /mcp
모든 MCP 요청은 이 단일 엔드포인트로 처리됩니다. 요청 본문은 JSON-RPC 2.0 형식을 따릅니다.
**요청 헤더:**
```
Content-Type: application/json
```
---
## MCP 메서드
### 1. initialize
MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "client-name",
"version": "1.0.0"
}
}
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": {
"name": "Dewey Memory Server",
"version": "0.0.1-SNAPSHOT"
}
}
}
```
**응답 DTO**: `McpInitializeResponse`
---
### 2. tools/list
사용 가능한 모든 도구 목록을 반환합니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "2",
"method": "tools/list"
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"tools": [
{
"name": "store_memory",
"description": "메모리를 Redis에 임시 저장합니다",
"inputSchema": {
"type": "object",
"properties": {
"memory_text": {
"type": "string",
"description": "저장할 메모리 텍스트"
},
"user_id": {
"type": "string",
"description": "사용자 ID"
},
"importance": {
"type": "integer",
"description": "중요도 (1-5)",
"minimum": 1,
"maximum": 5
}
},
"required": ["memory_text", "user_id"]
}
},
{
"name": "retrieve_memory",
"description": "사용자의 메모리를 조회합니다",
"inputSchema": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "사용자 ID"
},
"limit": {
"type": "integer",
"description": "조회할 메모리 개수",
"default": 10
}
},
"required": ["user_id"]
}
},
{
"name": "search_memory",
"description": "벡터 유사도 기반으로 메모리를 검색합니다",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색 쿼리"
},
"user_id": {
"type": "string",
"description": "사용자 ID (선택)"
},
"limit": {
"type": "integer",
"description": "검색 결과 개수",
"default": 5
}
},
"required": ["query"]
}
}
]
}
}
```
**응답 DTO**: `McpToolResponse.ToolListResponse`
---
### 3. tools/call
특정 도구를 실행합니다. 도구 실행 결과에 따라 응답 내용이 달라집니다.
#### 요청 형식
```json
{
"jsonrpc": "2.0",
"id": "3",
"method": "tools/call",
"params": {
"arguments": {
"name": "도구명",
"arguments": {
"도구별_파라미터": "값"
}
}
}
}
```
#### 응답 형식
```json
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"isError": false,
"content": "실행 결과 메시지",
"parts": [],
"metadata": {
"tool": "도구명",
"timestamp": 1701234567890
}
}
}
```
**응답 DTO**: `McpToolCallResponse`
#### 도구별 사용 예시
##### store_memory
메모리를 Redis에 임시 저장합니다.
**요청:**
```json
{
"jsonrpc": "2.0",
"id": "4",
"method": "tools/call",
"params": {
"arguments": {
"name": "store_memory",
"arguments": {
"memory_text": "오늘 사용자가 피자를 주문했습니다",
"user_id": "user123",
"importance": 4
}
}
}
}
```
**성공 응답:**
```json
{
"jsonrpc": "2.0",
"id": "4",
"result": {
"isError": false,
"content": "Memory stored successfully",
"parts": [],
"metadata": {
"tool": "store_memory",
"timestamp": 1701234567890
}
}
}
```
**비즈니스 로직**: `MemoryService.storeTemporaryMemory()` 호출
---
##### retrieve_memory
사용자의 영구 메모리를 PostgreSQL에서 조회합니다.
**요청:**
```json
{
"jsonrpc": "2.0",
"id": "5",
"method": "tools/call",
"params": {
"arguments": {
"name": "retrieve_memory",
"arguments": {
"user_id": "user123",
"limit": 10
}
}
}
}
```
**성공 응답:**
```json
{
"jsonrpc": "2.0",
"id": "5",
"result": {
"isError": false,
"content": "Retrieved 10 memories",
"parts": [],
"metadata": {
"tool": "retrieve_memory",
"timestamp": 1701234567890
}
}
}
```
**비즈니스 로직**: `MemoryService.getPermanentMemories()` 호출
---
##### search_memory
벡터 유사도 기반으로 메모리를 검색합니다.
**요청:**
```json
{
"jsonrpc": "2.0",
"id": "6",
"method": "tools/call",
"params": {
"arguments": {
"name": "search_memory",
"arguments": {
"query": "커피",
"user_id": "user123",
"limit": 5
}
}
}
}
```
**성공 응답:**
```json
{
"jsonrpc": "2.0",
"id": "6",
"result": {
"isError": false,
"content": "Found 3 matching memories",
"parts": [],
"metadata": {
"tool": "search_memory",
"timestamp": 1701234567890
}
}
}
```
**비즈니스 로직**: `MemoryService.searchMemoriesByVector()` 호출
**에러 응답 예시:**
```json
{
"jsonrpc": "2.0",
"id": "6",
"result": {
"isError": true,
"content": "Error: Unknown tool: invalid_tool",
"parts": [],
"metadata": {
"tool": "invalid_tool",
"timestamp": 1701234567890
}
}
}
```
---
### 4. resources/list
사용 가능한 모든 리소스 목록을 반환합니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "7",
"method": "resources/list"
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "7",
"result": {
"resources": [
{
"uri": "memory://recent",
"name": "Recent Memories",
"description": "최근 저장된 메모리 리소스",
"mimeType": "application/json"
},
{
"uri": "memory://long-term",
"name": "Long-term Memories",
"description": "장기 저장된 메모리 리소스",
"mimeType": "application/json"
}
]
}
}
```
**응답 DTO**: `McpResourceResponse.ResourceListResponse`
---
### 5. resources/read
특정 리소스의 내용을 읽습니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "8",
"method": "resources/read",
"params": {
"uri": "memory://recent"
}
}
```
또는
```json
{
"jsonrpc": "2.0",
"id": "8",
"method": "resources/read",
"params": {
"arguments": {
"uri": "memory://recent"
}
}
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "8",
"result": {
"uri": "memory://recent",
"mimeType": "application/json",
"text": "{\"message\": \"Resource content for memory://recent\"}",
"metadata": {
"uri": "memory://recent",
"timestamp": 1701234567890
}
}
}
```
**응답 DTO**: `McpResourceResponse.ResourceReadResponse`
---
### 6. prompts/list
사용 가능한 모든 프롬프트 목록을 반환합니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "9",
"method": "prompts/list"
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "9",
"result": {
"prompts": [
{
"name": "memory_summary",
"description": "메모리 목록을 요약하는 프롬프트",
"arguments": [
{
"name": "memories",
"description": "요약할 메모리 텍스트 목록",
"required": true
}
]
},
{
"name": "memory_search",
"description": "메모리 검색을 위한 프롬프트",
"arguments": [
{
"name": "query",
"description": "검색할 키워드",
"required": true
}
]
}
]
}
}
```
**응답 DTO**: `McpPromptResponse.PromptListResponse`
---
### 7. prompts/get
특정 프롬프트를 가져옵니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"id": "10",
"method": "prompts/get",
"params": {
"arguments": {
"name": "memory_summary",
"arguments": {
"memories": ["메모리1", "메모리2", "메모리3"]
}
}
}
}
```
#### 응답
```json
{
"jsonrpc": "2.0",
"id": "10",
"result": {
"name": "memory_summary",
"messages": [
{
"role": "user",
"content": "Prompt: memory_summary"
}
],
"arguments": {
"memories": ["메모리1", "메모리2", "메모리3"]
}
}
}
```
**응답 DTO**: `McpPromptResponse.PromptGetResponse`
---
## 에러 응답
에러가 발생한 경우 다음과 같은 형식으로 응답됩니다:
```json
{
"jsonrpc": "2.0",
"id": "1",
"error": {
"code": -32603,
"message": "Internal error",
"data": null
}
}
```
### 에러 코드
| 코드 | 의미 | 설명 |
|------|------|------|
| `-32600` | Invalid Request | 요청 형식이 잘못됨 |
| `-32601` | Method not found | 존재하지 않는 메서드 호출 |
| `-32602` | Invalid params | 파라미터가 잘못됨 |
| `-32603` | Internal error | 서버 내부 오류 |
| `-32700` | Parse error | JSON 파싱 오류 |
**에러 처리**: `GlobalExceptionHandler`에서 전역 예외 처리
---
## 사용 예시
### cURL 예시
#### initialize
```bash
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}'
```
#### tools/list
```bash
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "2",
"method": "tools/list"
}'
```
#### tools/call - store_memory
```bash
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "3",
"method": "tools/call",
"params": {
"arguments": {
"name": "store_memory",
"arguments": {
"memory_text": "사용자는 파이썬을 좋아합니다",
"user_id": "user123",
"importance": 4
}
}
}
}'
```
#### tools/call - retrieve_memory
```bash
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "4",
"method": "tools/call",
"params": {
"arguments": {
"name": "retrieve_memory",
"arguments": {
"user_id": "user123",
"limit": 10
}
}
}
}'
```
#### tools/call - search_memory
```bash
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "5",
"method": "tools/call",
"params": {
"arguments": {
"name": "search_memory",
"arguments": {
"query": "커피",
"user_id": "user123",
"limit": 5
}
}
}
}'
```
---
## 데이터 모델
### Memory (도메인 엔티티)
PostgreSQL에 저장되는 영구 메모리 엔티티입니다.
```java
@Entity
@Table(name = "memories")
public class Memory {
private Long id;
private String userId;
private String memoryText;
private Integer importance; // 1~5
private String tags; // JSON 형식
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
### TemporaryMemory (도메인 모델)
Redis에 저장되는 임시 메모리 모델입니다.
```java
public class TemporaryMemory {
private String id; // Redis key
private String userId;
private String memoryText;
private Integer importance;
private LocalDateTime createdAt;
private LocalDateTime expiresAt; // TTL 기반
}
```
### MemoryResponse (응답 DTO)
```java
public class MemoryResponse {
private Long id;
private String userId;
private String memoryText;
private Integer importance;
private String tags;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
---
## 참고사항
### 1. 요청/응답 형식
- 모든 요청은 `Content-Type: application/json` 헤더가 필요합니다.
- `id` 필드는 요청과 응답을 매칭하기 위해 사용됩니다.
- 모든 응답은 `jsonrpc: "2.0"` 필드를 포함합니다.
### 2. 아키텍처
- **MVC 패턴**을 따르며, 각 계층이 명확히 분리되어 있습니다.
- **Controller**는 요청 처리만 담당합니다.
- **API Service**는 Controller와 Domain Service 사이의 어댑터 역할을 합니다.
- **Domain Service**는 비즈니스 로직을 처리합니다.
- **Repository**는 데이터 접근만 담당합니다.
### 3. 데이터 저장
- **임시 메모리**: Redis에 저장 (TTL 3일)
- **영구 메모리**: PostgreSQL에 저장
- **벡터 검색**: PostgreSQL의 pgvector 확장 사용 (향후 구현)
### 4. CORS 설정
- CORS는 모든 origin에서 허용되도록 설정되어 있습니다 (`/mcp/**`).
- 프로덕션 환경에서는 특정 origin만 허용하도록 변경하는 것을 권장합니다.
### 5. 예외 처리
- 전역 예외 처리는 `GlobalExceptionHandler`에서 수행됩니다.
- 유효성 검증 오류는 `MethodArgumentNotValidException`으로 처리됩니다.
- 모든 예외는 JSON-RPC 2.0 에러 형식으로 변환됩니다.
### 6. 구현 상태
- ✅ 기본 MCP 프로토콜 구현 완료
- ✅ 도메인 모델 및 Repository 생성 완료
- ✅ MVC 구조 재설계 완료
- ⏳ Redis 연동 (TODO)
- ⏳ 벡터 검색 구현 (TODO)
- ⏳ 배치 처리 구현 (TODO)
---
## 버전 정보
- **문서 버전**: 2.0.0
- **최종 업데이트**: 2025-12-10
- **서버 버전**: 0.0.1-SNAPSHOT
- **아키텍처**: MVC (Model-View-Controller)
---
## 관련 파일 구조
```
src/main/java/com/pandol365/dewey/
├── api/
│ ├── controller/
│ │ └── McpController.java # Controller 계층
│ ├── dto/
│ │ ├── request/ # 요청 DTO
│ │ │ ├── McpJsonRpcRequest.java
│ │ │ ├── MemoryStoreRequest.java
│ │ │ └── MemorySearchRequest.java
│ │ └── response/ # 응답 DTO (View)
│ │ ├── McpJsonRpcResponse.java
│ │ ├── McpInitializeResponse.java
│ │ ├── McpToolResponse.java
│ │ ├── McpResourceResponse.java
│ │ ├── McpPromptResponse.java
│ │ ├── McpToolCallResponse.java
│ │ └── MemoryResponse.java
│ └── service/ # API Service 계층
│ ├── McpService.java
│ └── impl/
│ └── McpServiceImpl.java
├── domain/
│ └── memory/
│ ├── model/ # 도메인 모델 (Model)
│ │ ├── Memory.java
│ │ └── TemporaryMemory.java
│ ├── repository/ # Repository 계층
│ │ └── MemoryRepository.java
│ └── service/ # Domain Service 계층
│ ├── MemoryService.java
│ └── impl/
│ └── MemoryServiceImpl.java
└── exception/
└── GlobalExceptionHandler.java # 예외 처리
```

View File

@ -2,8 +2,11 @@ package com.pandol365.dewey;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.ai.vectorstore.pgvector.autoconfigure.PgVectorStoreAutoConfiguration;
@SpringBootApplication @SpringBootApplication(exclude = {
PgVectorStoreAutoConfiguration.class
})
public class DeweyApplication { public class DeweyApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(DeweyApplication.class, args); SpringApplication.run(DeweyApplication.class, args);

View File

@ -0,0 +1,44 @@
package com.pandol365.dewey.api.dto.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* MCP JSON-RPC 2.0 요청 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpJsonRpcRequest {
@JsonProperty("jsonrpc")
@Builder.Default
private String jsonrpc = "2.0";
private String id;
private String method;
private McpParams params;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class McpParams {
private String protocolVersion;
private Map<String, Object> capabilities;
private Map<String, Object> clientInfo;
private String name;
private String uri;
private String prompt;
private Object arguments;
}
}

View File

@ -0,0 +1,28 @@
package com.pandol365.dewey.api.dto.request;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 메모리 검색 요청 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemorySearchRequest {
@NotBlank(message = "검색 쿼리는 필수입니다")
private String query;
private String userId;
@Min(value = 1, message = "limit은 1 이상이어야 합니다")
@Builder.Default
private Integer limit = 5;
}

View File

@ -0,0 +1,30 @@
package com.pandol365.dewey.api.dto.request;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 메모리 저장 요청 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemoryStoreRequest {
@NotBlank(message = "메모리 텍스트는 필수입니다")
private String memoryText;
@NotBlank(message = "사용자 ID는 필수입니다")
private String userId;
@Min(value = 1, message = "중요도는 1 이상이어야 합니다")
@Max(value = 5, message = "중요도는 5 이하여야 합니다")
private Integer importance;
}

View File

@ -0,0 +1,34 @@
package com.pandol365.dewey.api.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* MCP initialize 메서드 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpInitializeResponse {
private String protocolVersion;
private Map<String, Object> capabilities;
private ServerInfo serverInfo;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ServerInfo {
private String name;
private String version;
}
}

View File

@ -0,0 +1,38 @@
package com.pandol365.dewey.api.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* MCP JSON-RPC 2.0 응답 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpJsonRpcResponse {
@JsonProperty("jsonrpc")
@Builder.Default
private String jsonrpc = "2.0";
private String id;
private Object result;
private McpError error;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class McpError {
private Integer code;
private String message;
private Object data;
}
}

View File

@ -0,0 +1,63 @@
package com.pandol365.dewey.api.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Prompt 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpPromptResponse {
private String name;
private String description;
private List<PromptArgument> arguments;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptArgument {
private String name;
private String description;
private Boolean required;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptListResponse {
private List<McpPromptResponse> prompts;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptGetResponse {
private String name;
private List<PromptMessage> messages;
private Map<String, Object> arguments;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptMessage {
private String role;
private Object content;
}
}

View File

@ -0,0 +1,47 @@
package com.pandol365.dewey.api.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Resource 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpResourceResponse {
private String uri;
private String name;
private String description;
private String mimeType;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceListResponse {
private List<McpResourceResponse> resources;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceReadResponse {
private String uri;
private String mimeType;
private String text;
private Map<String, Object> metadata;
}
}

View File

@ -0,0 +1,30 @@
package com.pandol365.dewey.api.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP tools/call 메서드 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpToolCallResponse {
@JsonProperty("isError")
private Boolean isError;
private String content;
private List<Map<String, Object>> parts;
private Map<String, Object> metadata;
}

View File

@ -0,0 +1,36 @@
package com.pandol365.dewey.api.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Tool 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpToolResponse {
private String name;
private String description;
@JsonProperty("inputSchema")
private Map<String, Object> inputSchema;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ToolListResponse {
private List<McpToolResponse> tools;
}
}

View File

@ -0,0 +1,33 @@
package com.pandol365.dewey.api.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 메모리 응답 DTO (View)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemoryResponse {
private Long id;
private String userId;
private String memoryText;
private Integer importance;
private String tags;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,40 @@
package com.pandol365.dewey.api.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* MCP JSON-RPC 2.0 Request DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpRequest {
@JsonProperty("jsonrpc")
@Builder.Default
private String jsonrpc = "2.0";
private String id;
private String method;
private McpParams params;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class McpParams {
private String name;
private String version;
private Object arguments;
private String uri;
private String prompt;
}
}

View File

@ -1,19 +0,0 @@
package com.pandol365.dewey.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpHealthResponse {
private String status;
private LocalDateTime timestamp;
}

View File

@ -1,18 +0,0 @@
package com.pandol365.dewey.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpInfoResponse {
private String name;
private String version;
private String description;
}

View File

@ -0,0 +1,34 @@
package com.pandol365.dewey.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* MCP initialize 메서드 응답 결과
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpInitializeResult {
private String protocolVersion;
private Map<String, Object> capabilities;
private ServerInfo serverInfo;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ServerInfo {
private String name;
private String version;
}
}

View File

@ -0,0 +1,63 @@
package com.pandol365.dewey.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Prompt 정의
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpPrompt {
private String name;
private String description;
private List<PromptArgument> arguments;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptArgument {
private String name;
private String description;
private Boolean required;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptListResult {
private List<McpPrompt> prompts;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptGetResult {
private String name;
private List<PromptMessage> messages;
private Map<String, Object> arguments;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PromptMessage {
private String role;
private Object content;
}
}

View File

@ -0,0 +1,47 @@
package com.pandol365.dewey.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Resource 정의
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpResource {
private String uri;
private String name;
private String description;
private String mimeType;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceListResult {
private List<McpResource> resources;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceReadResult {
private String uri;
private String mimeType;
private String text;
private Map<String, Object> metadata;
}
}

View File

@ -0,0 +1,38 @@
package com.pandol365.dewey.api.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* MCP JSON-RPC 2.0 Response DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpResponse {
@JsonProperty("jsonrpc")
@Builder.Default
private String jsonrpc = "2.0";
private String id;
private Object result;
private McpError error;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class McpError {
private Integer code;
private String message;
private Object data;
}
}

View File

@ -0,0 +1,36 @@
package com.pandol365.dewey.api.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP Tool 정의
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpTool {
private String name;
private String description;
@JsonProperty("inputSchema")
private Map<String, Object> inputSchema;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ToolListResult {
private List<McpTool> tools;
}
}

View File

@ -0,0 +1,30 @@
package com.pandol365.dewey.api.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* MCP tools/call 메서드 응답 결과
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class McpToolCallResult {
@JsonProperty("isError")
private Boolean isError;
private String content;
private List<Map<String, Object>> parts;
private Map<String, Object> metadata;
}

View File

@ -0,0 +1,46 @@
package com.pandol365.dewey.api.service;
import com.pandol365.dewey.api.dto.request.McpJsonRpcRequest;
import com.pandol365.dewey.api.dto.response.*;
/**
* MCP 서버 비즈니스 로직 인터페이스
* Controller와 Domain Service 사이의 어댑터 역할
*/
public interface McpService {
/**
* MCP 서버 초기화
*/
McpInitializeResponse initialize(McpJsonRpcRequest.McpParams params);
/**
* 사용 가능한 도구 목록 반환
*/
McpToolResponse.ToolListResponse listTools();
/**
* 도구 호출 실행
*/
McpToolCallResponse callTool(String name, Object arguments);
/**
* 사용 가능한 리소스 목록 반환
*/
McpResourceResponse.ResourceListResponse listResources();
/**
* 리소스 읽기
*/
McpResourceResponse.ResourceReadResponse readResource(String uri);
/**
* 사용 가능한 프롬프트 목록 반환
*/
McpPromptResponse.PromptListResponse listPrompts();
/**
* 프롬프트 가져오기
*/
McpPromptResponse.PromptGetResponse getPrompt(String name, Object arguments);
}

View File

@ -0,0 +1,293 @@
package com.pandol365.dewey.api.service.impl;
import com.pandol365.dewey.api.dto.request.McpJsonRpcRequest;
import com.pandol365.dewey.api.dto.response.*;
import com.pandol365.dewey.api.service.McpService;
import com.pandol365.dewey.domain.memory.service.MemoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* MCP 서버 구현체
* Controller와 Domain Service 사이의 어댑터 역할 수행
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class McpServiceImpl implements McpService {
private static final String PROTOCOL_VERSION = "2024-11-05";
private static final String SERVER_NAME = "Dewey Memory Server";
private static final String SERVER_VERSION = "0.0.1-SNAPSHOT";
private final MemoryService memoryService;
@Override
public McpInitializeResponse initialize(McpJsonRpcRequest.McpParams params) {
log.info("MCP 서버 초기화 요청: {}", params);
Map<String, Object> capabilities = new HashMap<>();
capabilities.put("tools", Map.of());
capabilities.put("resources", Map.of());
capabilities.put("prompts", Map.of());
McpInitializeResponse.ServerInfo serverInfo = McpInitializeResponse.ServerInfo.builder()
.name(SERVER_NAME)
.version(SERVER_VERSION)
.build();
return McpInitializeResponse.builder()
.protocolVersion(PROTOCOL_VERSION)
.capabilities(capabilities)
.serverInfo(serverInfo)
.build();
}
@Override
public McpToolResponse.ToolListResponse listTools() {
log.info("MCP 도구 목록 조회");
List<McpToolResponse> tools = new ArrayList<>();
tools.add(createMemoryStoreTool());
tools.add(createMemoryRetrieveTool());
tools.add(createMemorySearchTool());
return McpToolResponse.ToolListResponse.builder()
.tools(tools)
.build();
}
@Override
public McpToolCallResponse callTool(String name, Object arguments) {
log.info("MCP 도구 호출: name={}, arguments={}", name, arguments);
@SuppressWarnings("unchecked")
Map<String, Object> args = arguments instanceof Map ? (Map<String, Object>) arguments : new HashMap<>();
Map<String, Object> metadata = new HashMap<>();
metadata.put("tool", name);
metadata.put("timestamp", System.currentTimeMillis());
try {
switch (name) {
case "store_memory" -> {
String userId = (String) args.get("user_id");
String memoryText = (String) args.get("memory_text");
Integer importance = args.get("importance") != null ?
((Number) args.get("importance")).intValue() : 1;
memoryService.storeTemporaryMemory(userId, memoryText, importance);
return McpToolCallResponse.builder()
.isError(false)
.content("Memory stored successfully")
.parts(Collections.emptyList())
.metadata(metadata)
.build();
}
case "retrieve_memory" -> {
String userId = (String) args.get("user_id");
Integer limit = args.get("limit") != null ?
((Number) args.get("limit")).intValue() : 10;
var memories = memoryService.getPermanentMemories(userId, limit);
return McpToolCallResponse.builder()
.isError(false)
.content("Retrieved " + memories.size() + " memories")
.parts(Collections.emptyList())
.metadata(metadata)
.build();
}
case "search_memory" -> {
String query = (String) args.get("query");
String userId = (String) args.get("user_id");
Integer limit = args.get("limit") != null ?
((Number) args.get("limit")).intValue() : 5;
var memories = memoryService.searchMemoriesByVector(query, userId, limit);
return McpToolCallResponse.builder()
.isError(false)
.content("Found " + memories.size() + " matching memories")
.parts(Collections.emptyList())
.metadata(metadata)
.build();
}
default -> {
return McpToolCallResponse.builder()
.isError(true)
.content("Unknown tool: " + name)
.parts(Collections.emptyList())
.metadata(metadata)
.build();
}
}
} catch (Exception e) {
log.error("Tool execution error: {}", e.getMessage(), e);
return McpToolCallResponse.builder()
.isError(true)
.content("Error: " + e.getMessage())
.parts(Collections.emptyList())
.metadata(metadata)
.build();
}
}
@Override
public McpResourceResponse.ResourceListResponse listResources() {
log.info("MCP 리소스 목록 조회");
List<McpResourceResponse> resources = new ArrayList<>();
resources.add(McpResourceResponse.builder()
.uri("memory://recent")
.name("Recent Memories")
.description("최근 저장된 메모리 리소스")
.mimeType("application/json")
.build());
resources.add(McpResourceResponse.builder()
.uri("memory://long-term")
.name("Long-term Memories")
.description("장기 저장된 메모리 리소스")
.mimeType("application/json")
.build());
return McpResourceResponse.ResourceListResponse.builder()
.resources(resources)
.build();
}
@Override
public McpResourceResponse.ResourceReadResponse readResource(String uri) {
log.info("MCP 리소스 읽기: uri={}", uri);
Map<String, Object> metadata = new HashMap<>();
metadata.put("uri", uri);
metadata.put("timestamp", System.currentTimeMillis());
return McpResourceResponse.ResourceReadResponse.builder()
.uri(uri)
.mimeType("application/json")
.text("{\"message\": \"Resource content for " + uri + "\"}")
.metadata(metadata)
.build();
}
@Override
public McpPromptResponse.PromptListResponse listPrompts() {
log.info("MCP 프롬프트 목록 조회");
List<McpPromptResponse> prompts = new ArrayList<>();
prompts.add(createMemorySummaryPrompt());
prompts.add(createMemorySearchPrompt());
return McpPromptResponse.PromptListResponse.builder()
.prompts(prompts)
.build();
}
@Override
public McpPromptResponse.PromptGetResponse getPrompt(String name, Object arguments) {
log.info("MCP 프롬프트 가져오기: name={}, arguments={}", name, arguments);
List<McpPromptResponse.PromptMessage> messages = new ArrayList<>();
messages.add(McpPromptResponse.PromptMessage.builder()
.role("user")
.content("Prompt: " + name)
.build());
@SuppressWarnings("unchecked")
Map<String, Object> args = arguments instanceof Map ? (Map<String, Object>) arguments : new HashMap<>();
return McpPromptResponse.PromptGetResponse.builder()
.name(name)
.messages(messages)
.arguments(args)
.build();
}
// 도구 생성 헬퍼 메서드들
private McpToolResponse createMemoryStoreTool() {
Map<String, Object> inputSchema = new HashMap<>();
inputSchema.put("type", "object");
inputSchema.put("properties", Map.of(
"memory_text", Map.of("type", "string", "description", "저장할 메모리 텍스트"),
"user_id", Map.of("type", "string", "description", "사용자 ID"),
"importance", Map.of("type", "integer", "description", "중요도 (1-5)", "minimum", 1, "maximum", 5)
));
inputSchema.put("required", Arrays.asList("memory_text", "user_id"));
return McpToolResponse.builder()
.name("store_memory")
.description("메모리를 Redis에 임시 저장합니다")
.inputSchema(inputSchema)
.build();
}
private McpToolResponse createMemoryRetrieveTool() {
Map<String, Object> inputSchema = new HashMap<>();
inputSchema.put("type", "object");
inputSchema.put("properties", Map.of(
"user_id", Map.of("type", "string", "description", "사용자 ID"),
"limit", Map.of("type", "integer", "description", "조회할 메모리 개수", "default", 10)
));
inputSchema.put("required", Arrays.asList("user_id"));
return McpToolResponse.builder()
.name("retrieve_memory")
.description("사용자의 메모리를 조회합니다")
.inputSchema(inputSchema)
.build();
}
private McpToolResponse createMemorySearchTool() {
Map<String, Object> inputSchema = new HashMap<>();
inputSchema.put("type", "object");
inputSchema.put("properties", Map.of(
"query", Map.of("type", "string", "description", "검색 쿼리"),
"user_id", Map.of("type", "string", "description", "사용자 ID (선택)"),
"limit", Map.of("type", "integer", "description", "검색 결과 개수", "default", 5)
));
inputSchema.put("required", Arrays.asList("query"));
return McpToolResponse.builder()
.name("search_memory")
.description("벡터 유사도 기반으로 메모리를 검색합니다")
.inputSchema(inputSchema)
.build();
}
// 프롬프트 생성 헬퍼 메서드들
private McpPromptResponse createMemorySummaryPrompt() {
List<McpPromptResponse.PromptArgument> arguments = new ArrayList<>();
arguments.add(McpPromptResponse.PromptArgument.builder()
.name("memories")
.description("요약할 메모리 텍스트 목록")
.required(true)
.build());
return McpPromptResponse.builder()
.name("memory_summary")
.description("메모리 목록을 요약하는 프롬프트")
.arguments(arguments)
.build();
}
private McpPromptResponse createMemorySearchPrompt() {
List<McpPromptResponse.PromptArgument> arguments = new ArrayList<>();
arguments.add(McpPromptResponse.PromptArgument.builder()
.name("query")
.description("검색할 키워드")
.required(true)
.build());
return McpPromptResponse.builder()
.name("memory_search")
.description("메모리 검색을 위한 프롬프트")
.arguments(arguments)
.build();
}
}

View File

@ -0,0 +1,26 @@
package com.pandol365.dewey.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* MCP 서버 관련 설정
*/
@Configuration
public class McpConfig implements WebMvcConfigurer {
/**
* CORS 설정
* MCP 클라이언트가 다른 도메인에서 접근할 있도록 허용
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/mcp/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}

View File

@ -0,0 +1,50 @@
package com.pandol365.dewey.domain.memory.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 장기기억 엔티티 (Model)
* PostgreSQL에 영구 저장되는 정제된 메모리 데이터
*/
@Entity
@Table(name = "memories")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Memory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String userId;
@Column(nullable = false, columnDefinition = "TEXT")
private String memoryText;
@Column(nullable = false)
@Builder.Default
private Integer importance = 1; // 1~5
@Column(columnDefinition = "JSONB")
private String tags; // JSON 형식의 태그 배열
@Column(name = "created_at", nullable = false, updatable = false)
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 벡터 임베딩은 별도 테이블 또는 pgvector 컬럼으로 관리
// Spring AI의 PgVectorStore를 사용할 경우 별도 처리
}

View File

@ -0,0 +1,33 @@
package com.pandol365.dewey.domain.memory.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 임시 메모리 모델 (Model)
* Redis에 저장되는 임시 메모리 데이터를 표현
* 엔티티가 아닌 도메인 모델
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemporaryMemory {
private String id; // Redis key
private String userId;
private String memoryText;
private Integer importance;
private LocalDateTime createdAt;
private LocalDateTime expiresAt; // TTL 기반 만료 시간
}

View File

@ -0,0 +1,35 @@
package com.pandol365.dewey.domain.memory.repository;
import com.pandol365.dewey.domain.memory.model.Memory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Memory 엔티티에 대한 데이터 접근 계층 (Repository)
*/
@Repository
public interface MemoryRepository extends JpaRepository<Memory, Long> {
/**
* 사용자 ID로 메모리 조회
*/
List<Memory> findByUserIdOrderByCreatedAtDesc(String userId);
/**
* 사용자 ID와 중요도로 메모리 조회
*/
List<Memory> findByUserIdAndImportanceGreaterThanEqualOrderByCreatedAtDesc(
String userId, Integer minImportance);
/**
* 사용자 ID로 최근 N개 메모리 조회
*/
@Query("SELECT m FROM Memory m WHERE m.userId = :userId ORDER BY m.createdAt DESC")
List<Memory> findRecentMemoriesByUserId(@Param("userId") String userId,
org.springframework.data.domain.Pageable pageable);
}

View File

@ -0,0 +1,43 @@
package com.pandol365.dewey.domain.memory.service;
import com.pandol365.dewey.domain.memory.model.Memory;
import com.pandol365.dewey.domain.memory.model.TemporaryMemory;
import java.util.List;
/**
* 메모리 도메인 비즈니스 로직 인터페이스 (Service)
*/
public interface MemoryService {
/**
* 임시 메모리를 Redis에 저장
*/
TemporaryMemory storeTemporaryMemory(String userId, String memoryText, Integer importance);
/**
* 사용자의 임시 메모리 조회
*/
List<TemporaryMemory> getTemporaryMemories(String userId);
/**
* 임시 메모리를 영구 메모리로 저장 (PostgreSQL)
*/
Memory savePermanentMemory(Memory memory);
/**
* 사용자의 영구 메모리 조회
*/
List<Memory> getPermanentMemories(String userId, Integer limit);
/**
* 벡터 유사도 기반 메모리 검색
*/
List<Memory> searchMemoriesByVector(String query, String userId, Integer limit);
/**
* 키워드 기반 메모리 검색
*/
List<Memory> searchMemoriesByKeyword(String keyword, String userId, Integer limit);
}

View File

@ -0,0 +1,93 @@
package com.pandol365.dewey.domain.memory.service.impl;
import com.pandol365.dewey.domain.memory.model.Memory;
import com.pandol365.dewey.domain.memory.model.TemporaryMemory;
import com.pandol365.dewey.domain.memory.repository.MemoryRepository;
import com.pandol365.dewey.domain.memory.service.MemoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 메모리 도메인 비즈니스 로직 구현체 (Service)
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class MemoryServiceImpl implements MemoryService {
private final MemoryRepository memoryRepository;
// TODO: Redis 연동 추가 필요
// private final ReactiveRedisTemplate<String, TemporaryMemory> redisTemplate;
@Override
public TemporaryMemory storeTemporaryMemory(String userId, String memoryText, Integer importance) {
log.info("임시 메모리 저장: userId={}, importance={}", userId, importance);
// TODO: Redis에 저장하는 로직 구현
TemporaryMemory temporaryMemory = TemporaryMemory.builder()
.id(UUID.randomUUID().toString())
.userId(userId)
.memoryText(memoryText)
.importance(importance != null ? importance : 1)
.createdAt(LocalDateTime.now())
.expiresAt(LocalDateTime.now().plusDays(3)) // TTL 3일
.build();
// TODO: redisTemplate.opsForValue().set(key, temporaryMemory, Duration.ofDays(3));
return temporaryMemory;
}
@Override
@Transactional(readOnly = true)
public List<TemporaryMemory> getTemporaryMemories(String userId) {
log.info("임시 메모리 조회: userId={}", userId);
// TODO: Redis에서 조회하는 로직 구현
return List.of();
}
@Override
public Memory savePermanentMemory(Memory memory) {
log.info("영구 메모리 저장: userId={}, importance={}", memory.getUserId(), memory.getImportance());
memory.setUpdatedAt(LocalDateTime.now());
return memoryRepository.save(memory);
}
@Override
@Transactional(readOnly = true)
public List<Memory> getPermanentMemories(String userId, Integer limit) {
log.info("영구 메모리 조회: userId={}, limit={}", userId, limit);
PageRequest pageRequest = PageRequest.of(0, limit != null ? limit : 10);
return memoryRepository.findRecentMemoriesByUserId(userId, pageRequest);
}
@Override
@Transactional(readOnly = true)
public List<Memory> searchMemoriesByVector(String query, String userId, Integer limit) {
log.info("벡터 기반 메모리 검색: query={}, userId={}, limit={}", query, userId, limit);
// TODO: 벡터 유사도 검색 구현 (Spring AI PgVectorStore 사용)
return List.of();
}
@Override
@Transactional(readOnly = true)
public List<Memory> searchMemoriesByKeyword(String keyword, String userId, Integer limit) {
log.info("키워드 기반 메모리 검색: keyword={}, userId={}, limit={}", keyword, userId, limit);
// TODO: 키워드 검색 구현 (LIKE 또는 Full-Text Search)
return List.of();
}
}

View File

@ -0,0 +1,85 @@
package com.pandol365.dewey.exception;
import com.pandol365.dewey.api.dto.response.McpJsonRpcResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 글로벌 예외 처리 핸들러
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 유효성 검증 예외 처리
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
Map<String, Object> response = new HashMap<>();
response.put("errors", errors);
response.put("message", "Validation failed");
return ResponseEntity.badRequest().body(response);
}
/**
* IllegalArgumentException 처리
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<McpJsonRpcResponse> handleIllegalArgumentException(
IllegalArgumentException ex) {
log.error("Illegal argument exception: {}", ex.getMessage());
McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder()
.code(-32602) // Invalid params
.message(ex.getMessage())
.data(null)
.build();
McpJsonRpcResponse response = McpJsonRpcResponse.builder()
.jsonrpc("2.0")
.error(error)
.build();
return ResponseEntity.badRequest().body(response);
}
/**
* 일반 예외 처리
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<McpJsonRpcResponse> handleException(Exception ex) {
log.error("Unexpected exception: ", ex);
McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder()
.code(-32603) // Internal error
.message(ex.getMessage())
.data(null)
.build();
McpJsonRpcResponse response = McpJsonRpcResponse.builder()
.jsonrpc("2.0")
.error(error)
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

View File

@ -1 +1,17 @@
spring.application.name=dewey spring.application.name=dewey
# PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/dewey_memory
spring.datasource.username=dewey
spring.datasource.password=0bk1rWu98mGl5ea3
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
# Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379