
항목 | 값 |
Base URL | |
OpenAPI 스펙 | GET /data/{tableName}/openapi |
Content-Type | application/json |
{
"email": "user@example.com",
"password": "securePassword123",
"name": "홍길동",
"callbackUrl": "http://localhost:5173"
}필드 | 타입 | 필수 | 설명 |
email | string | O | 사용자 이메일 |
password | string | O | 비밀번호 |
name | string | O | 사용자 이름 |
callbackUrl | string | O | 콜백 URL (현재 도메인) |
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}{
"email": "user@example.com",
"password": "securePassword123",
"callbackUrl": "http://localhost:5173"
}필드 | 타입 | 필수 | 설명 |
email | string | O | 사용자 이메일 |
password | string | O | 비밀번호 |
callbackUrl | string | O | 콜백 URL (현재 도메인) |
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "홍길동",
"role": "user"
}
}헤더 | 설명 | 예시 |
Authorization | 인증 토큰 | Bearer eyJhbG... |
X-Project-Id | Bkend 프로젝트 ID | d1w4s46fuc576cobaogc |
X-Environment | 환경 이름 | dev, staging, prod |
Content-Type | 콘텐츠 타입 | application/json |
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"파라미터 | 타입 | 기본값 | 설명 |
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{
"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
}
}{
"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"
}
}{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Resource not found"
}
}{
"title": "새로운 할 일",
"completed": false,
"category": "personal",
"priority": "medium",
"description": "상세 설명",
"dueDate": "2025-12-31"
}{
"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"
}
}{
"completed": true
}{
"title": "수정된 할 일",
"priority": "high",
"description": "수정된 설명"
}{
"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"
});{
"success": true,
"data": {
"deleted": true
}
}HTTP 상태 | 에러 코드 | 설명 |
400 | VALIDATION_ERROR | 요청 데이터 유효성 검증 실패 |
401 | UNAUTHORIZED | 인증 토큰 없음 또는 만료 |
403 | FORBIDDEN | 권한 없음 |
404 | NOT_FOUND | 리소스를 찾을 수 없음 |
500 | INTERNAL_ERROR | 서버 내부 오류 |
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "title is required"
}
}// 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 };// 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;
},
};// 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// 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 || [];
}// 이번 주에 생성된 항목 조회
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 || [];
}// 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,
};
}// 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,
};
}