fromis_9/docs/api.md
caadiq 7c20e9bb17 feat(schedule): 행사 수정 폼 + 공개 상세 페이지 + 지도
- Admin: EventEditForm 추가 (기존 포스터 유지 + 신규 추가 조합), ScheduleItem 편집 경로에 '행사' 분기
- PC 공개 상세: EventSection 추가 - 포스터 Swiper 슬라이드 + 호버 화살표, 클릭 시 Lightbox, 카카오맵 + 마커 + 장소명 오버레이, 관련 링크는 중간점+primary 색상, max-w-5xl 및 text-2xl로 크기 확대
- Mobile 공개 상세: MobileEventSection 추가 (포스터/장소/지도/링크)
- KakaoMap 공용 컴포넌트 신규 (SDK 1회 로드 공유), VITE_KAKAO_JS_KEY 사용
- .gitignore: frontend/.env 제외
- routes/admin/events.js: PUT 핸들러의 addOrUpdateSchedule → syncScheduleById 정정
- 관련 문서(api/architecture/development) 업데이트

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:24:01 +09:00

14 KiB

API 명세

Base URL: /api

인증

POST /auth/login

로그인 (JWT 토큰 발급)

Rate Limit: 1분당 5회 (IP 기준)

GET /auth/verify

토큰 검증 및 사용자 정보 (인증 필요)


멤버

GET /members

멤버 목록 조회

GET /members/:name

멤버 상세 조회

Parameters:

  • name - 멤버 이름 (한글 또는 영문, 대소문자 무관)

예시:

  • /members/박지원 - 한글명으로 조회
  • /members/jiwon - 영문명으로 조회

앨범

GET /albums

앨범 목록 조회

GET /albums/:id

앨범 상세 조회


일정

GET /schedules

일정 조회

Query Parameters:

  • year, month - 월별 조회
  • startDate - 시작 날짜 (YYYY-MM-DD), 다가오는 일정 조회
  • search - 검색어 (Meilisearch 사용)
  • offset, limit - 페이징

search, startDate, year/month 중 하나는 필수

월별 조회 응답:

{
  "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": ["송하영"]
    }
  ]
}

특수 일정 ID 형식:

  • 생일: birthday-{year}-{nameEn} (예: birthday-2026-jiwon)
  • 데뷔: debut-{year} (예: debut-2018)
  • 주년: anniversary-{year} (예: anniversary-2026) ※ time: 시간이 없는 일정은 null, 00:00 시간은 "00:00:00"으로 반환

**source 객체 (카테고리별):**
- YouTube (category_id=2): `{ name: "채널명", url: "https://www.youtube.com/..." }`
- X (category_id=3): `{ name: "", url: "https://x.com/realfromis_9/status/..." }` (name 빈 문자열)
- 기타 카테고리: source 없음

**다가오는 일정 응답 (startDate):**
```json
{
  "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": ["송하영"]
    }
  ]
}

※ 현재 활동 멤버 전원인 경우 ["프로미스나인"] 반환 (탈퇴 멤버 제외) ※ time: 시간이 없는 일정은 null, 00:00 시간은 "00:00: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": ["송하영"],
      "_rankingScore": 0.95
    }
  ],
  "total": 100,
  "offset": 0,
  "limit": 20,
  "hasMore": true
}

time: 시간이 없는 일정은 null, 00:00 시간은 "00:00:00"으로 반환


### GET /schedules/categories
카테고리 목록 조회

**응답:**
```json
[
  { "id": 1, "name": "기타", "color": "#gray", "sort_order": 0 },
  { "id": 2, "name": "유튜브", "color": "#ff0033", "sort_order": 1 }
]

GET /schedules/:id

일정 상세 조회

DELETE /schedules/:id

일정 삭제 (인증 필요)

POST /schedules/sync-search

Meilisearch 전체 동기화 (인증 필요)


추천 검색어

GET /schedules/suggestions

추천 검색어 조회

Query Parameters:

  • q - 검색어 (2자 이상)
  • limit - 결과 개수 (기본 10)

응답:

{
  "suggestions": ["송하영", "송하영 직캠", "하영"]
}

인기 검색어 조회

Query Parameters:

  • limit - 결과 개수 (기본 10)

응답:

{
  "queries": ["프로미스나인", "송하영", "이서연"]
}

POST /schedules/suggestions/save

검색어 저장 (검색 실행 시 호출)

Request Body:

{
  "query": "검색어"
}

GET /schedules/suggestions/dict

사용자 사전 조회 (인증 필요)

응답:

{
  "content": "프로미스나인\t프로미스나인\tNNP\n..."
}

PUT /schedules/suggestions/dict

사용자 사전 저장 (인증 필요)

Request Body:

{
  "content": "프로미스나인\t프로미스나인\tNNP\n..."
}

관리자 - 봇 관리 (인증 필요)

GET /admin/bots

봇 목록 조회

응답:

[
  {
    "id": "youtube-fromis9",
    "name": "fromis_9",
    "type": "youtube",
    "status": "running",
    "last_check_at": "2026-01-18T19:30:00+09:00",
    "last_added_count": 2,
    "last_sync_duration": 1234,
    "schedules_added": 150,
    "check_interval": 2,
    "error_message": null,
    "enabled": true
  },
  {
    "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"
  }
]

필드 설명:

  • last_check_at: 마지막 동기화 시간 (KST, +09:00)
  • last_sync_duration: 마지막 동기화 소요 시간 (ms)
  • version: Meilisearch 버전 (meilisearch 타입만)

POST /admin/bots/:id/start

봇 시작

POST /admin/bots/:id/stop

봇 정지

POST /admin/bots/:id/sync-all

전체 동기화 (모든 영상/트윗 수집)

