API 토큰 기반 외부 게시글 작성
배경과 목표
이 블로그의 글은 관리자 화면의 마크다운 에디터에서 작성한다. 다만 개발로그 초안은 로컬 저장소의 devlogs/ 폴더에서 먼저 작성하므로, 게시하려면 본문을 에디터에 붙여 넣고 이미지를 하나씩 다시 첨부해야 하였다.
목표는 로컬 마크다운 파일 하나로 이미지까지 포함된 게시글 하나를 생성하는 것이다. 글 작성 API(POST /api/v1/posts)와 이미지 업로드 API(POST /api/v1/uploads/image)는 이미 존재하였으므로, 남은 과제는 브라우저 밖에서 사용할 자격증명과 두 API 를 순서대로 호출하는 도구였다.
세션 토큰의 한계
관리자 로그인은 GitHub OAuth 로 처리하고, 로그인 결과는 수명이 하루인 세션 JWT 로 발급된다. 이 토큰은 외부 도구에 보관하기에 적합하지 않다.
| 항목 | 세션 JWT | API 토큰(PAT) |
|---|---|---|
| 발급 | GitHub 로그인 | 관리자 화면 /admin/tokens |
| 수명 | 1일 | 30일 · 90일 · 365일 · 무기한 중 선택 |
| 재발급 | 브라우저 로그인 필요 | 관리자 화면에서 새로 발급 |
| 폐기 | 만료까지 유효 | 삭제 즉시 무효 |
전체 구성
외부 게시는 세 단계로 구성된다. 작성자 PC 의 스크립트가 이미지를 먼저 업로드하고, 응답으로 받은 주소로 본문의 이미지 경로를 치환한 뒤 글을 생성한다.

업로드 단계에서 하나라도 실패하면 글 생성 단계를 실행하지 않는다. 이미지가 빠진 채 게시되는 상태를 방지하기 위한 순서다.
API 토큰 발급과 인증
발급과 보관
토큰은 관리자 화면 /admin/tokens 에서 이름과 만료 기간을 지정하여 발급한다. 형식은 sop_pat_ 접두어와 무작위 문자열의 조합이며, 원문은 발급 직후 한 번만 표시된다.
서버는 원문이 아니라 SHA-256 해시만 DB 에 저장한다. 따라서 DB 가 유출되어도 토큰 원문은 복원되지 않으며, 분실 시에는 폐기하고 새로 발급한다.
주의
API 토큰은 발급한 관리자의 전체 권한으로 동작한다. 저장소에 커밋하지 않고, 로컬의 backend/.env 처럼 git 에서 제외된 파일에만 보관한다.
서버의 인증 분기
인증 계층은 Authorization: Bearer 값의 접두어로 토큰 종류를 구분한다. sop_pat_ 로 시작하면 해시를 대조하고, 그 외에는 기존 세션 JWT 검증을 수행한다. 글 작성 라우터와 서비스는 수정하지 않았다.

토큰이 사용될 때마다 last_used_at 이 갱신되어 관리자 화면에 마지막 사용 시각이 표시된다. 기억에 없는 사용 기록이 확인되면 해당 토큰을 즉시 폐기한다.
서버 단일 변환
API 로 전송하는 본문은 마크다운 원문(content_source)이다. 변환된 HTML 을 함께 보내지 않으면 서버가 markdown-it 으로 변환하고 nh3 로 허용 태그만 남긴 뒤 저장한다.
관리자 에디터의 미리보기와 저장도 같은 변환기를 사용한다. 이에 따라 어느 경로로 게시하여도 저장 결과가 같다. 콜아웃 · 각주 · 캡션 이미지 등 확장 문법도 API 경로에서 동일하게 적용된다.
호출 절차
이미지 업로드
이미지는 jpg · jpeg · png · gif · webp 형식, 10MB 이하만 허용된다. 응답의 url 을 본문 이미지 경로로 사용한다.
curl -sS -X POST "$BASE/uploads/image" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./images/diagram.png"
# {"url": "/static/uploads/2026/10/27/....png"}
게시글 생성
status 를 draft 로 지정하면 초안으로 저장되어 관리자 화면에서 검토한 뒤 발행할 수 있다. published 로 지정하면 즉시 발행되고 사이트맵 · RSS 에 반영된다.
curl -sS -X POST "$BASE/posts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "제목", "content_source": "## 본문", "content_format": "markdown",
"tags": ["API"], "status": "draft"}'
게시 스크립트 publish_post.py
위의 두 호출을 하나로 묶은 도구가 scripts/publish_post.py 이다. 표준 라이브러리만 사용하므로 별도 패키지 설치가 필요 없고, API 주소와 토큰은 backend/.env 의 SOP_API_BASE · SOP_API_TOKEN 에서 읽는다.
backend\.venv\Scripts\python scripts\publish_post.py "devlogs\글.md" --dry-run # 확인만
backend\.venv\Scripts\python scripts\publish_post.py "devlogs\글.md" # 초안 생성
backend\.venv\Scripts\python scripts\publish_post.py "devlogs\글.md" --publish # 즉시 발행
입력 파일은 첫 줄 제목:, 둘째 줄 태그:, 빈 줄 뒤 본문 마크다운으로 구성한다. 본문에서 찾는 이미지는  와 <img src="..."> 의 상대경로이며, 같은 파일은 한 번만 업로드한다.
전송 전 사전 확인 항목
제목:과 태그 1개 이상 존재 여부- 본문의 이미지 파일 존재 여부
- 확장자(jpg · jpeg · png · gif · webp)와 10MB 제한
http(s)://·/static/uploads/주소와 코드 블록 · 인라인 코드 내부는 업로드 대상에서 제외
사전 확인이 하나라도 실패하면 네트워크 요청을 보내지 않는다. 확인을 서버 응답에 맡기면 일부 이미지만 업로드된 상태로 중단될 수 있기 때문이다.
관련 글
댓글
아직 댓글이 없습니다.
댓글을 작성하려면 GitHub로 로그인하세요.