이 페이지에서
JunkBox apps/chess-api 가이드
1. 애플리케이션 역할
apps/chess-api는 JunkBox의 Lichess sleepzz-bot 체스 샌드박스 백엔드 앱임.
Lichess challenge 생성, Lichess account stream 연결, Lichess bot game stream 처리, sleepzz-bot move 선택, Lichess move 제출, WebSocket 실시간 로그 송출을 담당함.
실전 착수는 CHESS_MOVE route에 등록된 모델을 통해 sleepzz-bot이 담당하며, 현재 상황과 착수 근거도 함께 작성함.
AI provider 호출, route lookup, usage log 기록, LangGraph 실행은 libs/shared-ai를 통해 수행함.
DB 연결, 설정, 공통 로깅, 예외, AsyncSession 생성은 libs/shared-core를 사용함.
화면 정적 자산과 공통 다크 테마는 libs/shared-ui를 사용함.
2. 런타임 구성
FastAPI 애플리케이션 엔트리포인트는 junkbox_chess_api.main:app임.
기본 화면 진입 경로는 /chess임.
상태 확인 경로는 /health임.
OpenAPI 노출 경로는 /docs, /redoc, /openapi.json임.
apps/chess-ui/dist를 /chess-ui 경로로 직접 마운트함.
libs/shared-ui 산출 자산은 /shared-ui 경로로 직접 마운트함.
앱 favicon은 포털 메뉴의 menu_type_code=APP, icon_type_code=APP_ICON, icon_val 조합에서 chess 앱 아이콘 slug를 해석해 사용하며, 조회 실패 시 chess slug를 fallback으로 사용함.
서버 템플릿은 apps/chess-api/src/junkbox_chess_api/templates/chess.html에서 React 마운트 루트를 제공함.
chess-api의 Jinja 환경은 앱 템플릿 경로와 junkbox_shared_ui 템플릿 경로를 함께 로드함.
CSS 링크는 chess.html이 직접 선언하지 않고 shared-ui stylesheet 매크로를 import해 호출함.
체스 화면은 hub-ui-base.css + hub-ui-desktop.css + vendor/tabulator/tabulator_simple.min.css + junkbox-tabulator.css 조합을 사용함.
2.1 로컬 개발 서버 연동
chess-api는 18002 포트 기준 백엔드로 실행할 수 있음.
apps/chess-ui는 15002 포트 기준 Vite 개발 서버로 실행할 수 있음.
페이지 HTML과 API는 18002가 제공하고, React 엔트리와 HMR 자산은 15002/chess-ui/* 경로에서 로드할 수 있음.
2.2 운영 배포 기준
운영 컨테이너 기본 포트는 8002임.
운영 이미지는 apps/chess-ui/dist, libs/shared-ui 자산, chess-api 실행 코드를 함께 포함함.
운영 Docker Compose 서비스명은 junkbox-chess임.
운영 이미지는 ghcr.io/{owner}/junkbox-chess:latest 기준으로 배포됨.
GitHub Actions selective build/deploy는 apps/chess-api/**, apps/chess-ui/**, libs/shared-core/**, libs/shared-ai/**, libs/shared-ui/**, docker-compose.prod.yml 변경 시 junkbox-chess를 감지함.
운영 환경변수는 /sorc001/junkbox/env/.env.shared와 /sorc001/junkbox/env/.env.prod를 함께 참조하고, 컨테이너 내부 ENV_FILE=/app/env/.env.prod 기준으로 로딩함.
운영 로그 볼륨은 /logs001/junkbox/chess-api 호스트 경로를 컨테이너 내부 /app/logs에 연결함.
공통 애플리케이션 로그 파일 경로는 /app/logs/app.log임.
AI 라우팅 정책은 /sorc001/junkbox/master-data/ai-routing.yml를 컨테이너 내부 /app/var/master-data/ai-routing.yml에 read-only로 마운트함.
체스 실전 착수는 LiteLLM proxy를 통한 CHESS_MOVE route를 사용함.
체스 앱 전용 런타임 설정은 apps/chess-api/config/chess-runtime.yml을 사용함.
컨테이너 내부 체스 런타임 설정 기본 경로는 /app/apps/chess-api/config/chess-runtime.yml임.
CHESS_RUNTIME_CONFIG_YAML_PATH로 체스 런타임 설정 파일 경로를 override할 수 있음.
CHESS_BASE_URL, DATABASE_URL, LICHESS_BOT_TOKEN, LITELLM_BASE_URL, LITELLM_API_KEY, AI_ROUTING_YAML_PATH 또는 MASTER_DATA_DIR, CHESS_RUNTIME_CONFIG_YAML_PATH 설정 정합성이 중요함.
2.3 AI 착수 런타임 설정
체스 실전 착수는 CHESS_MOVE route의 LiteLLM provider/model 설정을 따름.
기본 모델은 openrouter/qwen/qwen-2.5-7b-instruct임.
LITELLM_BASE_URL, LITELLM_API_KEY, AI 라우팅 YAML의 CHESS_MOVE 설정이 착수 생성의 필수 런타임 입력임.
2.4 체스 런타임 YAML
체스 앱 전용 런타임 정책은 apps/chess-api/config/chess-runtime.yml에서 관리함.
commentary.timeout_seconds는 sleepzz-bot 착수 생성의 앱 레벨 제한 시간으로 사용하며 기본값은 45초임.
commentary.max_pgn_chars는 CHESS_MOVE 프롬프트에 포함할 PGN 최대 문자 수이며 기본값은 160자임.
commentary.max_board_ascii_chars는 CHESS_MOVE 프롬프트에 포함할 board ASCII 최대 문자 수이며 기본값은 120자임.
2.5 공통 로깅 초기화
앱 import 시점에 shared-core의 setup_app_logging()을 호출함.
설정값은 settings.modules.chess.logging에서 읽음.
로그 sink는 콘솔 출력과 파일 출력 두 축으로 동시에 구성됨.
3. 라우팅 구조
3.1 JSON API
POST /api/chess/challenges
GET /api/chess/games
POST /api/chess/games/{game_id}/resign
GET /api/chess/game-history
GET /api/chess/games/{game_id}/logs
3.2 WebSocket
3.3 웹 화면
4. Lichess 연동 계약
4.1 인증
Lichess API 인증은 LICHESS_BOT_TOKEN을 Bearer 토큰으로 전달함.
토큰에는 challenge 읽기/쓰기와 bot play 권한이 필요함.
Lichess 토큰은 사용자 JWT 또는 HUB_API_INTERNAL_TOKEN과 공유하지 않음.
4.2 Challenge 생성
일반 유저 또는 일반 bot 계정 상대는 POST https://lichess.org/api/challenge/{username} 경로를 사용함.
Lichess 내장 AI 상대는 POST https://lichess.org/api/challenge/ai 경로를 사용함.
내장 AI challenge에는 level 파라미터를 함께 전달함.
공통 challenge payload는 rated, clock.limit, clock.increment, color, variant를 포함함.
상대 ID가 비어 있으면 LICHESS_DEFAULT_OPPONENT_ID를 사용할 수 있음.
내장 AI 상대 유형에서는 상대 ID 입력이 필수가 아니며, lichess_ai_level 값이 1부터 8 사이여야 함.
4.3 Stream 처리
계정 이벤트 스트림은 /api/stream/event를 사용함.
gameStart 이벤트를 받으면 /api/bot/game/stream/{game_id}로 게임 스트림을 연결함.
게임 스트림은 gameFull과 gameState 이벤트를 처리함.
스트림이 끊기거나 정상 종료되면 LICHESS_STREAM_RECONNECT_DELAY_SECONDS를 최소 기준으로 지수 백오프를 적용하여 재연결함.
account stream 또는 진행 중인 game stream이 오류 없이 정상 종료된 경우에는 장시간 백오프를 적용하지 않고 짧은 지연 후 빠르게 재연결함.
game stream이 정상 종료되어도 게임 상태가 종료 상태가 아니면 스트림 task를 끝내지 않고 같은 game id로 재연결함.
Lichess가 429 Too Many Requests를 반환하면 Retry-After 헤더와 rate-limit 전용 백오프를 우선 적용하여 재접속 폭주를 방지함.
WebSocket 구독자에게 account stream, game stream, challenge, 오류, warning 상태를 status 이벤트로 송출함.
4.4 Move 제출
내 턴이 되면 현재 board에서 legal moves를 계산하고 sleepzz-bot CHESS_MOVE graph를 실행함.
sleepzz-bot은 legal moves 목록의 choice 번호 하나를 반환하고, 현재 상황:과 착수 근거: 두 줄 thought를 함께 반환해야 함.
앱 계층은 choice 번호를 UCI move로 매핑하고, 매핑된 UCI move가 legal moves에 포함되면 POST /api/bot/game/{game_id}/move/{move}를 호출함.
choice 번호는 own bot move의 event_payload.choice에 보존될 수 있으며, 화면 보드 메타의 Choice 값으로 사용됨.
sleepzz-bot 호출 실패, timeout, JSON 파싱 실패가 발생하면 시간패를 피하기 위해 python-chess legal moves 중 fallback 수를 선택해 제출함.
sleepzz-bot이 legal moves 밖의 수를 반환하면 같은 턴에서 해당 수를 forbidden move로 전달해 1회 재시도하고, 반복 실패 시 fallback 합법 수를 제출함.
fallback 선택은 mate-in-1, capture, check, castling, 중앙/전개 휴리스틱 순서의 보수적 정책을 사용함.
상대 수는 Lichess game state에서 새 ply를 확인하는 즉시 turn_log로 송출하여 화면 체스판이 한 수 단위로 갱신되게 함.
선택 수 제출 직후 move log row를 저장하고 WebSocket turn_log를 송출함.
commentary_latency_ms는 sleepzz-bot 착수와 thought 생성에 걸린 시간을 기록함.
4.5 게임 포기
진행 중인 게임은 POST /api/chess/games/{game_id}/resign으로 포기 처리할 수 있음.
API는 Lichess Bot API의 /api/bot/game/{game_id}/resign을 호출함.
포기 성공 시 런타임 상태를 resign으로 갱신하고 game row를 저장한 뒤 WebSocket game 이벤트와 status 이벤트를 송출함.
이미 종료된 게임이나 런타임에 없는 게임은 오류로 처리함.
5. LangGraph 체스 착수 흐름
chess-api는 shared-ai의 GraphAiRunner를 사용함.
graph key는 CHESS_MOVE임.
route key도 CHESS_MOVE임.
provider 값은 litellm을 사용함.
기본 모델은 openrouter/qwen/qwen-2.5-7b-instruct임.
체스 착수 AI 호출은 LITELLM_BASE_URL과 LITELLM_API_KEY 기준의 LiteLLM proxy를 통해 수행함.
provider 식별자를 기능별로 분리하지 않고 litellm 단일 provider를 사용함.
CHESS_MOVE route는 짧은 구조화 응답을 전제로 프롬프트 기반 JSON 출력과 낮은 temperature를 사용함.
CHESS_MOVE는 320 output token 예산과 간결한 프롬프트를 사용해 두 줄 thought와 choice 번호 JSON을 생성함.
sleepzz-bot은 legal moves choice 목록 중 제출할 수를 직접 선택함.
sleepzz-bot은 자신이 판단한 현재 상황과 이번 착수 근거를 현재 상황:과 착수 근거: 두 줄 형식으로 짧게 설명함.
착수 생성 timeout과 프롬프트 축약 정책은 apps/chess-api/config/chess-runtime.yml의 commentary 설정 값을 사용함.
5.1 Agent 입력
game_id
turn_number
fen
legal_moves
board_ascii
pgn
side
opponent_id
legal_moves는 현재 board에서 가능한 UCI 문자열 목록이며, sleepzz-bot이 반드시 이 목록 중 하나를 선택해야 함.
5.2 Agent 출력
thought
choice
thought는 현재 상황:과 착수 근거: 말머리를 가진 두 줄 해설을 기준으로 함.
choice는 legal moves choice 목록 안에 있는 정수여야 함.
앱 계층은 choice를 UCI move로 매핑한 뒤 Lichess 제출 수 결정에 사용하며, 다시 legal moves 포함 여부를 검증함.
앱 계층은 정상 choice 선택 시 결과 로그의 event_payload.choice에 원본 choice 번호를 보존함.
5.3 프롬프트 컨텍스트
FEN은 현재 보드 상태의 기계 판독 원본임.
board_ascii는 rank 8부터 rank 1까지 사람이 읽는 보드 시각화 문자열임.
pgn은 DB 저장과 로그에는 전체 기보를 보존하지만, AI 해설 프롬프트에는 토큰 비용과 로컬 LLM 지연을 줄이기 위해 최근 구간으로 축약됨.
프롬프트는 legal moves, FEN, 축약 PGN, 축약 board ASCII를 중심으로 구성됨.
sleepzz-bot이 직접 착수하므로 legal moves 전체 목록은 프롬프트에 포함함.
6. WebSocket 이벤트 계약
6.1 game 이벤트
type은 game임.
payload는 GameSchema임.
주요 필드는 game_id, opponent_id, side, status, result, started_at, ended_at, opponent_type, opponent_ai_level, opponent_display_name임.
6.2 turn_log 이벤트
type은 turn_log임.
payload는 TurnLogSchema임.
주요 필드는 game_id, turn_number, ply_number, side, actor_type, event_type, fen, thought, move, san, uci, board_ascii, pgn, my_remaining_time_ms, opponent_remaining_time_ms, commentary_latency_ms, event_payload, created_at임.
actor_type은 own_bot, opponent, system, lichess 계열 값을 사용함.
event_type은 move, game_start, game_finish, clock, status, warning, error 계열 값을 사용함.
상대 move 로그는 Lichess game state에서 새 ply를 확인하는 즉시 송출됨.
own bot move 로그는 sleepzz-bot 착수와 thought 생성 및 Lichess 제출이 완료된 뒤 송출됨.
commentary_latency_ms는 sleepzz-bot 착수와 thought 생성에 걸린 시간을 밀리초 단위로 나타냄.
own bot move의 event_payload.commentary_status는 completed, fallback_after_ai_error, fallback_after_invalid_move 같은 판단 상태를 나타낼 수 있음.
own bot move의 event_payload.choice는 sleepzz-bot이 선택한 legal moves choice 번호를 나타냄.
own bot move의 event_payload.fallback_reason은 fallback 합법 수 제출 원인을 나타냄.
6.3 status 이벤트
type은 status임.
payload는 level, message를 포함함.
level은 info, warning, error 계열 문자열을 사용함.
6.4 게임 이력 API 계약
GET /api/chess/game-history는 chess 앱을 통해 발생한 AI 대국 목록을 페이지 단위로 반환함.
요청 query는 page, size를 사용함.
응답은 items, page, size, total, last_page를 포함함.
items의 주요 필드는 game_id, side, status, result, started_at, ended_at, opponent_type, opponent_display_name, opponent_ai_level임.
정렬 기준은 started_at DESC, game_uid DESC임.
6.5 게임 로그 조회 API 계약
GET /api/chess/games/{game_id}/logs는 저장된 게임별 로그를 시간 순서 기준으로 반환함.
과거 게임 이력을 선택한 화면은 이 API로 로그를 조회함.
진행 중인 게임을 선택한 화면은 WebSocket live timeline으로 복귀함.
7. 데이터 저장 책임
7.1 PostgreSQL 스키마
체스 도메인 저장소는 PostgreSQL chess 스키마를 사용함.
대국 마스터 정보는 chess.tb_game_m을 사용함.
게임 로그는 chess.tb_game_move_log_n을 사용함.
7.2 chess.tb_game_m
game_id는 Lichess game id임.
game_id는 논리 unique key로 사용함.
주요 컬럼은 생성 사용자, 내 bot 이름, 내 Lichess id, 상대 이름, 상대 유형, 상대 Lichess id, 상대 표시명, Lichess 내장 AI level, side, speed, variant, status, result, 초기 FEN, 최종 FEN, 최종 PGN, 종료 사유, 내 남은 시간, 상대 남은 시간, 시작 일시, 종료 일시를 포함함.
created_by_user_id는 Hub SQLite tb_user_m.user_id 기준 문자열을 저장함.
opponent_type은 bot, user, official_ai, unknown 계열 값을 사용함.
opponent_ai_level은 Lichess 내장 AI 상대일 때 1부터 8 사이 값을 저장함.
upsert 기준은 game_id임.
7.3 chess.tb_game_move_log_n
로그 PK는 UUID 계열 log_id임.
game_id는 chess.tb_game_m.game_id를 참조함.
turn_number는 로그가 속한 수 순서를 나타냄.
ply_number는 반수 기준 순서를 나타내며 상대 수와 내 수를 동일한 시간축에서 정렬하는 데 사용함.
side는 해당 로그의 주체가 둔 색상을 의미함.
actor_type은 own_bot, opponent, system, lichess 계열 값을 사용함.
event_type은 이동 로그와 시스템 이벤트를 구분함.
fen은 로그 시점의 보드 상태임.
move와 uci는 UCI 이동 문자열을 저장함.
san은 SAN 표기 문자열을 저장할 수 있음.
thought_context는 내 AI 사고 원문이며 상대 수와 시스템 이벤트에서는 비어 있을 수 있음.
own bot move의 thought_context는 sleepzz-bot이 작성한 현재 상황과 착수 근거를 저장함.
board_ascii는 프롬프트 재구성 가능한 8x8 ASCII 보드 문자열이며 이벤트 종류에 따라 비어 있을 수 있음.
pgn은 로그 시점까지의 전체 PGN 문자열이며 이벤트 종류에 따라 비어 있을 수 있음.
my_remaining_time_ms, opponent_remaining_time_ms는 로그 시점의 남은 시간을 저장함.
commentary_latency_ms는 own bot move의 sleepzz-bot 착수와 thought 생성 지연 시간을 밀리초 단위로 저장함.
event_payload는 Lichess 원본 이벤트 일부, legal moves, 상태 payload, 오류 세부정보처럼 정형화가 덜 된 값을 JSONB로 보존함.
own bot move의 event_payload는 source, legal_moves, engine, commentary_status, commentary_usage_log_id, choice, invalid_moves, fallback_reason, ai_error_message 같은 보조 정보를 포함할 수 있음.
event_payload.engine은 name=sleepzz-bot, model=CHESS_MOVE 같은 착수 생성 정보를 포함할 수 있음.
created_at은 KST 기준 timezone-aware timestamp임.
game_id, ply_number, event_type, actor_type 조합은 ply_number가 있는 이동성 로그의 중복 저장 방지 기준임.
7.4 AsyncSession 사용 규칙
저장소 계층은 shared-core의 get_session_maker()를 사용함.
AsyncSession을 async with 컨텍스트로 열고 commit/rollback 경계를 명확히 유지함.
await session.execute(...) 기반 SQLAlchemy 2 패턴을 사용함.
Lichess stream 처리 중 DB 저장 실패가 발생해도 스트림 전체가 즉시 중단되지 않도록 안전 저장 래퍼에서 warning으로 처리할 수 있음.
8. 시간대 규칙
앱 내부 현재 시각은 now_kst() 기준으로 생성함.
프론트로 송출하는 ISO 문자열은 KST 기준 offset이 포함된 값을 사용함.
DB에 저장하는 시간은 timezone-aware datetime을 사용함.
Lichess 이벤트의 UTC epoch 또는 ISO 값은 Asia/Seoul 기준으로 변환한 뒤 화면과 저장소에 전달함.
Python datetime.utcnow()를 신규 코드에서 사용하지 않음.
9. 인증 및 권한 흐름
체스 샌드박스 화면, JSON API, WebSocket 로그 API는 hub 공통 인증을 사용함.
사용자 인증 JWT는 Authorization: Bearer 헤더 또는 COOKIE_NAME 기준 HttpOnly 쿠키에서 읽음.
JSON API는 shared-core의 require_module("CHESS") 의존성을 사용하며, 일반 사용자 JWT는 hub 내부 인증 API의 세션 유효성 검증을 통과해야 함.
/chess 페이지는 인증되지 않은 요청을 hub-api 공통 로그인으로 보냄.
/chess 페이지의 공통 로그인 진입에는 required_module=CHESS를 사용함.
사용자가 이미 로그인되어 있지만 기존 JWT에 CHESS 권한이 없으면 hub-api 로그인 화면으로 이동하며 permission_denied=1을 통해 권한 없음 alert를 한 번 표시함.
WebSocket /api/chess/ws/logs도 JWT 서명, hub 세션 유효성, allowed_modules의 CHESS 권한을 검증하고 실패 시 policy violation으로 연결을 거부함.
chess-api는 FastAPI 앱 초기화 시 shared-core의 SlidingSessionMiddleware를 등록함.
브라우저 쿠키 기반 HTTP 요청은 hub 내부 인증 API를 통해 세션 갱신 필요 여부를 확인함.
세션 갱신 판단과 새 JWT 발급은 hub-api의 POST /api/internal/auth/refresh가 담당함.
WebSocket 연결은 초기 연결 시점의 JWT 검증과 권한 검증을 수행하며, 슬라이딩 세션 쿠키 재발급은 HTTP 응답 기반 요청에서 처리됨.
Lichess API 인증은 사용자 JWT와 분리된 LICHESS_BOT_TOKEN을 사용함.
10. 운영 제약
Lichess Bot API는 해당 계정이 bot 계정으로 등록되어 있어야 정상 동작함.
Lichess 내장 AI는 이름 있는 username challenge가 아니라 /api/challenge/ai와 level 파라미터를 사용함.
일반 bot 계정 또는 일반 유저 계정은 /api/challenge/{username}을 사용함.
sleepzz-bot 응답의 choice는 legal moves choice 목록 범위 안에 있어야 하며, 앱 계층은 매핑된 UCI move의 legal moves 포함 여부를 항상 검증함.
choice 범위 오류, 매핑 실패, 합법 수 검증 실패가 발생하면 같은 턴에서 1회 재시도한 뒤 fallback 합법 수를 제출함.
체스 thought는 사용자 관전 로그이자 향후 학습/RAG 대상이므로 저장 시 임의로 잘라내지 않음.
실시간 대국 지연을 줄이기 위해 체스 착수 프롬프트는 짧은 두 줄 thought와 작은 output token 예산을 전제로 함.
sleepzz-bot 호출 지연은 실제 Lichess move 제출 지연으로 이어질 수 있으므로 timeout과 프롬프트 길이를 보수적으로 유지해야 함.
Lichess move 제출은 sleepzz-bot 착수 생성, 앱 계층 합법 수 검증, 또는 fallback 합법 수 선택 이후에만 수행함.
board_ascii는 사람이 읽기 쉬운 rank/file 포함 포맷을 유지해야 함.
11. 관련 문서
프로젝트 전체 구조는 docs/junkbox/common/guide-jb-core.md를 따름.
체스 프런트 구조는 docs/junkbox/apps/guide-apps-chess-ui.md를 따름.
공통 AI 런타임은 docs/junkbox/libs/guide-lib-shared-ai.md를 따름.
공통 설정, DB, 로깅은 docs/junkbox/libs/guide-lib-shared-core.md를 따름.
공통 UI 자산은 docs/junkbox/libs/guide-lib-shared-ui.md를 따름.