Skip to content
RememberIOmPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Ranker

항목을 1대1 또는 3개씩 비교해 개인 선호 순위를 만드는 웹앱입니다. 주제별로 랭킹을 여러 개 만들고, 기준마다 가중치를 줄 수 있습니다.

배포 주소: https://battle-ranker.fly.dev

사용 흐름

  1. 내 랭킹에서 랭킹을 만들고 관리에서 항목과 평가 기준을 추가합니다.
  2. 대결에서 기준마다 승·패·무승부를 고릅니다. 잘 모르는 기준은 건너뜁니다.
  3. 랭킹에서 기준별로 정렬하거나 이름으로 찾습니다. 순위 옆에는 90% 순위 범위가 함께 나옵니다.
  4. 내역에서 마지막 투표를 취소하거나 현재 설정으로 다시 계산합니다.
  5. 관리 → 데이터에서 JSON 백업을 받습니다.

기본값은 블라인드 평가입니다. 고르기 전에는 점수·순위·예상 확률을 숨기고, 설정에서 끌 수 있습니다. 3개 비교에서 ‘모두 A 최고’처럼 최고만 고르면 남은 두 항목의 순서는 정해지지 않으므로, 최하 항목이나 ‘나머지 동률’을 직접 골라야 합니다.

대결 화면을 새로고침하거나 다른 탭에서 열어도 같은 대결이 이어집니다. 한쪽에서 저장하면 다른 탭은 새로고침하라는 안내를 받습니다.

저장과 복구

새 랭킹은 브라우저의 내 랭킹 목록에 들어가며 직접 지우기 전까지 남습니다. 목록마다 복구 코드가 있고, 코드를 입력하면 다른 브라우저에서도 같은 목록을 열 수 있습니다. 코드를 가진 사람은 누구나 목록에 들어올 수 있습니다. 서버에는 코드의 SHA-256 해시만 저장하고, 쿠키는 HttpOnly·SameSite=Strict입니다. HTTPS에서는 COOKIE_SECURE=true를 설정합니다.

랭킹을 모두 지운 목록도 남아 있으므로 같은 복구 코드로 계속 쓸 수 있습니다.

백업 형식은 4입니다. 현재 항목·기준·설정, 기준별 누적 투표 수와 노출 수, 투표 내역을 담습니다. 투표 내역에는 투표할 때의 항목 이름과 기준 이름이 함께 저장되어, 나중에 이름을 바꿔도 내역은 그대로 읽힙니다.

  • 랭킹 하나는 항목 1,500개, 백업 기준 64MiB, 투표 내역 10만 건까지 저장합니다. 한도에 닿으면 저장을 거절하고 내역을 비우라고 안내합니다. 내역 비우기는 상세 내역만 비우고 누적 투표 수는 남기므로 점수와 재계산에는 영향이 없습니다.
  • 백업 불러오기는 파일을 확인해 서버에 잠시 보관하고, 바뀔 내용을 보여준 뒤 한 시간 안에 확인해야 적용됩니다. 그 사이 랭킹이 바뀌면 적용하지 않습니다.
  • 형식 4 파일만 불러올 수 있습니다. 이전 형식을 변환하는 경로는 없습니다.
  • 항목을 지워도 그 항목과의 과거 비교는 상대 점수 계산에 남습니다. 지운 항목의 ID는 다시 쓰지 않습니다.
  • 기준을 지우면 그 기준의 응답도 지워집니다. 항목 삭제나 기준 추가·삭제 전의 내역은 남기되 하나씩 취소할 수는 없습니다. 이름·가중치 변경과 항목 추가는 취소에 영향이 없습니다.
  • 마지막 투표를 취소하면 그 투표를 빼고 전체 점수를 다시 계산합니다. 그 대결에 나오지 않은 항목의 점수와 불확실성도 투표 전 상태로 돌아갑니다.

알고리즘

모델 이름은 **gpl-map-v2**입니다. 기준마다 독립적인 일반화 Plackett–Luce 모델을 쓰며, 1대1 승패·무승부와 3개 항목의 완전 순위·부분 동률·전체 동률을 하나의 우도로 다룹니다. 같은 응답 집합이면 입력 순서와 관계없이 반올림 오차 안에서 같은 결과가 나옵니다. 투표할 때마다 보존된 모든 응답으로 MAP 점수를 다시 계산합니다. 최적화는 정확한 Hessian을 Cholesky 분해로 푸는 감쇠 Newton법으로, 직전 적합에서 출발하며 수렴하지 않으면 저장하지 않습니다.

  • 표시 점수는 μ × 배율 + 중심점이며 μ는 MAP 위치입니다. ±는 결합 Hessian의 Laplace 근사로 구한 표준편차입니다.
  • 1대1 예상 확률은 같은 모델의 결합 불확실성을 수치 적분해 계산합니다. 정확한 사후분포나 실제 사용자 기준의 확률 보정을 보장하지는 않습니다.
  • 랭킹의 순위 범위와 ‘이웃한 두 항목의 순서가 맞을 확률’은 같은 근사에서 1,000개 표본을 뽑아 계산합니다. 항목이 1,000개를 넘으면 표시하지 않습니다.
  • 3개 비교도 기준당 응답 한 건입니다. ‘평가 부족’은 한 기준이라도 답한 대결이 5번 미만이라는 표시일 뿐, 순위가 확정되었다는 뜻은 아닙니다. 건너뛴 기준은 학습에 쓰지 않습니다.
  • 대진은 기준 가중치·응답률·결합 공분산을 반영해 종합 순서의 예상 Brier 위험이 가장 많이 줄어드는 조합을 고릅니다. 최근에 나온 쌍은 가능하면 피합니다. 항목 64개, 후보 512개를 무작위로 뽑아 탐색하므로 최적 대진을 보장하지는 않습니다.

