# End-User API 레퍼런스

📘 이 문서는 Bkend에서 자동 생성된 REST API의 상세 스펙을 제공합니다.

## API 기본 정보

| 항목 | 값 |
| --- | --- |
| **Base URL** | `[https://api-enduser.bkend.ai](https://api-enduser.bkend.ai)` |
| **OpenAPI 스펙** | `GET /data/{tableName}/openapi` |
| **Content-Type** | `application/json` |

---

## 인증 API

### POST `/auth/signup/password` - 회원가입

**Request:**

```
{
  "email": "user@example.com",
  "password": "securePassword123",
  "name": "홍길동",
  "callbackUrl": "http://localhost:5173"
}
```

| 필드 | 타입 | 필수 | 설명 |
| --- | --- | --- | --- |
| `email` | string | O | 사용자 이메일 |
| `password` | string | O | 비밀번호 |
| `name` | string | O | 사용자 이름 |
| `callbackUrl` | string | O | 콜백 URL (현재 도메인) |

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}
```

⚠️ **주의:** `callbackUrl`을 누락하면 `auth/invalid-callback-url` 에러가 발생합니다.

---

### POST `/auth/signin/password` - 로그인

**Request:**

```
{
  "email": "user@example.com",
  "password": "securePassword123",
  "callbackUrl": "http://localhost:5173"
}
```

| 필드 | 타입 | 필수 | 설명 |
| --- | --- | --- | --- |
| `email` | string | O | 사용자 이메일 |
| `password` | string | O | 비밀번호 |
| `callbackUrl` | string | O | 콜백 URL (현재 도메인) |

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}
```

---

### GET `/auth/me` - 현재 사용자 정보

**Headers:** `Authorization: Bearer {access_token}` 필수

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "name": "홍길동",
    "role": "user"
  }
}
```

⚠️ **주의:** 사용자 ID 필드명은 `id`입니다 (`_id`가 아님).

---

## 데이터 CRUD API

### 필수 헤더

모든 데이터 API 요청에 다음 헤더가 **필수**입니다:

| 헤더 | 설명 | 예시 |
| --- | --- | --- |
| `Authorization` | 인증 토큰 | `Bearer eyJhbG...` |
| `X-Project-Id` | Bkend 프로젝트 ID | `d1w4s46fuc576cobaogc` |
| `X-Environment` | 환경 이름 | `dev`, `staging`, `prod` |
| `Content-Type` | 콘텐츠 타입 | `application/json` |

**curl 예시:**

```
curl -X GET "https://api-enduser.bkend.ai/data/todos" \
  -H "Authorization: Bearer {access_token}" \
  -H "X-Project-Id: {project_id}" \
  -H "X-Environment: dev" \
  -H "Content-Type: application/json"
```

---

### GET `/data/{tableName}` - 목록 조회

### Query Parameters

**페이지네이션:**

| 파라미터 | 타입 | 기본값 | 설명 |
| --- | --- | --- | --- |
| `page` | number | 1 | 페이지 번호 (1-based) |
| `limit` | number | 20 | 페이지당 항목 수 (최대 100) |

**필터링:**

| 파라미터 | 타입 | 설명 |
| --- | --- | --- |
| `andFilters` | object | AND 조건 필터. 모든 조건 충족 필요 |
| `orFilters` | object | OR 조건 필터. 하나 이상 조건 충족 |
| `search` | string | 검색어 (대소문자 무시, 부분 일치) |
| `searchType` | string | 검색 대상 필드명 (생략 시 전체 필드) |

**정렬:**

| 파라미터 | 타입 | 설명 |
| --- | --- | --- |
| `sortBy` | string | 정렬 기준 필드 (예: `createdAt`, `title`) |
| `sortDirection` | string | 정렬 방향: `asc` (오름차순), `desc` (내림차순) |

### 필터링 예시

```
# 현재 사용자의 데이터만 조회
GET /data/todos?andFilters={"createdBy":"550e8400-e29b-41d4-a716-446655440000"}

# 완료되지 않은 항목만 조회
GET /data/todos?andFilters={"completed":false}

# 우선순위가 high인 항목 조회
GET /data/todos?andFilters={"priority":"high"}

# 복합 조건: 완료되지 않은 high 우선순위 항목
GET /data/todos?andFilters={"completed":false,"priority":"high"}

# 날짜 범위 필터 (MongoDB 연산자 지원)
GET /data/todos?andFilters={"createdAt":{"$gte":"2025-01-01"}}

# 검색: 제목에 "회의" 포함
GET /data/todos?search=회의&searchType=title

