Skip to content

feat: LoL Temporal 포지션 선호도 기반 5:5 팀 매칭 알고리즘 구현 (#84)#85

Merged
Sunja-An merged 34 commits intomainfrom
feat/#84-lol-temporal-v2
Apr 4, 2026
Merged

feat: LoL Temporal 포지션 선호도 기반 5:5 팀 매칭 알고리즘 구현 (#84)#85
Sunja-An merged 34 commits intomainfrom
feat/#84-lol-temporal-v2

Conversation

@Sunja-An
Copy link
Copy Markdown
Contributor

@Sunja-An Sunja-An commented Apr 2, 2026

Summary

  • 기존 라인별 2명 고정 배정 방식 → 10명 자유 입력 + 포지션 선호도 반영 알고리즘으로 교체
  • C(10,5)=252 × 5!×5!=14,400 조합 완전 탐색 (≈ 360만 케이스), 3단계 필터링으로 최적 팀 확정
  • 포지션 보정(Model A): 1순위 100% ~ 5순위 75% (순위당 6.25% 선형 감소)

작업 범위

  • DTO: LolTemporalPlayerV2, LolTemporalContestRequestV2, LolTemporalAssignedPlayer, LolTemporalContestResponseV2
  • 에러 코드: ErrLolTemporalInvalidPlayerCount (CT048), ErrLolTemporalInvalidPositions (CT049)
  • 알고리즘: balanceLolTeamsV2 — 조합/순열 생성 + 3단계 필터링 (lol_temporal_balance.go)
  • 서비스: BalanceLolTemporalContestV2 메서드
  • 컨트롤러: 기존 POST /api/contests/lol/temporal 신규 메서드로 교체
  • 단위 테스트: 서비스 9개 + 컨트롤러 8개 추가

알고리즘 상세

3단계 필터링

단계 기준 방향
1 |팀A 보정MMR합 - 팀B 보정MMR합| 최소
2 포지션별 맞대결 편차 합 최소
3 10명 포지션 선호 순위 합 최소

포지션 보정 공식

adjustedMMR = baseMMR × (17 - preferenceRank)   // ×16 스케일, 정수 연산

API 변경

Request POST /api/contests/lol/temporal

{
  "members": [
    { "username": "P1", "tag": "KR1", "rank": "GOLD II", "positions": ["TOP", "JG", "MID", "ADC", "SUP"] },
    ... // 총 10명
  ]
}

Response

{
  "team_a": [{ "username": "P1", "tag": "KR1", "rank": "GOLD II", "position": "TOP", "position_preference": 1 }, ...],
  "team_b": [...]
}

Test plan

  • TestBalanceLolTemporalContestV2_* (서비스 단위 테스트 9개) 전체 통과
  • TestLolTemporalV2Controller_* (컨트롤러 단위 테스트 8개) 전체 통과
  • 기존 TestLolTemporalController_*, TestBalanceLolTemporalContest_* 하위 호환 통과
  • go build ./... 통과

Closes #84

🤖 Generated with Claude Code

Sunja-An and others added 30 commits February 9, 2026 03:52
Add POST /api/contests/:id/thumbnail endpoint that uploads a thumbnail
image to R2 storage and updates the contest's thumbnail URL and
banner_key fields in a single operation. Only contest leaders can upload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

When users click "Deny" on Discord consent screen, the callback receives
an error query parameter instead of a code. Previously this caused a 500
error due to the required binding on code field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ication (#62)

- Replace string comparison with domain method (IsPending)
- Replace silent error swallowing with log.Printf
- Rename ErrContestAlreadyStarted to ErrNotContestLeader
- Add ErrContestStartTimePassed, ErrAlreadyContestMemberExists
- Use typed BusinessError in UpdateContestRequest validation
- Handle duplicate application with 409 Conflict response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
#63)

- Extract OAuth2TokenExchangerPort, DiscordUserInfoPort, OAuth2StatePort interfaces
- Create OAuth2TokenExchangerAdapter for production oauth2.Config wrapping
- Refactor DiscordService to depend on port interfaces for testability
- Add 9 auth service tests (Login, Logout, Refresh)
- Add 7 auth controller tests (httptest)
- Add 5 Discord service tests (Callback, LoginURL)
- Add 5 Discord controller tests (Redirect, Callback scenarios)
- Add 4 auth integration tests (testcontainers: MySQL + Redis)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move OAuth2 state validation to controller layer for all callback paths (error, missing_code, success)
- Fix incorrect error variable usage (err → appErr) in contest application controller
- Update tests to reflect state validation changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: Handle Discord OAuth2 deny button 500 error (#9)
…ntest application

- [feat #64] Calculate Valorant tier point (avg of current/peak) on RequestParticipate
  and propagate stored point to ContestMember on AcceptApplication
- [feat #64] Add Point, CurrentTier, PeakTier fields to SenderSnapshot and SenderResponse
- [feat #64] NewContestMember now accepts a point parameter instead of hardcoding 0
- [feat #64] Wire SetScoreTablePort in cmd/server.go to avoid circular dependency
- [refactor #65] Move getTierPoint logic into ValorantScoreTable.GetTierPoint domain method
- [feat #66] Add ErrValorantRankNotFound (VAL007) and nil-guard for tier data in CalculateContestPoint
- [test #67] Add unit tests for ContestApplicationService point calculation (10 cases)
- [test #67] Add integration tests for end-to-end point persistence with testcontainers
- [test #67] Add unit tests for ValorantScoreTable.GetTierPoint domain method
- [chore #68] Remove K6 load-test infrastructure (replaced by testcontainers integration tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- [HIGH] Propagate score table fetch error instead of silent fallback to 0:
  When contest has GamePointTableId but score table is not found,
  return the error to caller (server-side configuration error)
- [SECURITY] Add nil-check for application before AcceptApplication:
  Return ErrApplicationNotFound when application is nil to prevent
  unauthorized member addition by contest leaders
- [SECURITY] Add status check for application before AcceptApplication:
  Return ErrApplicationNotPending when application is not in PENDING state
- [TEST] Update TestRequestParticipate_ScoreTableNotFound to expect error return
- [TEST] Add TestAcceptApplication_NilApplication_ReturnsError
- [TEST] Add TestAcceptApplication_NonPendingApplication_ReturnsError
- [MEDIUM] Fix confusing test data initialization in integration test:
  Initialize Gold2+Platinum1 applicant with correct value (35) directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t-application

Feat: Valorant tier-based point calculation in contest application (#64, #65, #66, #67, #68)
JWT expiry uses second-precision, so tokens generated within the same
second produce identical strings. Added time.Sleep(time.Second) before
the Refresh call in TestFullLoginFlow to ensure unique timestamps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test: Add unit and integration tests for Auth and Discord OAuth2 login (#63)
- Return error from AcceptApplication when memberRepo.Save fails instead
  of silently logging it, preventing inconsistent state where application
  is ACCEPTED in Redis but member record is absent in DB
- Remove redundant SaveBatch call in startNonTournamentContest that was
  re-creating members with hardcoded point=0; members are already
  persisted with correct points during AcceptApplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix: Member point saved as 0 when contest application is accepted (#71)
…r list (#73)

- Define ValorantRole type (DUELIST, INITIATOR, CONTROLLER, SENTINEL) in domain
- Add ValorantRoles (custom JSON type) and Description to ContestMember entity
- Extend SenderSnapshot and ContestMemberWithUser with new fields
- Accept optional request body in POST /api/contests/:id/applications
- Validate description max 64 chars and role deduplication in service layer
- Include valorant_roles and description in GET /api/contests/:id/members response
- Add DB migration 000025 to add valorant_roles JSON and description VARCHAR(64) columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: Add Valorant roles/description to contest application and member list (#73)
Prometheus + Grafana 기반 모니터링 인프라를 추가한다.

- PrometheusMetrics() Gin 미들웨어 추가 (http_requests_total, http_request_duration_seconds, http_requests_in_flight)
- GET /metrics 엔드포인트 등록 (Prometheus scrape 용)
- docker-compose에 Prometheus(9090), Grafana(3001) 서비스 추가
- Prometheus scrape 설정 (15s interval, app:8080/metrics)
- Grafana datasource / dashboard 프로비저닝 자동화
  - HTTP RPS, 에러율, p50/p95/p99 응답 시간, In-Flight, Goroutine, Heap, GC 패널 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace hardcoded Grafana admin credentials with env vars (${GRAFANA_ADMIN_USER}, ${GRAFANA_ADMIN_PASSWORD})
- Add docker/.env.example as reference for required env vars
- Downgrade Prometheus image from v3.4.0 to stable v2.53.1
- Use defer for httpRequestsInFlight.Dec() to guarantee decrement on panic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: Grafana 모니터링 스택 연동 (#75)
- r2_storage_adapter.go 제거 (R2 커스텀 엔드포인트 방식 삭제)
- s3_storage_adapter.go 추가 (표준 AWS 리전 엔드포인트 사용)
  - StoragePort 인터페이스 동일하게 구현 (Upload/Delete/GetPublicURL)
  - BucketName, Region 누락 시 초기화 실패 처리
- provider.go: R2 → S3 어댑터 초기화로 교체
- env/.env.example: R2 변수 제거, AWS S3 변수 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- s3_storage_adapter.go 제거
- gcs_storage_adapter.go 추가 (cloud.google.com/go/storage v1.61.3)
  - GCS_CREDENTIALS_JSON 설정 시 서비스 계정 JSON 인증
  - 미설정 시 ADC(Application Default Credentials) 자동 폴백
  - GCS_BUCKET_NAME 누락 시 초기화 실패 처리
- provider.go: S3 → GCS 어댑터 초기화로 교체
- env/.env.example: AWS S3 변수 제거, GCS 변수 추가
- go.mod/go.sum: cloud.google.com/go/storage 의존성 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…API (#79)

Implements a type-safe Go package that communicates with the locally
running League of Legends client via the LCU HTTP/WebSocket API.

- lockfile.go: auto-discover lockfile on Windows/macOS/Linux and
  extract port + password
- client.go: http.Client with InsecureSkipVerify (self-signed cert)
  and automatic Basic auth header injection
- session.go: GetGameflowPhase, GetGameflowSession, GetCurrentSummoner
- match.go: GetLastGameID, GetMatchDetail, GetMatchHistory with full
  struct mappings for Game, ParticipantStats, Timeline, etc.
- event.go: WebSocket subscription via gorilla/websocket using WAMP
  protocol; delivers typed Event{Name,Type,URI,Data} to handler func
- process.go: LeagueClient.exe process detection and
  WaitForClientAndConnect helper

Adds github.com/gorilla/websocket v1.5.3 dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- client.go: Add functional options pattern (ClientOption/WithTimeout/WithTLSRootCA)
  for TLS and timeout customisation; store tlsConfig on Client for WebSocket reuse
- client.go: Handle io.ReadAll errors explicitly in Get() and RawGet()
- event.go: Log WebSocket Close() error instead of silently discarding it
- event.go: Share Client.tlsConfig with WebSocket dialer (remove duplicate InsecureSkipVerify)
- event.go: Handle json.Marshal error for WAMP subscribe message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- match.go: Fix Participants field type []Participant → []GameParticipant
  to match the actual LCU match history API response structure
- process.go: Replace shell-out (tasklist/pgrep) with gopsutil/v4/process
  for cross-platform process detection without external command dependency
- gcs_storage_adapter.go: Fix potential double-slash in GetPublicURL by
  trimming trailing slash from publicURL before concatenation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: Add pkg/lcu — Go LCU API client library (#79)
- exception: Add ErrContestStartTimePassed (CT038)
- port: Add Point, CurrentTier, PeakTier fields to SenderSnapshot
  to capture tier info at the time of application
- service: Fix NewContestMember call — pass point from sender snapshot,
  defaulting to 0 if no application snapshot is available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: Resolve build errors introduced by pkg/lcu merge (#79)
Sunja-An and others added 3 commits March 31, 2026 19:03
POST /api/contests/lol/temporal 엔드포인트 추가.
5개 라인(TOP/JG/MID/ADC/SUP)에 각 2명 플레이어 랭크 기반으로
2^5=32 완전 탐색으로 팀 점수 차를 최소화하는 5v5 팀 편성 반환.
DB 저장 없는 순수 연산(Temporal) 처리.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ParseLolRankScore: 전 티어/디비전 + 에러 케이스 (5개)
- BalanceLolTemporalContest: 정상/밸런싱 검증/에러 케이스 (7개)
- HandleLolTemporalContest: HTTP 201/400 케이스 (5개)
- 기존 RequestParticipate 시그니처 변경에 따른 테스트 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 기존 라인별 2명 고정 방식 → 10명 자유 입력 + 포지션 선호도 기반으로 교체
- C(10,5)=252 × 5!×5!=14,400 조합 완전 탐색 (약 360만 케이스)
- 3단계 필터링: 팀 MMR 밸런스 → 라인별 맞대결 편차 → 포지션 만족도
- 포지션 보정(Model A): 1순위 100% ~ 5순위 75% (6.25%씩 선형 감소)
- 새 에러코드: ErrLolTemporalInvalidPlayerCount(CT048), ErrLolTemporalInvalidPositions(CT049)
- 단위 테스트 13개 추가 (서비스 9개, 컨트롤러 8개)
- Swagger 문서 갱신

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several key features: Valorant-related fields (roles, description) for contest applications, point calculation based on Valorant tiers, thumbnail upload functionality for contests using Google Cloud Storage, and a new LoL team balancing algorithm. Additionally, it adds Prometheus metrics for monitoring and includes necessary infrastructure and test updates. My feedback focuses on improving the robustness of the application point calculation, optimizing the performance of the computationally intensive team balancing algorithm, and ensuring proper context management for asynchronous storage operations.

Comment thread internal/contest/application/contest_application_service.go Outdated
Comment thread internal/contest/application/lol_temporal_balance.go
Comment thread internal/contest/application/lol_temporal_balance.go Outdated
Comment thread internal/contest/application/lol_temporal_balance.go Outdated
Comment thread internal/contest/application/contest_service.go
@Sunja-An Sunja-An self-assigned this Apr 4, 2026
@Sunja-An Sunja-An added 🚀 ENHANCEMENT Enhancement 🔨 FEATURE Good for newcomers labels Apr 4, 2026
@Sunja-An Sunja-An linked an issue Apr 4, 2026 that may be closed by this pull request
6 tasks
- AcceptApplication에 application 조회 에러/nil/상태 검증 추가
- UploadThumbnail goroutine에서 context race condition 수정 (oldKey 캡처)
- balanceLolTeamsV2 조합/순열 테이블 패키지 레벨 precompute로 성능 최적화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Sunja-An Sunja-An merged commit 9b7d141 into main Apr 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 ENHANCEMENT Enhancement 🔨 FEATURE Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: LoL Temporal 포지션 선호도 기반 5:5 팀 매칭 알고리즘 구현

1 participant