본문으로 건너뛰기

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-api18002 포트 기준 백엔드로 실행할 수 있음.
  • apps/chess-ui15002 포트 기준 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_charsCHESS_MOVE 프롬프트에 포함할 PGN 최대 문자 수이며 기본값은 160자임.
  • commentary.max_board_ascii_charsCHESS_MOVE 프롬프트에 포함할 board ASCII 최대 문자 수이며 기본값은 120자임.

2.5 공통 로깅 초기화

  • 앱 import 시점에 shared-coresetup_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

  • GET /api/chess/ws/logs

3.3 웹 화면

  • GET /chess

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}로 게임 스트림을 연결함.
  • 게임 스트림은 gameFullgameState 이벤트를 처리함.
  • 스트림이 끊기거나 정상 종료되면 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-apishared-aiGraphAiRunner를 사용함.
  • graph key는 CHESS_MOVE임.
  • route key도 CHESS_MOVE임.
  • provider 값은 litellm을 사용함.
  • 기본 모델은 openrouter/qwen/qwen-2.5-7b-instruct임.
  • 체스 착수 AI 호출은 LITELLM_BASE_URLLITELLM_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_typeown_bot, opponent, system, lichess 계열 값을 사용함.
  • event_typemove, 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_statuscompleted, 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를 포함함.
  • levelinfo, 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_typebot, 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_idchess.tb_game_m.game_id를 참조함.
  • turn_number는 로그가 속한 수 순서를 나타냄.
  • ply_number는 반수 기준 순서를 나타내며 상대 수와 내 수를 동일한 시간축에서 정렬하는 데 사용함.
  • side는 해당 로그의 주체가 둔 색상을 의미함.
  • actor_typeown_bot, opponent, system, lichess 계열 값을 사용함.
  • event_type은 이동 로그와 시스템 이벤트를 구분함.
  • fen은 로그 시점의 보드 상태임.
  • moveuci는 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_payloadsource, legal_moves, engine, commentary_status, commentary_usage_log_id, choice, invalid_moves, fallback_reason, ai_error_message 같은 보조 정보를 포함할 수 있음.
  • event_payload.enginename=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-coreget_session_maker()를 사용함.
  • AsyncSessionasync 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-corerequire_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_modulesCHESS 권한을 검증하고 실패 시 policy violation으로 연결을 거부함.
  • chess-api는 FastAPI 앱 초기화 시 shared-coreSlidingSessionMiddleware를 등록함.
  • 브라우저 쿠키 기반 HTTP 요청은 hub 내부 인증 API를 통해 세션 갱신 필요 여부를 확인함.
  • 세션 갱신 판단과 새 JWT 발급은 hub-apiPOST /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를 따름.