2026-01-18 18:54:27 +09:00
# API 명세
Base URL: `/api`
## 인증
### POST /auth/login
로그인 (JWT 토큰 발급)
2026-01-23 22:06:25 +09:00
**Rate Limit:** 1분당 5회 (IP 기준)
2026-01-21 12:57:25 +09:00
### GET /auth/verify
토큰 검증 및 사용자 정보 (인증 필요)
2026-01-18 18:54:27 +09:00
---
## 멤버
### GET /members
멤버 목록 조회
2026-01-25 20:39:39 +09:00
### GET /members/:name
2026-01-18 18:54:27 +09:00
멤버 상세 조회
2026-01-25 20:39:39 +09:00
**Parameters:**
- `name` - 멤버 이름 (한글 또는 영문, 대소문자 무관)
**예시:**
- `/members/박지원` - 한글명으로 조회
- `/members/jiwon` - 영문명으로 조회
2026-01-18 18:54:27 +09:00
---
## 앨범
### GET /albums
앨범 목록 조회
### GET /albums/:id
앨범 상세 조회
---
## 일정
### GET /schedules
일정 조회
**Query Parameters:**
2026-01-20 17:28:50 +09:00
- `year` , `month` - 월별 조회
- `startDate` - 시작 날짜 (YYYY-MM-DD), 다가오는 일정 조회
2026-01-18 18:54:27 +09:00
- `search` - 검색어 (Meilisearch 사용)
- `offset` , `limit` - 페이징
2026-01-20 17:28:50 +09:00
※ `search` , `startDate` , `year/month` 중 하나는 필수
2026-01-18 18:54:27 +09:00
**월별 조회 응답:**
```json
{
2026-01-24 10:11:02 +09:00
"schedules": [
{
"id": 123,
"title": "...",
"date": "2026-01-18",
"time": "19:00:00",
"category": { "id": 2, "name": "유튜브", "color": "#ff0033 " },
"source": {
"name": "fromis_9",
"url": "https://www.youtube.com/watch?v=VIDEO_ID"
},
"members": ["송하영"]
}
]
2026-01-18 18:54:27 +09:00
}
```
2026-01-25 20:39:39 +09:00
**특수 일정 ID 형식:**
- 생일: `birthday-{year}-{nameEn}` (예: `birthday-2026-jiwon` )
- 데뷔: `debut-{year}` (예: `debut-2018` )
- 주년: `anniversary-{year}` (예: `anniversary-2026` )
2026-01-24 10:11:02 +09:00
※ `time` : 시간이 없는 일정은 `null` , 00:00 시간은 `"00:00:00"` 으로 반환
```
2026-01-18 18:54:27 +09:00
2026-01-18 21:50:04 +09:00
**source 객체 (카테고리별):**
- YouTube (category_id=2): `{ name: "채널명", url: "https://www.youtube.com/..." }`
2026-01-19 12:32:04 +09:00
- X (category_id=3): `{ name: "", url: "https://x.com/realfromis_9/status/..." }` (name 빈 문자열)
2026-01-18 21:50:04 +09:00
- 기타 카테고리: source 없음
2026-01-20 17:28:50 +09:00
**다가오는 일정 응답 (startDate):**
```json
2026-01-24 10:11:02 +09:00
{
"schedules": [
{
"id": 123,
"title": "...",
"date": "2026-01-18",
"time": "19:00:00",
"category": { "id": 2, "name": "유튜브", "color": "#ff0033 " },
"source": { "name": "fromis_9", "url": "https://..." },
"members": ["송하영"]
}
]
}
2026-01-20 17:28:50 +09:00
```
2026-01-24 10:11:02 +09:00
※ 현재 활동 멤버 전원인 경우 `["프로미스나인"]` 반환 (탈퇴 멤버 제외)
※ `time` : 시간이 없는 일정은 `null` , 00:00 시간은 `"00:00:00"` 으로 반환
2026-01-20 17:28:50 +09:00
2026-01-18 18:54:27 +09:00
**검색 응답:**
```json
{
"schedules": [
{
"id": 123,
"title": "...",
2026-01-24 10:11:02 +09:00
"date": "2026-01-18",
"time": "19:00:00",
2026-01-18 18:54:27 +09:00
"category": { "id": 2, "name": "유튜브", "color": "#ff0033 " },
2026-01-18 21:50:04 +09:00
"source": { "name": "fromis_9", "url": "https://..." },
2026-01-18 18:54:27 +09:00
"members": ["송하영"],
"_rankingScore": 0.95
}
],
"total": 100,
"offset": 0,
"limit": 20,
"hasMore": true
}
```
2026-01-24 10:11:02 +09:00
※ `time` : 시간이 없는 일정은 `null` , 00:00 시간은 `"00:00:00"` 으로 반환
```
2026-01-18 18:54:27 +09:00
2026-01-19 12:49:29 +09:00
### GET /schedules/categories
카테고리 목록 조회
**응답:**
```json
[
{ "id": 1, "name": "기타", "color": "#gray ", "sort_order": 0 },
{ "id": 2, "name": "유튜브", "color": "#ff0033 ", "sort_order": 1 }
]
```
2026-01-18 18:54:27 +09:00
### GET /schedules/:id
일정 상세 조회
2026-01-19 12:49:29 +09:00
### DELETE /schedules/:id
일정 삭제 (인증 필요)
2026-01-18 18:54:27 +09:00
### POST /schedules/sync-search
Meilisearch 전체 동기화 (인증 필요)
---
## 추천 검색어
### GET /schedules/suggestions
추천 검색어 조회
**Query Parameters:**
- `q` - 검색어 (2자 이상)
- `limit` - 결과 개수 (기본 10)
**응답:**
```json
{
"suggestions": ["송하영", "송하영 직캠", "하영"]
}
```
2026-01-21 12:57:25 +09:00
### GET /schedules/suggestions/popular
인기 검색어 조회
**Query Parameters:**
- `limit` - 결과 개수 (기본 10)
**응답:**
```json
{
"queries": ["프로미스나인", "송하영", "이서연"]
}
```
### POST /schedules/suggestions/save
검색어 저장 (검색 실행 시 호출)
**Request Body:**
```json
{
"query": "검색어"
}
```
### GET /schedules/suggestions/dict
사용자 사전 조회 (인증 필요)
**응답:**
```json
{
"content": "프로미스나인\t프로미스나인\tNNP\n..."
}
```
### PUT /schedules/suggestions/dict
사용자 사전 저장 (인증 필요)
**Request Body:**
```json
{
"content": "프로미스나인\t프로미스나인\tNNP\n..."
}
```
2026-01-18 18:54:27 +09:00
---
2026-01-18 23:48:21 +09:00
## 관리자 - 봇 관리 (인증 필요)
2026-01-18 18:54:27 +09:00
2026-01-18 23:48:21 +09:00
### GET /admin/bots
봇 목록 조회
**응답:**
```json
[
{
"id": "youtube-fromis9",
"name": "fromis_9",
"type": "youtube",
"status": "running",
2026-01-23 22:06:25 +09:00
"last_check_at": "2026-01-18T19:30:00+09:00",
2026-01-18 23:48:21 +09:00
"last_added_count": 2,
2026-01-23 22:06:25 +09:00
"last_sync_duration": 1234,
2026-01-18 23:48:21 +09:00
"schedules_added": 150,
"check_interval": 2,
"error_message": null,
"enabled": true
2026-01-23 22:06:25 +09:00
},
{
"id": "meilisearch-sync",
"name": "Meilisearch 동기화",
"type": "meilisearch",
"status": "running",
"last_check_at": "2026-01-18T04:00:00+09:00",
"last_added_count": 500,
"last_sync_duration": 2500,
"schedules_added": 500,
"check_interval": 0,
"error_message": null,
"enabled": true,
"version": "1.6.0"
2026-01-18 23:48:21 +09:00
}
]
```
2026-01-23 22:06:25 +09:00
**필드 설명:**
- `last_check_at` : 마지막 동기화 시간 (KST, +09:00)
- `last_sync_duration` : 마지막 동기화 소요 시간 (ms)
- `version` : Meilisearch 버전 (meilisearch 타입만)
2026-01-18 23:48:21 +09:00
### POST /admin/bots/:id/start
봇 시작
### POST /admin/bots/:id/stop
봇 정지
### POST /admin/bots/:id/sync-all
전체 동기화 (모든 영상/트윗 수집)
**응답:**
```json
{
"success": true,
"addedCount": 25,
"total": 100
}
```
### GET /admin/bots/quota-warning
YouTube API 할당량 경고 조회
**응답:**
```json
{
"active": true,
"message": "YouTube API 할당량 초과",
2026-01-23 22:06:25 +09:00
"timestamp": "2026-01-18T19:00:00+09:00"
2026-01-18 23:48:21 +09:00
}
```
### DELETE /admin/bots/quota-warning
할당량 경고 해제
2026-01-18 18:54:27 +09:00
---
2026-02-08 09:23:45 +09:00
## 관리자 - YouTube 봇 (인증 필요)
### POST /admin/youtube-bots/lookup
채널 핸들로 채널 정보 조회
**Request Body:**
```json
{
"handle": "@studiofromis_9 "
}
```
**응답:**
```json
{
"channelId": "UCxxx",
"title": "채널명",
"thumbnailUrl": "https://...",
"bannerUrl": "https://..."
}
```
### GET /admin/youtube-bots
YouTube 봇 목록 조회
### GET /admin/youtube-bots/:id
YouTube 봇 상세 조회
### POST /admin/youtube-bots
YouTube 봇 추가
**Request Body:**
```json
{
"channel_id": "UCxxx",
"channel_handle": "@studiofromis_9 ",
"channel_name": "채널명",
"cron_interval": 2,
"title_filters": ["fromis_9", "프로미스나인"],
"default_member_ids": [1, 2],
"extract_members_from_desc": true,
"auto_schedule_config": {
"dayOfWeek": 4,
"time": "18:00:00",
"titleTemplate": "{channelName} {episode}화",
"deadlineDayOfWeek": 5
2026-04-22 20:56:54 +09:00
},
"weekly_schedule_config": {
"dayOfWeek": 3,
"startTime": "19:00",
"intervalSeconds": 30,
"durationMinutes": 30
2026-02-08 09:23:45 +09:00
}
}
```
2026-04-22 20:56:54 +09:00
**폴링 방식:**
- `cron_interval` (분): 상시 폴링. `weekly_schedule_config` 가 null이면 이 값 사용
- `weekly_schedule_config` : 지정 요일/시각에만 집중 폴링. 값이 있으면 `cron_interval` 은 무시(서버에서 null로 저장). 새 영상 1개 발견 시 즉시 종료(stopOnFound 기본), `durationMinutes` 초과 시에도 종료
2026-02-08 09:23:45 +09:00
### PUT /admin/youtube-bots/:id
YouTube 봇 수정
### DELETE /admin/youtube-bots/:id
YouTube 봇 삭제
---
## 관리자 - X 봇 (인증 필요)
### POST /admin/x-bots/lookup
X username으로 프로필 정보 조회 (Nitter 사용)
**Request Body:**
```json
{
"username": "realfromis_9"
}
```
**응답:**
```json
{
"username": "realfromis_9",
"displayName": "프로미스나인 (fromis_9)",
"avatarUrl": "https://..."
}
```
### GET /admin/x-bots
X 봇 목록 조회
2026-02-10 13:12:00 +09:00
**응답:** `XBot[]`
2026-02-08 09:23:45 +09:00
### GET /admin/x-bots/:id
X 봇 상세 조회
2026-02-10 13:12:00 +09:00
**응답:**
```json
{
"id": 1,
"username": "realfromis_9",
"display_name": "프로미스나인 (fromis_9)",
"avatar_url": "https://...",
"text_filters": ["fromis", "프로미스"],
"include_retweets": false,
"extract_youtube": true,
"cron_interval": 1,
"enabled": true
}
```
2026-02-08 09:23:45 +09:00
### POST /admin/x-bots
X 봇 추가
**Request Body:**
```json
{
"username": "realfromis_9",
"display_name": "프로미스나인 (fromis_9)",
"avatar_url": "https://...",
2026-02-10 13:12:00 +09:00
"text_filters": ["fromis"],
"include_retweets": false,
"extract_youtube": false,
2026-02-08 09:23:45 +09:00
"cron_interval": 1
}
```
2026-02-10 13:12:00 +09:00
| 필드 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| `username` | string | (필수) | X username (@ 없이) |
| `display_name` | string\|null | null | 표시 이름 |
| `avatar_url` | string\|null | null | 프로필 이미지 URL |
| `text_filters` | string[]\|null | null | 텍스트 필터 (하나라도 포함 시 추가, 비어있으면 모든 트윗) |
| `include_retweets` | boolean | false | 리트윗 포함 여부 |
| `extract_youtube` | boolean | false | 트윗 내 YouTube 링크 자동 추출하여 유튜브 일정 추가 |
| `cron_interval` | integer | 1 | 동기화 간격 (분) |
2026-02-08 09:23:45 +09:00
### PUT /admin/x-bots/:id
2026-02-10 13:12:00 +09:00
X 봇 수정 (부분 업데이트 가능)
2026-02-08 09:23:45 +09:00
### DELETE /admin/x-bots/:id
X 봇 삭제
---
2026-01-19 12:49:29 +09:00
## 관리자 - YouTube (인증 필요)
### GET /admin/youtube/video-info
YouTube 영상 정보 조회
**Query Parameters:**
- `url` - YouTube URL (watch, shorts, youtu.be 모두 지원)
**응답:**
```json
{
"videoId": "abc123",
"title": "영상 제목",
"channelId": "UCxxx",
"channelName": "채널명",
"date": "2026-01-19",
"time": "15:00:00",
"videoType": "video",
"videoUrl": "https://www.youtube.com/watch?v=abc123"
}
```
### POST /admin/youtube/schedule
YouTube 일정 저장
**Request Body:**
```json
{
"videoId": "abc123",
"title": "영상 제목",
"channelId": "UCxxx",
"channelName": "채널명",
"date": "2026-01-19",
"time": "15:00:00",
"videoType": "video"
}
```
2026-01-20 17:28:50 +09:00
### PUT /admin/youtube/schedule/:id
YouTube 일정 수정 (멤버, 영상 유형)
**Request Body:**
```json
{
"memberIds": [1, 2, 3],
"videoType": "video"
}
```
※ `videoType` : "video" 또는 "shorts"
2026-01-19 12:49:29 +09:00
---
2026-01-19 12:57:06 +09:00
## 관리자 - X (인증 필요)
### GET /admin/x/post-info
X 게시글 정보 조회 (Nitter 스크래핑)
**Query Parameters:**
- `postId` - 게시글 ID (필수)
- `username` - 사용자명 (기본: realfromis_9)
**응답:**
```json
{
"postId": "1234567890",
"username": "realfromis_9",
"text": "게시글 전체 내용",
"title": "첫 문단 (자동 추출)",
"imageUrls": ["https://pbs.twimg.com/media/..."],
"date": "2026-01-19",
"time": "15:00:00",
"postUrl": "https://x.com/realfromis_9/status/1234567890",
"profile": {
"displayName": "프로미스나인 (fromis_9)",
"avatarUrl": "https://..."
}
}
```
### POST /admin/x/schedule
X 일정 저장
**Request Body:**
```json
{
"postId": "1234567890",
"title": "게시글 제목",
"content": "게시글 내용",
"imageUrls": ["https://..."],
"date": "2026-01-19",
"time": "15:00:00"
}
```
---
2026-03-02 17:08:35 +09:00
## 관리자 - 활동 로그 (인증 필요)
### GET /admin/logs
활동 로그 목록 조회
**Query Parameters:**
- `page` - 페이지 번호 (기본 1)
- `limit` - 페이지당 개수 (기본 50, 최대 100)
- `category` - 카테고리 필터 (콤마 구분: album, schedule, member, bot, category, dict, concert, sync)
- `actor` - 행위자 필터 (admin 또는 bot)
- `search` - summary 텍스트 검색
- `from` - 시작 날짜 (YYYY-MM-DD)
- `to` - 종료 날짜 (YYYY-MM-DD)
**응답:**
```json
{
"logs": [
{
"id": 1,
"actor": "admin",
"action": "create",
"category": "album",
"target_type": "album",
"target_id": 12,
"summary": "앨범 생성: Unlock My World",
"details": null,
"created_at": "2026-03-02 14:30:00"
}
],
"total": 150,
"page": 1,
"limit": 50,
"totalPages": 3
}
```
**actor 값:**
- `"admin"` - 관리자 수동 작업
- `"youtube-{id}"` - YouTube 봇 (예: youtube-3)
- `"x-{id}"` - X 봇 (예: x-1)
**action 값:**
- `create` , `update` , `delete` , `upload` - CRUD 작업
- `start` , `stop` - 봇 시작/정지
- `sync_complete` - 봇 동기화 완료
- `error` - 봇 동기화 에러
---
2026-01-18 18:54:27 +09:00
## 헬스 체크
### GET /health
서버 상태 확인
---
## API 문서
### GET /docs
Scalar API Reference UI
### GET /docs/json
OpenAPI JSON 스펙