# 정렬: 최신순
GET /data/todos?sortBy=createdAt&sortDirection=desc
```

### 지원하는 MongoDB 연산자

- `$gte`, `$gt`: 이상, 초과

- `$lte`, `$lt`: 이하, 미만

- `$in`: 배열 내 값 포함

- `$regex`: 정규표현식 매칭

### Response (200 OK)

```
{
  "success": true,
  "data": {
    "items": [
      {
        "_id": "507f1f77bcf86cd799439011",
        "title": "할 일 1",
        "completed": false,
        "category": "work",
        "priority": "high",
        "createdBy": "550e8400-e29b-41d4-a716-446655440000",
        "createdAt": "2025-11-29T10:00:00.000Z",
        "updatedAt": "2025-11-29T10:00:00.000Z"
      }
    ],
    "page": 1,
    "limit": 20,
    "total": 1
  }
}
```

**응답 구조 주의:**

- 실제 데이터 배열: `response.data.items`

- 페이지네이션 정보: `response.data.page`, `limit`, `total`

---

### GET `/data/{tableName}/:id` - 단건 조회

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439011",
    "title": "할 일 1",
    "completed": false,
    "category": "work",
    "priority": "high",
    "createdBy": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2025-11-29T10:00:00.000Z",
    "updatedAt": "2025-11-29T10:00:00.000Z"
  }
}
```

**Response (404 Not Found):**

```
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found"
  }
}
```

---

### POST `/data/{tableName}` - 생성

**Request:**

```
{
  "title": "새로운 할 일",
  "completed": false,
  "category": "personal",
  "priority": "medium",
  "description": "상세 설명",
  "dueDate": "2025-12-31"
}
```

**Response (201 Created):**

```
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439012",
    "title": "새로운 할 일",
    "completed": false,
    "category": "personal",
    "priority": "medium",
    "description": "상세 설명",
    "dueDate": "2025-12-31",
    "createdBy": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2025-11-29T12:00:00.000Z",
    "updatedAt": "2025-11-29T12:00:00.000Z"
  }
}
```

**자동 생성 필드:**

- `_id`: MongoDB ObjectId

- `createdBy`: 요청한 사용자 ID

- `createdAt`: 생성 시간

- `updatedAt`: 수정 시간

---

### PATCH `/data/{tableName}/:id` - 수정

> 💡 **부분 수정 가능:** 수정하고 싶은 필드만 보내면 됩니다.

**Request (예시 1 - 완료 상태만 변경):**

```
{
  "completed": true
}
```

**Request (예시 2 - 여러 필드 수정):**

```
{
  "title": "수정된 할 일",
  "priority": "high",
  "description": "수정된 설명"
}
```

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439012",
    "title": "수정된 할 일",
    "completed": true,
    "category": "personal",
    "priority": "high",
    "description": "수정된 설명",
    "dueDate": "2025-12-31",
    "createdBy": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2025-11-29T12:00:00.000Z",
    "updatedAt": "2025-11-29T14:00:00.000Z"
  }
}
```

**부분 수정 예시:**

```
// 완료 상태만 변경
const updated = await todosApi.update(id, {
  completed: true
});

// 여러 필드 한 번에 수정
const updated = await todosApi.update(id, {
  title: "수정된 제목",
  priority: "high"
});
```

---

### DELETE `/data/{tableName}/:id` - 삭제

**Response (200 OK):**

```
{
  "success": true,
  "data": {
    "deleted": true
  }
}
```

---

### GET `/data/{tableName}/openapi` - OpenAPI 스펙 조회

테이블의 OpenAPI 3.0 스펙을 JSON 형식으로 반환합니다.

---

## 공통 에러 응답

| HTTP 상태 | 에러 코드 | 설명 |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | 요청 데이터 유효성 검증 실패 |
| 401 | `UNAUTHORIZED` | 인증 토큰 없음 또는 만료 |
| 403 | `FORBIDDEN` | 권한 없음 |
| 404 | `NOT_FOUND` | 리소스를 찾을 수 없음 |
| 500 | `INTERNAL_ERROR` | 서버 내부 오류 |

**에러 응답 형식:**

```
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "title is required"
  }
}
```

---

## 프론트엔드 통합 코드

### API 클라이언트 설정

```
// src/api/client.ts
import axios from 'axios';

const API_BASE_URL = import.meta.env.DEV
  ? '/api'  // 개발: Vite 프록시 사용
  : import.meta.env.VITE_API_BASE_URL;

