Rust + Poem + SQLite로 만든 개인용 CMS API 서버입니다. 블로그 콘텐츠를 관리하고, 기간 총예산/지출을 추적하기 위해 만든 백엔드이며 현재는 단일 운영자(개인) 사용 시나리오를 중심으로 구성되어 있습니다.
이 API는 크게 두 가지를 해결하기 위해 만들어졌습니다.
- 블로그 운영: 포스트/태그/이미지/포트폴리오 데이터를 CMS 형태로 관리
- 소비 추적: 기간 총예산 설정, 지출 기록, 예산 요약 및 기간별 계산
즉, "공용 SaaS형 CMS"보다는 "개인 콘텐츠 + 개인 소비 관리"에 최적화된 구조입니다.
- Rust (Edition 2021)
- Poem
- SQLx (SQLite)
- Tokio
- JWT (
jsonwebtoken) - bcrypt
프로젝트 루트 .env에 아래 값을 설정하세요.
# 상대 경로 예시
DATABASE_PATH=./data/database.db
UPLOAD_PATH=.uploads/images
# JWT
JWT_ACCESS_SECRET=replace-with-access-secret
JWT_REFRESH_SECRET=replace-with-refresh-secret
# Google Login
GOOGLE_CLIENT_ID=replace-with-google-oauth-client-id
# Web Push (RSS 기반 브라우저 푸시)
VAPID_PUBLIC_KEY=replace-with-vapid-public-key
VAPID_PRIVATE_KEY=replace-with-vapid-private-key
VAPID_SUBJECT=mailto:you@example.com
DATABASE_PATH, UPLOAD_PATH는 절대 경로로 직접 지정해도 됩니다.
# 절대 경로 예시
DATABASE_PATH=/Users/yourname/data/tyange/database.db
UPLOAD_PATH=/Users/yourname/data/tyange/uploads/imagesRSS 기반 브라우저 푸시를 사용하려면 아래 환경변수를 함께 설정해야 합니다.
VAPID_PUBLIC_KEY:GET /push/public-key가 대시보드에 공개하는 브라우저 등록용 공개키VAPID_PRIVATE_KEY: 실제 Web Push 발송 시 VAPID 서명을 만드는 비공개키VAPID_SUBJECT: Web Push VAPIDsubclaim에 넣는 연락처. 일반적으로mailto:...또는 HTTPS URL
동작 방식은 다음과 같습니다.
VAPID_PUBLIC_KEY가 없거나 빈 문자열이면GET /push/public-key는503 Service Unavailable을 반환합니다.VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT가 모두 있어야 RSS polling worker가 실제 브라우저 푸시를 전송할 수 있습니다.
로컬 개발 예시는 아래와 같습니다.
VAPID_PUBLIC_KEY=your-generated-public-key
VAPID_PRIVATE_KEY=your-generated-private-key
VAPID_SUBJECT=mailto:dev@example.comcargo run -q기본 바인드 주소는 0.0.0.0:8080 입니다.
Authorization 헤더에 JWT 액세스 토큰을 넣어 호출합니다.
관리자/작성자 권한이 필요한 CMS 수정 계열 API와 일부 Budget 설정 API에서 사용됩니다.
API Key는 유저별로 여러 개 발급할 수 있고, 원문은 발급 시 1회만 반환됩니다.
DB에는 bcrypt 해시만 저장하며 name, created_at, last_used_at, revoked_at를 관리합니다.
인증 규칙은 다음과 같습니다.
- 대부분의 관리/조회 API: JWT(
Authorization) 사용 POST /budget/spending: JWT 또는X-API-Key둘 다 허용- API Key 관리 API(
POST /api-keys,GET /api-keys,DELETE /api-keys/:id): JWT 필요
POST /budget/spending은 request body의 user_id를 신뢰하지 않고, 항상 인증 컨텍스트의 user_id를 사용합니다.
-
GET /health서버 생존 확인용 헬스체크. -
GET /health-check서버 생존 확인용 대체 헬스체크. -
OPTIONS /*pathCORS preflight 처리.
-
POST /login사용자 로그인 후 access/refresh 토큰 발급. -
POST /login/google프론트엔드가 Google Sign-In 후 받은id_token을 전달하면, 서버가 토큰을 검증한 뒤 access/refresh 토큰을 발급합니다. 동일 이메일의 기존 로컬 계정이 있으면 해당 계정에 Google 로그인을 연결합니다. -
POST /admin/add-user(JWT) 신규 사용자 계정 추가(비밀번호 해시 저장). -
POST /api-keys(JWT) 현재 로그인한 유저용 API Key 발급. 원문 API key는 이 응답에서만 반환. -
GET /api-keys(JWT) 현재 로그인한 유저가 발급한 API key 목록 조회. -
DELETE /api-keys/:id(JWT) 현재 로그인한 유저의 API key 폐기(revoke).
-
GET /posts공개용 포스트 목록 조회(초안 제외), 작성자 필터 지원. -
GET /posts/search-with-tags포함/제외 태그 조건으로 포스트 검색. -
GET /post/:post_id단일 포스트 상세 조회. -
POST /post/upload(JWT) 새 포스트 작성 및 태그 연결. 공개 상태(status != draft)이면서dev태그가 없으면 커밋 직후tyange-blogrebuild trigger를 보낸다. -
PUT /post/update/:post_id(JWT) 본인 포스트 내용/태그 수정. blog 대상 포스트 판정은status != draft이고dev태그가 없는 경우다. 이 기준으로 draft에서 공개로 전환되거나, 공개 필드가 바뀌거나,dev태그 추가/삭제 때문에 blog 포함 여부가 바뀌면 커밋 직후tyange-blogrebuild trigger를 보낸다. -
DELETE /post/delete/:post_id(JWT) 본인 포스트 삭제. 삭제 전 blog 대상 포스트였던 경우만 커밋 직후tyange-blogrebuild trigger를 보낸다. -
GET /admin/posts(JWT) 관리자용 전체 포스트 목록 조회(초안 포함). -
GET /tags태그별 사용 횟수 조회(카테고리 필터 가능). -
GET /tags-with-category카테고리별 태그 묶음 조회.
-
POST /upload-image(JWT) 이미지 업로드 후 웹 경로(/images/...) 반환. -
GET /portfolio포트폴리오 콘텐츠 조회. -
PUT /portfolio/update(JWT) 포트폴리오 콘텐츠 수정.
-
GET /budget(JWT) 현재 활성 기간 예산 요약 조회. 응답 필드:budget_id,total_budget,from_date,to_date,total_spent,remaining_budget,usage_rate,alert,alert_threshold,is_overspent -
PUT /budget(JWT) 현재 활성 기간 예산의 총액과alert_threshold를 다시 설정한다. 기간 필드(from_date,to_date)는 수정할 수 없다. -
POST /budget/plan(JWT) 기간 총예산을 생성한다. -
GET /budget/spending현재 활성 예산 기간의 소비 기록을 조회하고, 응답에서만 ISO week 기준으로 그룹핑한다. -
POST /budget/spending(JWT or API Key) 소비 기록 생성 및 기간 누적/남은 예산 계산.transacted_at는 현재 활성 예산 기간 안에 있어야 합니다. -
DELETE /budget/spending(JWT) 현재 로그인 사용자의 소비 기록을 모두 삭제한다. 예산 기간과 주간 설정은 유지된다. -
POST /budget/spending/import-preview(JWT) 신한카드 XLS 파일을 업로드해 미리보기 결과를 반환한다. 응답에는summary,rows가 포함되며 각 row는fingerprint,transacted_at,amount,merchant,status,reason을 가진다. -
POST /budget/spending/import-commit(JWT) 신한카드 XLS 파일과selected_fingerprints를 함께 보내 선택한 거래만 반영한다. 이미 반영된 imported row는 중복으로 건너뛴다. -
PUT /budget/spending/:record_id소비 기록 수정. -
DELETE /budget/spending/:record_id소비 기록 삭제.
POST /budget/plan
{
"total_budget": 1500,
"from_date": "2026-04-01",
"to_date": "2026-04-30",
"alert_threshold": 0.9
}PUT /budget
{
"total_budget": 1800,
"alert_threshold": 0.9
}remaining_budget = total_budget - total_spentis_overspent = total_spent > total_budgetusage_rate = total_spent / total_budget(total_budget > 0)alert = usage_rate >= alert_thresholdtotal_spent는 항상 해당 기간의spending_records합계로 계산한다.
- import 대상은 신한카드 XLS 거래내역이며 서버는 stateless하게
preview -> commit2단계로 처리한다. - imported row는
source_type='shinhancard_xls',source_fingerprint를 저장해 동일 거래 재업로드를 중복으로 막는다. - import 후 예산 요약의
total_spent도 같은 거래원장 기준으로 계산된다.
cargo test게시글 테스트에는 dev 태그 제외 규칙을 반영한 publish/update/delete trigger 조건과, dispatch 실패가 있어도 CMS 저장은 유지되는지가 포함됩니다.
curl -sS -X POST http://127.0.0.1:8080/login \
-H 'Content-Type: application/json' \
-d '{"user_id":"me@example.com","password":"secret"}'curl -sS -X POST http://127.0.0.1:8080/login/google \
-H 'Content-Type: application/json' \
-d '{"id_token":"GOOGLE_ID_TOKEN_FROM_FRONTEND"}'curl -sS -X POST http://127.0.0.1:8080/api-keys \
-H "Authorization: $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"macrodroid-main-phone"}'응답의 api_key 값은 이때만 확인할 수 있습니다.
curl -sS http://127.0.0.1:8080/api-keys \
-H "Authorization: $ACCESS_TOKEN"curl -sS -X DELETE http://127.0.0.1:8080/api-keys/1 \
-H "Authorization: $ACCESS_TOKEN"curl -sS -X POST http://127.0.0.1:8080/budget/spending \
-H "Authorization: $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"amount":12000,"merchant":"CU 역삼신웅점","transacted_at":"2026-03-03T12:20:00"}'curl -sS -X POST http://127.0.0.1:8080/budget/spending \
-H "X-API-Key: $MACRODROID_USER_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"amount":12000,"merchant":"CU 역삼신웅점","transacted_at":"2026-03-03T12:20:00"}'