# 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); ``` ## CI/CD (Gitea Actions) Workflow: `.gitea/workflows/cicd.yml` ### Trigger - `main` 브랜치로 `push` 시 실행 ### Secrets (레포 Actions/Secrets에 등록) | Key | Purpose | |---|---| | `NKEY_PAT` | 워크플로우에서 Gitea repo를 수동 checkout(fetch)할 때 사용 | | `DOCKERHUB_USERNAME` | Docker Hub 이미지 네임스페이스 | | `DOCKERHUB_TOKEN` | Docker Hub 로그인 토큰 | | `DISCORD_WEBHOOK` | 성공/실패 알림 전송 | ### Build & Push - 워크플로우는 아래와 동일한 형태로 이미지를 빌드/푸시합니다. ```bash IMAGE="${DOCKERHUB_USERNAME}/baekjoon-bot:latest" docker build -t "${IMAGE}" . docker push "${IMAGE}" ``` > NOTE: 위 커맨드는 기본 Dockerfile을 사용합니다. 레포에 있는 파일은 `dockerfile`(소문자)이므로, 로컬/CI에서 동일하게 동작시키려면 파일명/옵션(`-f dockerfile`) 정합성을 확인하세요. ### Deploy - 워크플로우 배포는 레포 내부 파일이 아닌 서버(또는 self-hosted runner)에 존재하는 compose 파일을 사용합니다. ```bash docker compose -f /nkeysworld/compose.apps.yml pull baekjoon-bot docker compose -f /nkeysworld/compose.apps.yml up -d baekjoon-bot docker image prune -f ``` ### Notifications - 워크플로우는 항상(`if: always()`) Discord webhook으로 결과를 전송합니다.