모델은 선호가 변하지 않는다고 가정합니다. 합성 데이터 평가와 한계는 gpl-map-v2 구현과 검증에 있습니다. 실제 사용자를 대상으로 한 정확도와 소요 시간은 아직 측정하지 않았습니다.

개발

Python 3.15, FastAPI, Jinja2, SQLite(aiosqlite), HTMX, Tailwind CSS를 씁니다. 의존성은 uv.lock과 package-lock.json에 고정되어 있습니다.

uv sync --frozen --extra dev
npm ci
npm run build:css
uv run pytest
npm run test:js

로컬 서버는 uv run uvicorn ranker.main:app --reload로 띄웁니다. Docker를 쓰면 docker compose up --build로 서버와 CSS 감시가 함께 뜨고 http://localhost:8080 에서 열립니다.

DB 경로는 DATABASE_PATH로 정합니다. 기본값은 ./data/ranker.db이고, Docker 개발·배포 환경은 /data/ranker.db를 씁니다. 컨테이너 안에서 테스트하려면 docker compose exec app python -m pytest를 실행합니다. 테스트는 임시 DB를 씁니다.

브라우저 테스트:

npx playwright install chromium
npm run build:css
npm run test:browser

Playwright는 임시 DB를 쓰는 로컬 서버를 직접 띄우고, 데스크톱과 모바일 다크 모드에서 실행합니다. 따로 띄운 테스트용 서버를 쓰려면 RANKER_E2E_BASE_URL=http://127.0.0.1:8080을 지정합니다. 이 서버에는 테스트 랭킹이 생깁니다. 설치된 Chrome을 쓰려면 RANKER_CHROME_PATH=/usr/bin/google-chrome을 지정합니다.

uv run ruff check .
uv run ruff format --check .
uv run pip-audit --skip-editable

배포 이미지는 루트에서 docker build -f docker/Dockerfile -t ranker .로 빌드합니다. CI는 PR과 main 푸시에서 Ruff·pip-audit·npm audit·Python·JavaScript·브라우저 테스트를 돌리고, main에서 모두 통과하면 Fly.io에 배포합니다.

의존성은 Dependabot이 직접 의존성·Docker 이미지·GitHub Actions를, lock-maintenance.yml이 lock 파일의 간접 의존성과 flyctl 버전을 매주 갱신합니다. 둘 다 출시 후 3일이 지난 버전만 받고, CI를 통과하면 자동으로 병합되어 배포됩니다. 이를 위해 DEPS_TOKEN(이 저장소의 Contents·Pull requests·Workflows 쓰기 권한을 가진 fine-grained 토큰)을 Actions secret과 Dependabot secret에 모두 등록해야 합니다. Python 마이너 버전과 Node 메이저 버전은 .python-version·CI 설정과 함께 직접 올립니다.

구조

경로 역할
ranker/main.py 앱 구성, 수명주기, 오류 응답, ASGI 미들웨어
ranker/rating_engine.py 저장소와 독립적인 적합·예측
ranker/services.py 표시 변환, 대진 선택, 순위와 순위 범위
ranker/store.py, ranker/database.py 랭킹 상태·투표 내역·백업, 스키마와 트랜잭션
ranker/boards.py, ranker/cookies.py, ranker/deps.py 내 랭킹 목록, 쿠키, 요청 의존성
ranker/routers/ 관리·대결·랭킹·내역·목록 화면
ranker/templates/, ranker/static/ 서버 HTML, 페이지 스크립트, 자체 호스팅 라이브러리·폰트·빌드 CSS
frontend/input.css Tailwind 원본
docker/ 개발·배포 Dockerfile과 진입 스크립트
tests/, tests/browser/ Python·JavaScript 테스트, Playwright 설정과 임시 서버
scripts/, docs/ 알고리즘 평가 도구와 문서

이전 구조 정리의 결정 사항은 2026-09-08 구조 검토 기록에 있습니다.

운영

단일 uvicorn 워커와 SQLite WAL을 씁니다. 세션 락과 DB 커넥션이 프로세스 안에 있으므로 워커를 늘리면 안 됩니다. 랭킹을 바꾸는 요청은 락 안에서 최신 상태를 다시 읽고, 저장할 때 revision을 확인합니다.

  • 일반 요청은 투표 내역을 읽지 않습니다. 내역·취소·백업에서만 SQL로 읽고, 새 투표는 한 행만 추가합니다. 대결 라운드 발급도 한 행만 씁니다.
  • 스크립트·스타일·차트 라이브러리·라틴 글꼴은 직접 호스팅하며, CSP는 인라인 스크립트를 허용하지 않습니다. 한글은 기기 글꼴을 씁니다.
  • 백업 파일 업로드를 제외한 요청 본문은 1MB로 제한합니다. 다른 출처에서 온 쓰기 요청은 거절합니다.
  • /health는 세션을 만들지 않고 상태만 확인합니다. 대결은 JavaScript가 필요하고, 관리 폼은 JavaScript 없이도 제출됩니다.

앱은 시작할 때 현재 스키마의 테이블만 만들고, 이전 스키마를 자동으로 바꾸지 않습니다.

라이선스

AGPL-3.0

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages