ROI · P9 · 대형 독립 작업 — 임팩트 높음(SSOT/무결성) / 노력 L(1 PR 다층)
매치 로직 백엔드 이관. 규모가 커 단독 PR·계획으로 마지막.
왜
- 매치 진행 로직(페어링·셔플·부전승·다음매치 계산)이 프론트에만 있고, 서버는 탈락 여부·소속·라운드만 검증한다. "누가 누구와 붙나"가 100% 프론트 신뢰라 서버가 브래킷 무결성을 보장하지 못한다 (SSOT 아님).
- 셔플이 클라 랜덤이라 같은 토너먼트도 사용자마다 페어 순서가 다르고, 서버가 진행 순서를 추적·복원할 수 없다.
- web·app·향후 플랫폼이 각자 알고리즘을 재구현해야 한다.
무엇을
서버가 tournament_histories 에서 매치를 파생해 내려주고, 프론트는 그리기만 한다. 스키마 변경 0 (전부 파생).
| 지금 프론트가 하는 일 |
이관 후 서버 |
| 페어 정하기 (가격순 정렬 + 인접) |
서버가 페어 정해 currentMatch 로 내려줌 |
| 셔플 (매번 랜덤) |
결정론 시드(tournamentUserId+round) — 새로고침해도 같은 페어 |
| 부전승 (홀수 시 마지막 아이템) |
시드 랜덤(중립, 가격 편향 없음)으로 서버가 선정 |
| 다음 매치 계산 (낙관 remainingItems 조작) |
POST /matches 응답에 nextMatch (결승 아니어도) |
| 라운드 끝 감지 후 refetch |
nextMatch 가 다음 라운드 첫 페어로 자동 |
| 준결승/결승 연출 판정 |
서버가 주는 currentRound 로 FE 파생 (<=4 준결승, ==2 결승) |
새로 생기는 것 (이관의 핵심 가치)
- 페어링 강제 검증 — POST 의
{first,second} 가 서버 지정 페어와 다르면 거부 (브래킷 무결성)
- 멱등 처리 — 같은 매치 재시도 시 409 대신 멱등 성공 응답 (승자 다르면 거부)
transitionStage 필드는 신설 안 함 — currentRound 파생 중복이라 서버는 안 내려줌
결정 사항: 부전승 = 시드 랜덤(중립), 재시도 = 멱등 성공, 응답 = additive(remainingItems 유지, POST 만 래퍼로).
공수 (1 PR, 중간 규모, 마이그레이션 0)
| 작업 |
크기 |
파생 함수 신설 (RoundBracket: 정렬→페어→시드 부전승→시드 셔플→current/next) |
M (본체) |
| 도메인 단위테스트 (부전승·홀수·타이·시드 재현) |
M |
recordMatch (페어 검증 + 응답 확장 + 멱등) |
M |
getTournamentById inProgress 배선 |
S |
| DTO (MatchPair + POST 응답 래퍼) |
S |
컨트롤러 + *Api/*ApiExamples |
S~M |
| 통합테스트 contract |
M |
왜
무엇을
서버가
tournament_histories에서 매치를 파생해 내려주고, 프론트는 그리기만 한다. 스키마 변경 0 (전부 파생).currentMatch로 내려줌tournamentUserId+round) — 새로고침해도 같은 페어POST /matches응답에nextMatch(결승 아니어도)nextMatch가 다음 라운드 첫 페어로 자동currentRound로 FE 파생 (<=4준결승,==2결승)새로 생기는 것 (이관의 핵심 가치)
{first,second}가 서버 지정 페어와 다르면 거부 (브래킷 무결성)transitionStage필드는 신설 안 함 —currentRound파생 중복이라 서버는 안 내려줌결정 사항: 부전승 = 시드 랜덤(중립), 재시도 = 멱등 성공, 응답 = additive(remainingItems 유지, POST 만 래퍼로).
공수 (1 PR, 중간 규모, 마이그레이션 0)
RoundBracket: 정렬→페어→시드 부전승→시드 셔플→current/next)recordMatch(페어 검증 + 응답 확장 + 멱등)getTournamentByIdinProgress 배선*Api/*ApiExamples