From 5b67d6f19223e3755789d0d7e32d5bbd01f87ca8 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Thu, 15 Jan 2026 12:17:59 +0900 Subject: [PATCH] =?UTF-8?q?Docs:=20[main]=20readme,=20api,=20development,?= =?UTF-8?q?=20operations=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 93 +++++++++++++++++++++++++++++++++++++++ docs/API.md | 105 ++++++++++++++++++++++++++++++++++++++++++++ docs/DEVELOPMENT.md | 97 ++++++++++++++++++++++++++++++++++++++++ docs/OPERATIONS.md | 92 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 README.md create mode 100644 docs/API.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/OPERATIONS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3f3b02 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# baekjoon-bot +작성: AI / 수정: nkey + +매일 백준(BOJ) 문제를 추천해주고, 디스코드 메시지(payload)로 쓸 수 있는 형태로 반환하는 FastAPI 서비스입니다. +`/today`에서 추천 문제 + 링크 + Discord embed payload를 생성하며, (선택) PostgreSQL에 문제집(workbook) 진행상황을 저장해 “문제집에서 아직 안 보낸 문제”를 고르는 모드도 제공합니다. +디스코드 메시지 자동화는 n8n을 활용하였습니다. + +## Quickstart +### Recommended +```bash +# clone +git clone https://nkeystudy.site/gitea/nkey/baekjoon-bot.git +cd baekjoon-bot + +# venv +python -m venv .venv +source .venv/bin/activate + +# deps +pip install -r requirements.txt + +# env (필수: DATABASE_URL) +cat > .env <<'EOF' +DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@HOST:5432/DBNAME +# (admin API 사용 시 필수) +ADMIN_PASSWORD=change-me +EOF + +# run +uvicorn app:app --host 0.0.0.0 --port 8000 + +# verify +curl -s http://localhost:8000/ | cat +curl -s "http://localhost:8000/today" | cat +``` + +### Alternative (optional) - Docker +```bash +docker build -f dockerfile -t baekjoon-bot:local . +docker run --rm -p 8000:8000 --env-file .env baekjoon-bot:local + +# verify +curl -s http://localhost:8000/ | cat +``` + +## Requirements +- Runtime/Language: Python 3.12+ (Dockerfile 기준) +- Dependencies: `fastapi`, `uvicorn`, `sqlalchemy>=2.0`, `asyncpg`, `requests`, `httpx`, `python-dotenv` 등 (`requirements.txt`) +- Tools: (optional) Docker + +## Configuration +### Environment Variables +| Key | Description | Default | Required | +|---|---|---:|:---:| +| `DATABASE_URL` | SQLAlchemy async DB URL (예: `postgresql+asyncpg://...`) | - | ✅ | +| `ADMIN_PASSWORD` | Admin API 인증 비밀번호 (`X-Admin-Password` 헤더와 비교) | `""` | ✅ (admin API) | +| `SOURCE_MODE_DEFAULT` | `/today` 기본 소스 모드 | `search` | | +| `WORKBOOK_ID_DEFAULT` | `source_mode=workbook`에서 `workbook_id` 미지정 시 기본값 | - | | +| `DIFFICULTY_MODE_DEFAULT` | `/today` 기본 난이도 모드 | `easy` | | +| `TAG_MODE_DEFAULT` | `/today` 기본 태그 모드 | `easy` | | +| `LANG_DEFAULT` | `/today` 기본 언어 | `all` | | +| `DIFFICULTY_EASY` | easy 난이도 범위 | `6..10` | | +| `DIFFICULTY_HARD` | hard 난이도 범위 | `11..15` | | +| `DIFFICULTY_ALL` | all 난이도 범위 | `1..30` | | +| `TAGS_EASY` | easy 태그 프리셋(CSV) | `""` | | +| `TAGS_HARD` | hard 태그 프리셋(CSV) | `""` | | +| `TAGS_ALL` | all 태그 프리셋(CSV) | `""` | | +| `TAG_PICK` | 태그 선택 정책 (random/none/전체) | `random` | | +| `TAG_PICK_EASY` | easy 태그 선택 정책 | `TAG_PICK` | | +| `TAG_PICK_HARD` | hard 태그 선택 정책 | `TAG_PICK` | | +| `TAG_PICK_ALL` | all 태그 선택 정책 | `TAG_PICK` 또는 `none` | | +| `TAGS_JOIN` | 태그 결합 방식 | `or` | | + +### Ports +| Service | Port | Description | +|---|---:|---| +| API | 8000 | FastAPI(Uvicorn) | + +## Usage (minimal) +- 오늘의 추천 문제(검색 모드, 기본값) + - `GET /today` +- 난이도/태그/언어 지정 + - `GET /today?difficulty=6..10&tags=dp,graphs&lang=ko,en` +- 문제집(workbook) 모드로 추천 + - `GET /today?source_mode=workbook&workbook_id=12345&workbook_pick=level_asc` +- Admin: 문제집 메타(제목/레벨/태그) 보강 + - `POST /admin/workbooks/{workbook_id}/enrich` + `X-Admin-Password: ...` + +## Docs +- Operations: `docs/OPERATIONS.md` +- Development: `docs/DEVELOPMENT.md` +- API: `docs/API.md` + diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..1ece057 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,105 @@ +# API Reference +작성: AI / 수정: nkey + +## Base URL +- 로컬: `http://localhost:8000` + +## Authentication +- Admin API 전용 + - Header: `X-Admin-Password: ` + - `ADMIN_PASSWORD` 미설정 시 admin API는 500으로 실패합니다. + +## Endpoints (Top) + +### GET / +상태 확인(헬스체크). +```bash +curl -s http://localhost:8000/ +``` + +### GET /today +오늘의 추천 문제를 반환합니다. 기본은 solved.ac 검색(search) 모드이며, workbook 모드로도 동작합니다. + +#### Query Parameters +- `source_mode` (string): `search|workbook` (기본값: `SOURCE_MODE_DEFAULT` 또는 `search`) +- workbook 모드: + - `workbook_id` (int, optional): workbook id. 미지정 시 `WORKBOOK_ID_DEFAULT` 사용 시도 + - `workbook_pick` (string): `random|level_asc` (기본: `level_asc`) +- search 모드(필터): + - `difficulty_mode` (string): `easy|hard|all` (기본값: `DIFFICULTY_MODE_DEFAULT` 또는 `easy`) + - `tag_mode` (string): `easy|hard|all` (기본값: `TAG_MODE_DEFAULT` 또는 `easy`) + - `difficulty` (string, optional): 예 `6..10` (지정 시 `difficulty_mode`보다 우선) + - `tags` (string, optional): 예 `dp,graphs` (지정 시 `tag_mode`보다 우선) + - `lang` (string): `ko | en | ko,en | all` (기본값: `LANG_DEFAULT` 또는 `all`) + +#### Examples +- 기본(search) 모드: +```bash +curl -s "http://localhost:8000/today" | cat +``` + +- 난이도/태그/언어 직접 지정: +```bash +curl -s "http://localhost:8000/today?difficulty=6..10&tags=dp,graphs&lang=ko,en" | cat +``` + +- workbook 모드: +```bash +curl -s "http://localhost:8000/today?source_mode=workbook&workbook_id=12345&workbook_pick=level_asc" | cat +``` + +#### Response (성공 예시 형태) +- 공통: `problemId`, `title`, `level`, `problemUrl`, `solvedUrl`, `discordPayload` 포함 +- search 모드: `query`, `difficulty`, `tags`, `difficulty_mode`, `tag_mode`, `lang` 포함 +- workbook 모드: `workbook_id` 포함 + +#### Errors +- workbook 모드에서 `workbook_id`를 확정할 수 없을 때: 400 +- workbook 모드에서 더 이상 뽑을 문제가 없을 때: 409 (`no_more_problems_in_workbook`) +- search 모드에서 solved.ac 조회 실패: 503 (`failed_to_fetch_problem`) + +--- + +### POST /admin/workbooks/{workbook_id}/enrich +workbook에 포함된 문제들의 메타(제목/레벨/태그)를 solved.ac `problem/show`로 채웁니다. (DB 필요) + +#### Auth +- `X-Admin-Password` 필요 + +#### Query Parameters +- `only_missing` (bool, default `true`): `true`면 NULL만 채움 / `false`면 덮어씀 +- `commit_every` (int, default `50`, 1~500): 몇 개마다 commit할지 +- `sleep_sec` (float, default `0.12`, 0.0~2.0): solved.ac 호출 사이 대기 + +#### Example +```bash +curl -s -X POST \ + -H "X-Admin-Password: change-me" \ + "http://localhost:8000/admin/workbooks/12345/enrich?only_missing=true&commit_every=50&sleep_sec=0.12" | cat +``` + +#### Response +- `status: ok` +- `result`에 `target_count`, `updated`, `skipped`, `failed` 등 요약 포함 + +--- + +### DELETE /admin/workbooks/{workbook_id}/reset +해당 workbook의 전송(픽) 기록을 초기화하여, 다시 문제를 뽑을 수 있게 합니다. (DB 필요) + +#### Auth +- `X-Admin-Password` 필요 + +#### Example +```bash +curl -s -X DELETE \ + -H "X-Admin-Password: change-me" \ + "http://localhost:8000/admin/workbooks/12345/reset" | cat +``` + +#### Response +- `deleted_sends`: 삭제된 기록 수 + +## Data Models (참고) +- `discordPayload`는 Discord Webhook/봇 메시지에 사용할 수 있는 `embeds` 구조를 포함합니다. + - 실제 전송(Discord webhook 호출)은 이 서비스 밖(외부 스케줄러/워크플로우)에서 수행하는 것을 전제로 합니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..235b037 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,97 @@ +# Development Guide +작성: AI / 수정: nkey + +## Project Layout +- `app.py` : FastAPI 엔트리포인트, 라우팅(`/`, `/today`, `/admin/...`) +- `utils.py` : 환경변수 헬퍼, solved.ac 검색 쿼리 생성, admin 인증, solved.ac 검색 호출 +- `db.py` : SQLAlchemy Async 엔진/세션 및 `get_db()` DI +- `workbook_picker.py` : DB에서 “아직 보내지 않은 문제” 1개 선택 + send 기록 +- `workbook_enricher.py` : solved.ac `problem/show`로 workbook 문제 메타(제목/레벨/태그) 채우기 +- `workbook_importer.py` : BOJ 문제집 페이지에서 문제 ID 수집 + DB upsert (현재 `app.py`에서 라우팅은 주석 처리됨) +- `requirements.txt` : 파이썬 의존성 +- `dockerfile` : 컨테이너 실행 정의(uvicorn, 8000) + +## Prerequisites +- Python 3.12+ 권장 (Docker 이미지 기준: `python:3.12-slim`) +- PostgreSQL (workbook 모드/관리 API 사용 시) +- pip / venv + +## Local Setup +```bash +git clone https://nkeystudy.site/gitea/nkey/baekjoon-bot.git +cd baekjoon-bot + +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +cat > .env <<'EOF' +DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@HOST:5432/DBNAME +ADMIN_PASSWORD=change-me +EOF + +# FastAPI runs +uvicorn app:app --reload --host 0.0.0.0 --port 8000 + +# sanity check +curl -s http://localhost:8000/ | cat +``` + +## Common Commands +레포에 Makefile/스크립트/테스트 명령이 따로 정의되어 있지 않아, 일반적인 실행 명령만 기재합니다. +```bash +# run +uvicorn app:app --reload --host 0.0.0.0 --port 8000 + +# install deps +pip install -r requirements.txt +``` + +## Environment Variables (Dev) +- 필수: `DATABASE_URL` +- Admin API 개발/테스트 시: `ADMIN_PASSWORD` +- `/today`의 기본값을 바꾸려면 아래 변수를 사용: + - `SOURCE_MODE_DEFAULT`, `WORKBOOK_ID_DEFAULT` + - `DIFFICULTY_MODE_DEFAULT`, `TAG_MODE_DEFAULT`, `LANG_DEFAULT` + - `DIFFICULTY_EASY|HARD|ALL`, `TAGS_EASY|HARD|ALL`, `TAG_PICK*`, `TAGS_JOIN` + +## DB/Migrations (if applicable) +- 코드에서 참조하는 테이블(DDL은 레포에 없음): + - `workbooks`, `workbook_problems`, `workbook_sends` +```bash +-- 1) 문제집 +CREATE TABLE IF NOT EXISTS workbooks ( + id BIGINT PRIMARY KEY, -- BOJ workbook id 그대로 사용 + title TEXT, + source TEXT NOT NULL DEFAULT 'boj', -- 'boj' 고정(필요하면 확장) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- 2) 문제집-문제 목록 +CREATE TABLE IF NOT EXISTS workbook_problems ( + workbook_id BIGINT NOT NULL REFERENCES workbooks(id) ON DELETE CASCADE, + problem_id INTEGER NOT NULL, + title_ko TEXT, + title_en TEXT, + level INTEGER, -- solved.ac level(0~30) + tags TEXT[] DEFAULT NULL, -- optional + added_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (workbook_id, problem_id) +); + +CREATE INDEX IF NOT EXISTS idx_workbook_problems_workbook + ON workbook_problems(workbook_id); + +-- 3) 발송/사용 기록(문제집 단위 중복 방지 핵심) +CREATE TABLE IF NOT EXISTS workbook_sends ( + workbook_id BIGINT NOT NULL REFERENCES workbooks(id) ON DELETE CASCADE, + problem_id INTEGER NOT NULL, + sent_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (workbook_id, problem_id) +); + +CREATE INDEX IF NOT EXISTS idx_workbook_sends_workbook + ON workbook_sends(workbook_id); + +``` diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..0f42cd1 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,92 @@ +# Operations Guide +작성: AI / 수정: nkey + +## Supported Deployment Modes +- Dockerfile 기반 단일 컨테이너 실행 (`dockerfile`) +- (참고) 로컬에서 Uvicorn 직접 실행 + +> Docker Compose / Kubernetes / Terraform 관련 파일은 레포에 없습니다. + +## Production Checklist +- Secrets/ENV: + - `DATABASE_URL`는 필수입니다. + - Admin API를 운영에서 사용할 경우 `ADMIN_PASSWORD`를 반드시 설정하고, 클라이언트에서 `X-Admin-Password` 헤더로 전달해야 합니다. +- Networking: + - 인바운드: TCP 8000 (API) + - 아웃바운드: solved.ac API, acmicpc.net(백준) 접근 필요(문제/문제집 정보 조회). +- Storage: + - 앱 자체는 로컬 파일 저장을 전제로 하지 않습니다. + - DB는 외부 PostgreSQL을 사용합니다. (주의) 레포에 DDL/마이그레이션이 포함되어 있지 않으므로, 운영 DB 스키마는 별도로 준비되어 있어야 합니다. +- Observability: + - 기본 Uvicorn 로그(stdout) 기반. 별도 로깅/메트릭 구성은 레포에 없습니다. +- Scaling: + - `/today`의 search 모드는 DB 없이도 동작 가능(단, 코드상 DB 의존성 주입은 존재). + - workbook 모드는 DB 상태(workbook_sends)에 의해 결과가 달라지므로, 다중 인스턴스 운영 시 동일 DB를 바라보도록 구성해야 합니다. + +## Deployment +### Docker +```bash +# build (주의: 파일명이 'Dockerfile'이 아니라 'dockerfile' 입니다) +docker build -f dockerfile -t baekjoon-bot:prod . + +# run +cat > .env <<'EOF' +DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@HOST:5432/DBNAME +ADMIN_PASSWORD=change-me +EOF + +docker run -d --name baekjoon-bot \ + --env-file .env \ + -p 8000:8000 \ + baekjoon-bot:prod + +# verify +curl -s http://localhost:8000/ | cat +``` + +## Operations +### Healthcheck +- `GET /` + - 성공 조건: `{"status":"ok"}` 응답 + +### Logs +- Docker 실행 시: +```bash +docker logs -f baekjoon-bot +``` + +## Security Notes (only with evidence) +- Admin API는 `X-Admin-Password` 헤더 기반 단일 비밀번호 체크입니다. + - `ADMIN_PASSWORD`가 비어 있으면 admin API 호출 시 500으로 실패합니다(설정 누락). +- `.env`는 `.gitignore`에 포함되어 있어 커밋되지 않도록 되어 있습니다. + +## Incident Runbook (Top 5) +1) **env 누락** +- Symptom: 앱 시작 시 `DATABASE_URL is required...`로 즉시 종료 +- Cause: `DATABASE_URL` 미설정 +- Fix: `.env` 또는 런타임 환경변수에 `DATABASE_URL` 설정 +- Verify: 컨테이너/프로세스 정상 기동 후 `GET /`가 200 + +2) **포트 충돌** +- Symptom: 컨테이너 실행 시 `bind: address already in use` 또는 접근 불가 +- Cause: 호스트 8000 포트 사용 중 +- Fix: `-p 18000:8000` 같이 호스트 포트 변경 +- Verify: `curl http://localhost:18000/` + +3) **DB 연결 실패** +- Symptom: `/today` 또는 admin 호출 시 500, 로그에 DB 커넥션 에러 +- Cause: `DATABASE_URL` 잘못됨 / 네트워크/방화벽 / DB 다운 +- Fix: URL 재확인, DB 접근 허용, DB 상태 확인 +- Verify: `/today?source_mode=workbook&workbook_id=...`가 정상 응답(워크북 모드 사용 시) + +4) **Admin 인증 실패** +- Symptom: admin API가 403 `invalid admin password` +- Cause: `X-Admin-Password` 누락 또는 불일치 +- Fix: `ADMIN_PASSWORD` 값과 동일한 헤더 전달 +- Verify: `POST /admin/workbooks/{id}/enrich`가 200 + +5) **문제집 소진** +- Symptom: `GET /today?source_mode=workbook&workbook_id=...`가 409 + `no_more_problems_in_workbook` +- Cause: 해당 workbook에서 아직 보내지 않은 문제 후보가 없음(`workbook_sends`에 모두 기록됨) +- Fix: 진행상황 초기화 API 호출(주의: 관리자 권한 필요) 또는 다른 workbook 사용 +- Verify: 초기화 후 workbook 모드 호출 시 정상 추천 반환