API v1 Documentation
현재 코드에 구현된 라우트와 요청/응답 형식을 기준으로 정리한 문서입니다.
핵심 규칙
공개 조회 API와 검색 API는 인증 없이 접근할 수 있습니다. 외부에서 글을 쓰려면 먼저 간단 로그인으로 Bearer 토큰을 발급받고, 그 토큰으로 쓰기/수정/삭제 API를 호출하면 됩니다.
- 권장 인증 흐름은
POST /login 으로 Bearer 토큰 발급, GET /token/validate 로 상태 확인, POST /token/reissue 로 새 토큰 재발급입니다.
- 장기 운영용 외부 연동은
POST /tokens 로 별도 API 토큰을 발급해서 사용하는 방식을 권장합니다.
- 공개 카테고리/검색 API는
status = published 이고 is_hidden = false 인 글만 반환합니다.
GET /posts, GET /posts/{slug}, GET /random 은 현재 구현상 is_hidden = false 만 확인합니다.
- 포스트 생성 시 문서용
excerpt, is_published 는 사용되지 않습니다. 상태 전환은 수정 API의 status 필드로 처리합니다.
- 포스트 수정은
/posts/{id} 와 /posts/slug/{slug} 두 방식이 모두 제공됩니다.
권장 인증 흐름
POST/login
외부 API 작성용 간단 로그인입니다. email, password 만으로 Passport Bearer 토큰을 발급합니다.
| 필드 | 설명 |
email | 사용자 이메일 |
password | 사용자 비밀번호 |
token_name | 선택. 발급할 토큰 이름 |
POST/tokens
현재 유효한 Bearer 토큰으로 새 API 전용 토큰을 발급합니다. 실제 외부 연동에는 이 방식을 권장합니다.
| 필드 | 설명 |
token_name | 선택. 새 토큰 이름 |
GET/token/validate
현재 Bearer 토큰이 유효하면 토큰 메타데이터와 사용자 정보를 반환합니다. 유효하지 않거나 만료되었으면 401 이 반환됩니다.
POST/token/reissue
현재 유효한 토큰을 폐기하고 새 Bearer 토큰을 다시 발급합니다. 외부 API 토큰 운영 중 교체가 필요할 때 사용하기 좋습니다.
| 필드 | 설명 |
token_name | 선택. 새 토큰 이름 |
GET/tokens
현재 로그인 사용자 소유의 API 토큰 목록을 조회합니다. 토큰 문자열 자체는 다시 반환하지 않고 관리용 메타데이터만 반환합니다.
DELETE/tokens/{id}
내 API 토큰 하나를 폐기합니다. 즉시 Bearer 인증에 사용할 수 없게 됩니다.
POST/auth/logout
현재 access token을 revoke 합니다.
GET/user
현재 로그인한 사용자 정보를 반환합니다.
curl -X POST "http://www.closetoya.com/api/v1/login" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "password123"
}'
curl -X POST "http://www.closetoya.com/api/v1/tokens" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"token_name": "external-writer-prod"
}'
curl -X POST "http://www.closetoya.com/api/v1/posts" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "외부 API로 쓴 글",
"content": "# 제목
본문",
"category_path": "api/external"
}'
고급 OAuth 흐름
여기부터는 직접 OAuth client를 운영하는 경우에만 필요합니다. 일반적인 외부 글쓰기 자동화는 위의 /login 흐름을 권장합니다.
POST/auth/login
이메일/비밀번호와 OAuth client 정보로 access token, refresh token을 발급합니다.
| 필드 | 설명 |
email | 사용자 이메일 |
password | 사용자 비밀번호 |
client_id | OAuth client ID |
client_secret | OAuth client secret |
grant_type | password 고정 |
POST/auth/refresh
OAuth refresh token으로 access token을 갱신합니다.
| 필드 | 설명 |
refresh_token | 기존 refresh token |
client_id | OAuth client ID |
client_secret | OAuth client secret |
grant_type | refresh_token 고정 |
POST/auth/register
새 사용자를 생성합니다. 생성 직후 is_approved = false 상태이며 관리자 승인 전에는 로그인할 수 없습니다.
curl -X POST "http://www.closetoya.com/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "password123",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"grant_type": "password"
}'
공개 포스트 조회
GET/posts
포스트 목록을 페이지네이션과 함께 반환합니다. 쿼리: per_page (기본 20)
GET/posts/{slug}
특정 포스트 전체 정보를 반환하고 조회수를 1 증가시킵니다.
GET/posts/{slug}/content
본문만 별도로 반환합니다. 응답 필드: content, content_type, last_modified
GET/random
숨김 처리되지 않은 글 중 하나를 랜덤 반환합니다.
포스트 쓰기
아래 엔드포인트는 모두 Bearer 토큰이 필요합니다.
POST/posts
새 포스트를 생성합니다.
| 필드 | 필수 | 설명 |
title | 예 | 포스트 제목 |
content | 예 | 마크다운 본문 |
slug | 아니오 | 미입력 시 제목 기반 자동 생성 |
category_path | 아니오 | 카테고리 경로 |
parent_slug | 아니오 | 상위 문서 slug |
PUT/posts/{id}
ID 기준 수정. 부분 업데이트가 가능하며 status 로 draft/published 전환도 할 수 있습니다.
| 필드 | 필수 | 설명 |
title | 아니오 | 변경할 포스트 제목 |
content | 아니오 | 변경할 마크다운 본문 |
slug | 아니오 | 변경할 slug. 다른 글과 중복될 수 없습니다. |
status | 아니오 | draft 또는 published. 발행으로 바꿀 때 published_at 이 비어 있으면 현재 시각으로 채웁니다. |
category_path | 아니오 | 카테고리 경로 |
PUT/posts/slug/{slug}
slug 기준 수정. 요청 형식은 /posts/{id} 와 같고, 경로 파라미터만 slug를 사용합니다.
| 필드 | 필수 | 설명 |
title | 아니오 | 변경할 포스트 제목 |
content | 아니오 | 변경할 마크다운 본문 |
slug | 아니오 | 변경할 slug. 다른 글과 중복될 수 없습니다. |
status | 아니오 | draft 또는 published |
category_path | 아니오 | 카테고리 경로 |
DELETE/posts/{id}
ID 기준 삭제. 작성자 또는 관리자만 삭제할 수 있으며, 현재 구현은 삭제 후 POST /posts/{id}/unhide 로 복구할 수 있는 흐름과 함께 사용됩니다.
| 항목 | 설명 |
| 인증 | Bearer 토큰 필요 |
| 권한 | 포스트 작성자 또는 관리자만 가능 |
| 성공 응답 | { "message": "포스트가 삭제되었습니다." } |
| 실패 응답 | 포스트가 없으면 404 post_not_found, 권한이 없으면 403 unauthorized |
| 복구 | 삭제 후 복구가 필요하면 POST /posts/{id}/unhide 를 사용 |
POST/posts/{id}/unhide
soft delete 된 포스트를 복구하고 is_hidden 을 false 로 되돌립니다.
| 항목 | 설명 |
| 인증 | Bearer 토큰 필요 |
| 권한 | 포스트 작성자 또는 관리자만 가능 |
| 성공 응답 | message 와 함께 id, title, slug, is_hidden=false 를 반환 |
| 실패 응답 | 포스트가 없으면 404 post_not_found, 권한이 없으면 403 unauthorized |
| 용도 | DELETE /posts/{id} 로 숨겨진 포스트를 다시 공개 상태로 되돌릴 때 사용 |
POST/posts/{parentSlug}/sub
부모 문서 아래 하위 문서를 생성합니다. 구현상 excerpt validation은 남아있지만 저장/응답에는 사용하지 않습니다.
curl -X PUT "http://www.closetoya.com/api/v1/posts/123" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "수정된 제목",
"content": "# 수정된 본문
내용을 바꿉니다.",
"category_path": "dev/api",
"status": "published"
}'
curl -X PUT "http://www.closetoya.com/api/v1/posts/slug/existing-post-slug" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "draft"
}'
curl -X DELETE "http://www.closetoya.com/api/v1/posts/123" \
-H "Authorization: Bearer ACCESS_TOKEN"
curl -X POST "http://www.closetoya.com/api/v1/posts/123/unhide" \
-H "Authorization: Bearer ACCESS_TOKEN"
카테고리와 검색
GET/categories
공개 카테고리 목록을 반환합니다.
GET/categories/{path}
카테고리 상세와 포함된 포스트 목록을 반환합니다. 슬래시가 포함된 경로는 URL 인코딩이 필요할 수 있습니다.
GET/search
쿼리: q(필수), category, author, sort(relevance/date/views), per_page
GET/search/suggestions
q 길이가 2 미만이면 빈 배열을 반환합니다.
첨부파일과 고급 OAuth
POST/attachments
multipart/form-data 업로드. 필드: file, post_id, description
DELETE/attachments/{id}
업로더 또는 관리자만 삭제할 수 있습니다.
GET/oauth/clients
현재 로그인한 사용자가 소유한 고급 OAuth 연결 목록입니다.
POST/oauth/clients
현재 사용자 소유 고급 OAuth 연결을 생성합니다. 응답에서 secret은 생성 시점에만 반환됩니다.
GET/admin/clients
관리자 전용 전체 고급 OAuth 연결 목록
POST/admin/clients
관리자 전용 고급 OAuth 연결 생성
DELETE/admin/clients/{id}
관리자 전용 고급 OAuth 연결 revoke
응답 형식 예시
성공 예시
{
"message": "포스트가 생성되었습니다.",
"post": {
"id": 12,
"title": "새 글",
"slug": "새-글",
"category_path": "tech/api",
"parent_id": null,
"created_at": "2026-03-13T01:23:45.000000Z"
}
}
검증 오류 예시
{
"error": "validation_error",
"message": "입력 데이터가 올바르지 않습니다.",
"errors": {
"title": [
"The title field is required."
]
}
}