소셜 오브 복숭아.
← 목록으로
블로그·API·자동화·스크립트

API 토큰 기반 외부 게시글 작성

Ethan· 2026. 10. 27.· 6분 읽기· 조회 6· 댓글 0

배경과 목표

이 블로그의 글은 관리자 화면의 마크다운 에디터에서 작성한다. 다만 개발로그 초안은 로컬 저장소의 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 의 스크립트가 이미지를 먼저 업로드하고, 응답으로 받은 주소로 본문의 이미지 경로를 치환한 뒤 글을 생성한다.

작성자 PC, 블로그 API 서버, 저장소 사이의 요청 순서
publish_post.py 의 글 생성 순서 — 사전 확인, 이미지 업로드, 경로 치환, 글 생성

업로드 단계에서 하나라도 실패하면 글 생성 단계를 실행하지 않는다. 이미지가 빠진 채 게시되는 상태를 방지하기 위한 순서다.

API 토큰 발급과 인증

발급과 보관

토큰은 관리자 화면 /admin/tokens 에서 이름과 만료 기간을 지정하여 발급한다. 형식은 sop_pat_ 접두어와 무작위 문자열의 조합이며, 원문은 발급 직후 한 번만 표시된다.

서버는 원문이 아니라 SHA-256 해시만 DB 에 저장한다. 따라서 DB 가 유출되어도 토큰 원문은 복원되지 않으며, 분실 시에는 폐기하고 새로 발급한다.

주의

API 토큰은 발급한 관리자의 전체 권한으로 동작한다. 저장소에 커밋하지 않고, 로컬의 backend/.env 처럼 git 에서 제외된 파일에만 보관한다.

서버의 인증 분기

인증 계층은 Authorization: Bearer 값의 접두어로 토큰 종류를 구분한다. sop_pat_ 로 시작하면 해시를 대조하고, 그 외에는 기존 세션 JWT 검증을 수행한다. 글 작성 라우터와 서비스는 수정하지 않았다.

토큰 접두어로 인증을 분기한 뒤 변환, sanitize, 저장으로 이어지는 서버 처리 순서
서버의 POST /posts 처리 순서

토큰이 사용될 때마다 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   # 즉시 발행

입력 파일은 첫 줄 제목:, 둘째 줄 태그:, 빈 줄 뒤 본문 마크다운으로 구성한다. 본문에서 찾는 이미지는 ![설명](images/a.png) 와 <img src="..."> 의 상대경로이며, 같은 파일은 한 번만 업로드한다.

전송 전 사전 확인 항목
  • 제목: 과 태그 1개 이상 존재 여부
  • 본문의 이미지 파일 존재 여부
  • 확장자(jpg · jpeg · png · gif · webp)와 10MB 제한
  • http(s):// · /static/uploads/ 주소와 코드 블록 · 인라인 코드 내부는 업로드 대상에서 제외

사전 확인이 하나라도 실패하면 네트워크 요청을 보내지 않는다. 확인을 서버 응답에 맡기면 일부 이미지만 업로드된 상태로 중단될 수 있기 때문이다.

이 글 공유하기

관련 글

댓글

아직 댓글이 없습니다.

    댓글을 작성하려면 GitHub로 로그인하세요.