ABS(Aolda Blog Service) 백엔드 API 서버입니다.
블로그 게시글, 이미지, 사용자, 인증 기능을 제공하며 FastAPI 기반으로 동작합니다.
- Python
- FastAPI
- SQLAlchemy
- Alembic
- MySQL
- boto3
- Authlib
abs-backend/
├── app/
│ ├── api/
│ │ ├── deps.py
│ │ └── v1/
│ │ ├── api_router.py
│ │ └── endpoints/
│ │ ├── auth.py
│ │ ├── images.py
│ │ ├── keycloak_auth.py
│ │ ├── posts.py
│ │ └── users.py
│ ├── core/
│ │ ├── config.py
│ │ └── security.py
│ ├── db/
│ │ ├── database.py
│ │ ├── models/
│ │ └── schemas/
│ ├── services/
│ └── main.py
├── alembic/
│ └── versions/
├── compose.yaml
├── compose.production.yaml
├── Dockerfile
└── requirements.txt
.env 파일이 필요합니다.
DATABASE_URL=mysql+pymysql://root:<password>@127.0.0.1:3306/abs_db
JWT_SECRET_KEY=<secret>
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_MINUTES=10080
KEYCLOAK_ISSUER_URI=https://sso.example.com/realms/<realm>
KEYCLOAK_CLIENT_ID=<keycloak-client-id>
KEYCLOAK_CLIENT_SECRET=<keycloak-client-secret>
API_SERVER_URL=https://blog-api.example.com
CONSOLE_PAGE_URL=https://blog.example.com
S3_ENDPOINT_URL=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3_REGION=auto
S3_ACCESS_KEY_ID=<access-key-id>
S3_SECRET_ACCESS_KEY=<secret-access-key>
S3_BUCKET_NAME=<bucket-name>
S3_PUBLIC_BASE_URL=https://<public-bucket-url>설명:
DATABASE_URL: 애플리케이션이 사용할 DB 연결 문자열JWT_SECRET_KEY: 내부 JWT 서명 키KEYCLOAK_ISSUER_URI: Keycloak realm issuer URIKEYCLOAK_CLIENT_ID,KEYCLOAK_CLIENT_SECRET: Keycloak OIDC 클라이언트 정보API_SERVER_URL: Keycloak callback URL 생성에 사용할 백엔드 base URLCONSOLE_PAGE_URL: 로그인 후 기본적으로 돌아갈 프론트 콘솔 URLS3_*: Cloudflare R2 등 S3 호환 스토리지 업로드 설정
주의:
API_SERVER_URL에는 origin만 넣어야 합니다.
예:https://backend.localhost
잘못된 예:https://backend.localhost/api/v1/auth/callback
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
alembic upgrade head
uvicorn app.main:app --host 127.0.0.1 --port 8000 --reloaddocker compose -f compose.yaml up -d --build- API Base:
http://127.0.0.1:8000/api/v1 - Swagger UI:
http://127.0.0.1:8000/docs - Root:
http://127.0.0.1:8000/
인증 흐름은 다음과 같습니다.
- 프론트가
GET /api/v1/auth/login호출 - 백엔드가 Keycloak 로그인 페이지로 리다이렉트
- Keycloak 인증 완료 후
GET /api/v1/auth/callback - 백엔드가 Keycloak 사용자 정보를 조회
keycloak_sub기준으로 기존 사용자 연결 또는 신규 사용자 생성- 내부 access token / refresh token 발급
- 프론트 콘솔의
/auth/callback으로 리다이렉트
백엔드는 Keycloak 토큰을 그대로 프론트에 넘기지 않고, 내부 JWT를 발급합니다.
- access token: API 인증용
- refresh token: 재발급용
토큰 갱신 엔드포인트:
POST /api/v1/auth/refresh
OAuth state를 이용해 로그인 시작 환경에 따라 적절한 콘솔 URL로 다시 리다이렉트합니다.
예:
- 운영 콘솔에서 로그인 시작 -> 운영 콘솔
/auth/callback - 로컬 콘솔에서 로그인 시작 -> 로컬 콘솔
/auth/callback
기본값은 CONSOLE_PAGE_URL 입니다.
사용자는 Keycloak 기반으로 관리되며, 주요 필드는 다음과 같습니다.
idusernameemailnamebioavatarrolekeycloak_subwebsite,github,gitlab,linkedin,discord,mail
특징:
- 기존 사용자 로그인 시
keycloak_sub가 연결됩니다. - 사용자가 직접 수정한
name,avatar는 로그인할 때 매번 강제로 덮어쓰지 않습니다. - 작성자 응답에서 프로필 이미지가 없으면
avatar는null입니다.
게시글은 기존 author_id를 유지하면서, 별도의 연결 테이블로 공동 편집자를 관리합니다.
관련 구조:
posts.author_id: 기존 대표 작성자post_users: 게시글과 사용자 간 다대다 연결 테이블
즉 한 게시글에 여러 사용자가 공동 편집자로 연결될 수 있습니다.
- 게시글 목록/상세는 비로그인 사용자도 조회 가능
- 비로그인 상태에서는 응답의
can_edit가 항상false
다음 작업은 공동 편집자만 가능합니다.
- 게시글 본문 저장
- 게시글 삭제
- 이미지 업로드
- 이미지 삭제
GET /api/v1/auth/loginGET /api/v1/auth/callbackPOST /api/v1/auth/refresh
GET /api/v1/users/mePUT /api/v1/users/meGET /api/v1/users/authorsGET /api/v1/users/{username}
POST /api/v1/posts/templateGET /api/v1/postsGET /api/v1/posts/{post_id}PUT /api/v1/posts/{post_id}/contentDELETE /api/v1/posts/{post_id}POST /api/v1/posts/{post_id}/viewsGET /api/v1/posts/{post_id}/views
POST /api/v1/images/GET /api/v1/images/posts/{post_id}DELETE /api/v1/images/{image_id}
게시글 목록/상세 응답에는 다음 정보가 포함됩니다.
idauthor_idauthorscan_editviewscreated_attitledescriptiontagsimagecontent(상세 조회 시)
핵심 필드:
authors: 공동 편집자 username 배열can_edit: 현재 사용자가 이 글을 수정할 수 있는지 여부
PUT /api/v1/posts/{post_id}/content 요청 body는 다음 필드를 지원합니다.
{
"title": "제목",
"description": "설명",
"tags": ["tag1", "tag2"],
"image": null,
"content": "본문",
"authors": ["alice", "bob"]
}설명:
authors는 선택값입니다.- 값이 들어오면 공동 편집자 목록을 해당 username 배열로 갱신합니다.
- 존재하지 않는 username이 포함되면
400 Bad Request를 반환합니다.
이미지는 S3 호환 오브젝트 스토리지에 저장됩니다. 현재 기준으로는 Cloudflare R2를 사용할 수 있습니다.
동작:
- 업로드 시 object storage에 파일 저장 후 DB에 공개 URL과
object_key저장 - 게시글별 이미지 목록 조회 가능
- 삭제 시 DB 레코드와 object storage object를 함께 삭제
환경 변수 예시:
S3_ENDPOINT_URL=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3_REGION=auto
S3_ACCESS_KEY_ID=<access-key-id>
S3_SECRET_ACCESS_KEY=<secret-access-key>
S3_BUCKET_NAME=<bucket-name>
S3_PUBLIC_BASE_URL=https://<public-bucket-url>게시글 조회수는 별도 API로 증가합니다.
POST /api/v1/posts/{post_id}/viewsGET /api/v1/posts/{post_id}/views
즉 게시글 상세 조회와 조회수 증가가 분리되어 있습니다.
Alembic을 사용합니다.
alembic upgrade head
alembic downgrade -1
alembic current로컬 HTTPS 환경에서는 Portless와 같은 도구를 이용해 다음 조합으로 테스트할 수 있습니다.
- 프론트:
https://abs.localhost - 백엔드:
https://backend.localhost
예시:
API_SERVER_URL=https://backend.localhost
CONSOLE_PAGE_URL=https://abs.localhost이 경우 Keycloak client 설정에도 동일한 callback URL이 등록되어 있어야 합니다.
예:
- Valid redirect URI:
https://backend.localhost/api/v1/auth/callback - Web origin:
https://abs.localhost