응답:

{
  "success": true,
  "addedCount": 25,
  "total": 100
}

GET /admin/bots/quota-warning

YouTube API 할당량 경고 조회

응답:

{
  "active": true,
  "message": "YouTube API 할당량 초과",
  "timestamp": "2026-01-18T19:00:00+09:00"
}

DELETE /admin/bots/quota-warning

할당량 경고 해제


관리자 - YouTube 봇 (인증 필요)

POST /admin/youtube-bots/lookup

채널 핸들로 채널 정보 조회

Request Body:

{
  "handle": "@studiofromis_9"
}

응답:

{
  "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:

{
  "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
  },
  "weekly_schedule_config": {
    "dayOfWeek": 3,
    "startTime": "19:00",
    "intervalSeconds": 30,
    "durationMinutes": 30
  }
}

폴링 방식:

  • cron_interval (분): 상시 폴링. weekly_schedule_config가 null이면 이 값 사용
  • weekly_schedule_config: 지정 요일/시각에만 집중 폴링. 값이 있으면 cron_interval은 무시(서버에서 null로 저장). 새 영상 1개 발견 시 즉시 종료(stopOnFound 기본), durationMinutes 초과 시에도 종료

PUT /admin/youtube-bots/:id

YouTube 봇 수정

DELETE /admin/youtube-bots/:id

YouTube 봇 삭제


관리자 - X 봇 (인증 필요)

POST /admin/x-bots/lookup

X username으로 프로필 정보 조회 (Nitter 사용)

Request Body:

{
  "username": "realfromis_9"
}

응답:

{
  "username": "realfromis_9",
  "displayName": "프로미스나인 (fromis_9)",
  "avatarUrl": "https://..."
}

GET /admin/x-bots

X 봇 목록 조회

응답: XBot[]

GET /admin/x-bots/:id

X 봇 상세 조회

응답:

{
  "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
}

POST /admin/x-bots

X 봇 추가

Request Body:

{
  "username": "realfromis_9",
  "display_name": "프로미스나인 (fromis_9)",
  "avatar_url": "https://...",
  "text_filters": ["fromis"],
  "include_retweets": false,
  "extract_youtube": false,
  "cron_interval": 1
}
필드 타입 기본값 설명
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 동기화 간격 (분)

PUT /admin/x-bots/:id

X 봇 수정 (부분 업데이트 가능)

DELETE /admin/x-bots/:id

X 봇 삭제


관리자 - YouTube (인증 필요)

GET /admin/youtube/video-info

YouTube 영상 정보 조회

Query Parameters:

  • url - YouTube URL (watch, shorts, youtu.be 모두 지원)

응답:

{
  "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:

{
  "videoId": "abc123",
  "title": "영상 제목",
  "channelId": "UCxxx",
  "channelName": "채널명",
  "date": "2026-01-19",
  "time": "15:00:00",
  "videoType": "video"
}

PUT /admin/youtube/schedule/:id

YouTube 일정 수정 (멤버, 영상 유형)

Request Body:

{
  "memberIds": [1, 2, 3],
  "videoType": "video"
}

videoType: "video" 또는 "shorts"


관리자 - X (인증 필요)

GET /admin/x/post-info

X 게시글 정보 조회 (Nitter 스크래핑)

Query Parameters:

  • postId - 게시글 ID (필수)
  • username - 사용자명 (기본: realfromis_9)

응답:

{
  "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:

{
  "postId": "1234567890",
  "title": "게시글 제목",
  "content": "게시글 내용",
  "imageUrls": ["https://..."],
  "date": "2026-01-19",
  "time": "15:00:00"
}

관리자 - 행사 (인증 필요)

GET /admin/events/:id

행사 상세 조회 (수정 폼용)

응답:

{
  "id": 2565,
  "title": "2026 UNION : PAINT THE UNION🎨",
  "date": "2026-05-07",
  "time": "21:30",
  "subtype": "university",
  "schoolName": "인천대학교",
  "memberIds": [1, 2, 3, 4, 5],
  "venue": {
    "id": 1,
    "name": "인천대학교",
    "address": "...",
    "roadAddress": "...",
    "lat": 37.xxx,
    "lng": 126.xxx,
    "kakao_id": null
  },
  "postUrls": ["https://www.instagram.com/p/..."],
  "posters": [
    { "id": 10001, "originalUrl": "...", "mediumUrl": "...", "thumbUrl": "..." }
  ]
}

POST /admin/events

행사 생성 (multipart/form-data)

multipart 파트:

  • payload (JSON string): { subtype, title, schoolName, date, time, memberIds, venue, postUrls }
    • subtype: 현재 'university'만 지원
    • venue: { name, address, roadAddress?, lat, lng, kakao_id? } — kakao_id 기준으로 event_venues 테이블에 upsert
    • title, schoolName, date, venue 필수
  • posters (파일, 0개 이상): 포스터 이미지. 여러 장 가능

응답: { "id": 2565 }

PUT /admin/events/:id

행사 수정 (multipart/form-data)

multipart 파트:

  • payload (JSON string): 위 POST 필드 + keepPosterIds: number[] (유지할 기존 포스터 ID 순서대로)
  • posters (파일, 0개 이상): 새로 추가할 포스터

서버는 keepPosterIds 다음에 새 파일 id들을 이어붙여 poster_image_ids 업데이트.

DELETE /admin/events/:id

행사 삭제 (schedules CASCADE로 schedule_event도 정리)


관리자 - 활동 로그 (인증 필요)

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)

응답:

{
  "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 - 봇 동기화 에러

헬스 체크

GET /health

서버 상태 확인


API 문서

GET /docs

Scalar API Reference UI

GET /docs/json

OpenAPI JSON 스펙