const apiClient = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
    'X-Project-Id': import.meta.env.VITE_PROJECT_ID,
    'X-Environment': import.meta.env.VITE_ENVIRONMENT,
  },
});

// 요청 인터셉터: 토큰 자동 추가
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 응답 인터셉터: 401 에러 처리
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('access_token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export { apiClient };
```

---

### 인증 API 래퍼

```
// src/api/auth.ts
import { apiClient } from './client';

interface AuthResponse {
  success: boolean;
  data: {
    access_token: string;
    refresh_token: string;
  };
}

interface MeResponse {
  success: boolean;
  data: {
    id: string;      // 주의: _id가 아닌 id
    email: string;
    name: string;
    role: string;
  };
}

export const authApi = {
  async signup(email: string, password: string, name: string): Promise<AuthResponse> {
    const response = await apiClient.post<AuthResponse>('/auth/signup/password', {
      email,
      password,
      name,
      callbackUrl: window.location.origin,  // 필수!
    });
    return response.data;
  },

  async signin(email: string, password: string): Promise<AuthResponse> {
    const response = await apiClient.post<AuthResponse>('/auth/signin/password', {
      email,
      password,
      callbackUrl: window.location.origin,  // 필수!
    });
    return response.data;
  },

  async me(): Promise<MeResponse> {
    const response = await apiClient.get<MeResponse>('/auth/me');
    return response.data;
  },
};
```

---

### 데이터 API 래퍼

```
// src/api/todos.ts
import { apiClient } from './client';
import type { Todo, TodoFormData } from '../types';

interface ListResponse {
  success: boolean;
  data: {
    items: Todo[];
    page: number;
    limit: number;
    total: number;
  };
}

interface SingleResponse {
  success: boolean;
  data: Todo;
}

export const todosApi = {
  // 목록 조회 (서버 사이드 필터링)
  async list(userId: string): Promise<Todo[]> {
    const filters = JSON.stringify({ createdBy: userId });
    const response = await apiClient.get<ListResponse>(
      `/data/todos?andFilters=${encodeURIComponent(filters)}`
    );
    return response.data.items || [];
  },

  // 단건 조회
  async getById(id: string): Promise<Todo> {
    const response = await apiClient.get<SingleResponse>(`/data/todos/${id}`);
    return response.data;
  },

  // 생성
  async create(data: TodoFormData): Promise<Todo> {
    const response = await apiClient.post<SingleResponse>('/data/todos', data);
    return response.data;
  },

  // 수정 (PATCH - 부분 수정 가능)
  async update(id: string, data: Partial<TodoFormData>): Promise<Todo> {
    // PATCH이므로 수정할 필드만 보내면 됨
    const response = await apiClient.patch<SingleResponse>(`/data/todos/${id}`, data);
    return response.data;
  },

  // 삭제
  async delete(id: string): Promise<void> {
    await apiClient.delete(`/data/todos/${id}`);
  },

  // 완료 토글 (update 활용)
  async toggle(id: string): Promise<Todo> {
    const todo = await this.getById(id);
    return this.update(id, { completed: !todo.completed });
  },
};
```

---

### 환경 변수 설정

```
# .env
VITE_API_BASE_URL=https://api-enduser.bkend.ai
VITE_PROJECT_ID=your-project-id
VITE_ENVIRONMENT=dev
```

---

### CORS 처리 (Vite)

```
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api-enduser.bkend.ai',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});
```

---

### 서버 사이드 필터링 예시

### 기본 필터링

```
// 현재 사용자의 데이터만 조회
async list(userId: string) {
  const filters = JSON.stringify({ createdBy: userId });
  const response = await apiClient.get(`/data/todos?andFilters=${encodeURIComponent(filters)}`);
  return response.data.items || [];
}
```

### 복합 조건 필터링

```
// 완료되지 않은 high 우선순위 항목만 조회
async listUrgent(userId: string) {
  const filters = JSON.stringify({
    createdBy: userId,
    completed: false,
    priority: "high"
  });
  const response = await apiClient.get(`/data/todos?andFilters=${encodeURIComponent(filters)}`);
  return response.data.items || [];
}
```

### 날짜 범위 필터링 (MongoDB 연산자)

```
// 이번 주에 생성된 항목 조회
async listThisWeek(userId: string) {
  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  const filters = JSON.stringify({
    createdBy: userId,
    createdAt: { "$gte": weekAgo.toISOString() }
  });
  const response = await apiClient.get(`/data/todos?andFilters=${encodeURIComponent(filters)}`);
  return response.data.items || [];
}
```

### 검색 + 필터링 + 정렬

```
// 제목에 "회의" 포함, 완료되지 않은 항목, 최신순
async searchTodos(userId: string, keyword: string) {
  const filters = JSON.stringify({ createdBy: userId, completed: false });
  const params = new URLSearchParams({
    andFilters: filters,
    search: keyword,
    searchType: "title",
    sortBy: "createdAt",
    sortDirection: "desc"
  });
  const response = await apiClient.get(`/data/todos?${params.toString()}`);
  return response.data.items || [];
}
```

---

## LLM을 위한 API 호출 체크리스트

API 호출 시 다음을 반드시 확인하세요:

- [ ] **Base URL**: `https://api-enduser.bkend.ai`

- [ ] **필수 헤더 3개**: `Authorization`, `X-Project-Id`, `X-Environment`

- [ ] **인증 요청 시**: `callbackUrl` 필드 포함 (누락 시 에러)

- [ ] **사용자 ID 참조 시**: `user.id` 사용 (`user._id` 아님)

- [ ] **목록 조회 응답**: `response.data.items`로 접근

- [ ] **단건 조회 응답**: `response.data`로 접근

- [ ] **수정 요청 시**: PATCH 사용, 수정할 필드만 포함

---

## 전체 코드 샘플

### React 예시

```
// src/hooks/useTodos.ts
import { useState, useEffect } from 'react';
import { todosApi } from '../api/todos';
import type { Todo, TodoFormData } from '../types';

export function useTodos(userId: string) {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    loadTodos();
  }, [userId]);

  async function loadTodos() {
    try {
      setLoading(true);
      const data = await todosApi.list(userId);
      setTodos(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to load todos');
    } finally {
      setLoading(false);
    }
  }

  async function createTodo(data: TodoFormData) {
    try {
      const newTodo = await todosApi.create(data);
      setTodos([...todos, newTodo]);
      return newTodo;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create todo');
      throw err;
    }
  }

  async function updateTodo(id: string, data: Partial<TodoFormData>) {
    try {
      const updated = await todosApi.update(id, data);
      setTodos(todos.map(t => t._id === id ? updated : t));
      return updated;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to update todo');
      throw err;
    }
  }

  async function deleteTodo(id: string) {
    try {
      await todosApi.delete(id);
      setTodos(todos.filter(t => t._id !== id));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to delete todo');
      throw err;
    }
  }

  async function toggleTodo(id: string) {
    try {
      const updated = await todosApi.toggle(id);
      setTodos(todos.map(t => t._id === id ? updated : t));
      return updated;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to toggle todo');
      throw err;
    }
  }

  return {
    todos,
    loading,
    error,
    createTodo,
    updateTodo,
    deleteTodo,
    toggleTodo,
    reload: loadTodos,
  };
}
```

---

### Vue 예시

```
// src/composables/useTodos.ts
import { ref, onMounted } from 'vue';
import { todosApi } from '../api/todos';
import type { Todo, TodoFormData } from '../types';

export function useTodos(userId: string) {
  const todos = ref<Todo[]>([]);
  const loading = ref(true);
  const error = ref<string | null>(null);

  onMounted(() => {
    loadTodos();
  });

  async function loadTodos() {
    try {
      loading.value = true;
      todos.value = await todosApi.list(userId);
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to load todos';
    } finally {
      loading.value = false;
    }
  }

  async function createTodo(data: TodoFormData) {
    try {
      const newTodo = await todosApi.create(data);
      todos.value.push(newTodo);
      return newTodo;
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to create todo';
      throw err;
    }
  }

  async function updateTodo(id: string, data: Partial<TodoFormData>) {
    try {
      const updated = await todosApi.update(id, data);
      const index = todos.value.findIndex(t => t._id === id);
      if (index !== -1) {
        todos.value[index] = updated;
      }
      return updated;
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to update todo';
      throw err;
    }
  }

  async function deleteTodo(id: string) {
    try {
      await todosApi.delete(id);
      todos.value = todos.value.filter(t => t._id !== id);
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to delete todo';
      throw err;
    }
  }

  async function toggleTodo(id: string) {
    try {
      const updated = await todosApi.toggle(id);
      const index = todos.value.findIndex(t => t._id === id);
      if (index !== -1) {
        todos.value[index] = updated;
      }
      return updated;
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to toggle todo';
      throw err;
    }
  }

  return {
    todos,
    loading,
    error,
    createTodo,
    updateTodo,
    deleteTodo,
    toggleTodo,
    reload: loadTodos,
  };
}
```

For the site tree, see the [root Markdown](https://slashpage.com/bkend.md).
