Skip to content

feat(auth): 심사 계정 코드 로그인 + 시드 태스크(create·reset) - #389

Merged
selee1012 merged 17 commits into
devfrom
feature/review-login
Oct 31, 2026
Merged

selee1012 merged 17 commits into
devfrom
feature/review-login

Conversation

@selee1012

Copy link
Copy Markdown
Contributor

노션 「🎫 심사 계정 — 테크 스펙 · 공통」 구현. 앱 심사위원이 코드로 로그인하는 심사 계정과, 그 계정·데모 데이터를 심고 되돌리는 일회성 시드 태스크. 마이그레이션 없음, 심사 기능은 기본 꺼짐(REVIEW_LOGIN_ENABLED=false)이라 stg·prod 기동에 영향 없음.

심사 코드 로그인 (1266fb4 ~ 60dfc6a)

  • POST 심사 코드 로그인 — 심사 계정은 일반 유저와 같다(격리·집계 제외 없음). 회차(round)마다 새 계정, 이번 회차 계정만 로그인
  • 스위치 꺼짐·만료 → 코드 세션 끊고 codes 계정 자동 탈퇴(일반 탈퇴 흐름), 다음 코드 로그인 = 탈퇴 복원
  • 심사 코드 계정은 푸시 토큰 저장 안 함, 켠 채 종료 시각이 없으면 기동 중단
  • 시도 제한은 Cloudflare 뒤 실제 클라이언트 IP(CF-Connecting-IP, 엣지 대역일 때만)로 센다
  • 실패 사유·성공 구조화 로그, Swagger 정리

심사 계정 시드 태스크 (b09b647 ~ 4cc1624)

APP_RUNTIME_ROLE=review_seed + command --mode=create --accounts=N --evaluation-end=YYYY-MM-DD / --mode=reset [--confirm-round=R]. 종료 코드 0 성공 · 2 커밋됐으나 후속(통계·탐색 색인) 미완 · 1 실패. 코드 평문·해시는 다루지 않고 round·UUID·건수만 출력.

  • create: round 발급 + 새 UUID 계정 + 데모 방 2개(수동 독서·자동 만 보)와 4주 이력·점수·알림. 더미(ruleup-demo-NN)는 회차 무관 재사용, 더미도 정산(안 하면 1분 점수 보정 배치가 정산해 버림)
  • reset: 이번 회차 계정만 시드 상태로. 방 잠금 뒤 재탐색으로 리셋 중 실제 유저 가입 경합 차단, 실제 유저가 있던 방은 남기고 심사 계정·더미만 내보냄
  • reset 은 리셋 전 미처리 아웃박스 중 리셋이 지우는 원본에서 나온 사건(점수·티어·감시자 실패 통지·이의 남용 표본)만 표식 남겨 닫음. 제재 나가기·자동 강퇴는 그대로 집행. 사건 종류가 늘면 분류 테스트가 막음

함께 고친 것

  • d945cd9 entrypoint.sh 가 태스크 command 인자("$@")를 java 로 넘기지 않던 문제
  • 83659dc 운영 버그: 회원 탈퇴(사유 VOLUNTARY)로 나간 멤버가 다음 일일 갱신까지 전체 랭킹 집계에 남던 문제
  • 00a570f 탐색 색인 결과를 돌려주는 tryIndex(기존 index 는 Redis 실패를 삼킴)
  • ac546d1 한 방만 외부 사유로 내보내는 leaveExternally(user, room, reason) (기존 루프 몸통 추출)

검증

  • 로컬: 시드 IT 49개, 심사 계정·랭킹·탐색 회로·탈퇴/복원·제재·점수 재생·나가기·아웃박스 관련 스위트 통과
  • 리셋 전 사건 정리는 수정을 끈 음성 실행에서 원장 36→40 으로 실패하는 것 확인

남은 일(이 PR 밖)

  • prod 태스크 정의(REVIEW_LOGIN_*·REVIEW_ACCOUNT_IDS·APP_RUNTIME_ROLE=review_seed) 추가
  • 닉네임 확인·관리자 로그인 시도 제한의 클라이언트 IP 처리(XFF) 정리
  • 공유 더미 원장은 회차·리셋마다 늘어 언젠가 재생 상한(20,000)에 닿을 수 있음 — 소수 회차면 문제없음

🤖 Generated with Claude Code

selee1012 and others added 17 commits October 31, 2026 21:01
노션 「심사 계정 — 테크 스펙 · 공통」 기준. 구현은 다음 커밋.

- 코드 로그인: code 만 받고 기기·설치 정보는 무시, device null, 시드 device_id NULL
- 스위치(enabled·expiresAt)·코드 폐기(revoked)·회차(round = subject 회차) 판정과 기동 검증
- 재발급 차단: 회전 유예보다 먼저 + 발급 뒤 재확인, 거절 시 family 회수 — 끊김 상한 30분
- 종료 스케줄러: 1분 주기·기동 직후, 꺼지면 codes 계정 일반 탈퇴(봇 방장·방 닫힘),
  동시 실행 1회·부분 실패 롤백·재시도
- 푸시 토큰: codes 계정은 저장하지 않고 남은 토큰을 내림
- 다음 회차는 새 계정 — 지난 회차 계정은 코드가 맞아도 열리지 않음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/v1/auth/review-login — 코드만 받아 이번 회차 심사 계정으로 로그인한다.

- 판정(ReviewAccountPolicy): 스위치(enabled·expiresAt, 요청마다 비교)·코드 대조(정규화 SHA-256)·
  폐기(revoked)·회차(oauth_subject 의 round). 매핑·round·기간 오류는 기동 중단
- 순서: 꺼짐 404(본문 없음) → IP 실패 10회/10분 429 + Retry-After → 불일치 400 INVALID_REVIEW_CODE →
  행 잠금·회차 확인·정지 403·탈퇴 복원 → 발급 → 스위치 재확인(꺼졌으면 롤백 후 404)
- 기기·설치 정보는 받지 않는다(device null, device_id NULL 유지, 단일 기기 revoke·설치 인계 없음)
- 429 응답에 retryAfterSeconds 가 있으면 Retry-After 헤더도 싣는다(공통 핸들러)
- 설정 기본값은 꺼짐(REVIEW_LOGIN_* 환경변수)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 재발급: 코드 세션이 끝났으면(꺼짐·코드 폐기) 회전 유예보다 먼저 거절, 발급 뒤 한 번 더 확인.
  거절 시 family 회수를 커밋하고 401 SESSION_EXPIRED — 끊김 상한 = 꺼진 시각 + AT 수명
- 종료 스케줄러(ReviewSessionTerminator): 기동 직후·1분 주기, ShedLock 없음.
  꺼지면 codes 계정을 일반 탈퇴 흐름으로 탈퇴(계정별 트랜잭션 + 행 FOR UPDATE 후 재확인 → 겹쳐 돌아도 1회,
  한 계정 실패는 그 계정만 롤백·다음 실행 재시도). 켜져 있으면 폐기 코드 계정의 RT 만 회수
- 탈퇴 확인 문구 상수를 공개해 스케줄러가 같은 값을 쓴다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
등록은 200 으로 받되 저장하지 않고, 그 계정에 남은 활성 토큰을 내린다 — 같은 기기에서 쓰던
심사위원 본인 계정의 토큰이 심사 계정으로 넘어가지 않는다. Play 심사 계정·일반 유저는 기존대로.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uniq 앞 6자리(나노초 상위 자리)는 1초 동안 같아 전체 실행에서 닉네임 중복 409 가 났다.
뒤쪽(나노초 하위 자리 + 순번) 8자리를 쓴다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- review-login: 성공 응답 차이(device null·restored), 기기 정책 미적용, 400·403·429 예시, 꺼짐 404(본문 없음),
  429 Retry-After 헤더
- 재발급·푸시 토큰 등록에 심사 코드 계정 동작을 적는다
- 에러 봉투 스키마에 retryAfterSeconds 를 추가하고, @operation 에 선언한 응답 헤더가 에러 예시로 덮여 사라지지 않게 한다
- OpenApiDocsIT 에 경로·예시·헤더·본문 없는 404 검사를 더한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
expiresAt 이 없으면 꺼짐으로 판정돼 종료 스케줄러가 기동 직후 코드 계정을 탈퇴시켰다 — 준비한 방에서
나가고 방장이 봇으로 바뀐다. enabled=true + codes + expiresAt 없음이면 기동 중단.
enabled=false 는 막지 않는다(긴급 차단 재배포가 기동 실패로 막히면 안 된다).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영은 forward-headers-strategy=framework 라 ForwardedHeaderFilter 가 X-Forwarded-For 를 숨기고 접속 주소를
그 첫 값(클라이언트가 지어낼 수 있는 값)으로 바꾼다 — 앞쪽을 바꾸면 한도를 피할 수 있었다. MockMvc 는 이 필터를
거치지 않아 테스트가 통과했다.

- 래퍼를 벗겨 원본 헤더를 읽는다
- ALB 가 끝에 붙인 주소가 Cloudflare 대역(공식 ips-v4/v6)이면 CF-Connecting-IP, 아니면 그 주소(직접 접근).
  ALB 가 직접 접근도 받으므로(SG 0.0.0.0/0) 엣지를 거치지 않은 CF-Connecting-IP 는 믿지 않는다
- 운영 필터를 세운 MockMvc 통합 테스트와 ForwardedHeaderFilter 를 거친 단위 테스트를 더한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
실패는 review_login_failed reason=TOO_MANY_ATTEMPTS · CODE_MISMATCH · ACCOUNT_MISSING ·
EARLIER_REVIEW_ACCOUNT · ACCOUNT_BANNED · SWITCHED_OFF_DURING_LOGIN, 성공은
review_login_succeeded label · userId · ip · restored. 입력값 원문은 남기지 않는다(5-7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
구현 전 테스트(5-8 「모드별 규칙은 테스트를 먼저 작성」). 아직 빨간 상태다.
- create: round 를 실행일+순번으로 발급 · 같은 날 순번 증가 · 99 소진 실패 ·
  같은 ID / 같은 round subject 가 있으면 전체 롤백 · 출력은 round·UUID 뿐(코드·해시 없음) ·
  계정 행 구성(5-3) · 데모 방 2개(수동 그룹 · 걸음 자동 그룹, 방장은 더미) · 4주 이력 ·
  이의 인용 · 스트릭 · 정산 로직 점수 · 샘플 알림 · 감시자 없음 · 더미 재사용
- reset: 설정의 round 로만 돈다 · 확인 round 불일치·만료·스위치 꺼짐·회차 섞임·대상 없음이면
  아무것도 바꾸지 않고 실패 · 잠금·참여·점수·개별 동의·탈퇴·기기 초기화 · RT·더미·일반 유저·
  다른 회차 그대로 · 실제 유저가 들어온 방은 봇 방장으로 남김
- 인자: 어느 모드도 round 를 고르는 인자를 받지 않는다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 코드 모양 검사 전에 UUID 를 지운다 — UUID 의 마지막 12자리·중간 4-4-4 가 코드 모양과 같아
  정상 출력에서도 실패했다
- 다른 회차 불변은 그 회차 심사 계정과 그 회차 방 안의 데이터로 좁힌다 — 더미는 회차 간 공유라
  더미의 다른 방 참여·전역 점수는 이번 회차 리셋으로 바뀐다. 더미 점수는 재생 일치로 본다
- 리셋 도중 실패: 계약상 반드시 일어나는 쓰기 6곳(푸시 토큰·개별 동의 삭제, 탈퇴 복원, 새 데모 방,
  새 이력, 점수 원장)을 루트 연결의 트리거로 하나씩 막고 전체가 되돌아가는지 본다
- 실제 유저가 방장인 방에 심사 계정이 참여한 경우 — 리셋 뒤에도 실제 유저가 방장이고 방이 남는다
- 픽스처 insert 뒤 참여 인원 수를 실제 멤버 수로 맞춘다
- 스트릭은 날짜가 하루씩 이어져야 센다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 리셋 예외의 원인 사슬에 트리거 메시지가 있어야 통과한다 — 사전 검사나 다른 버그로 쓰기 전에
  실패하면 롤백을 검증한 것이 아니다
- 실패 스냅숏에 더미(어느 더미든)·대상 계정이 방장인 방, 그 방의 탐색 투영 행, 계정 단위 가입 사건을
  더한다 — 기존 방 ID 로만 보면 새 데모 방만 남은 실패를 놓친다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ENTRYPOINT 가 sh entrypoint.sh 라 ECS 태스크 정의의 command 는 스크립트 인자로 붙는다.
스크립트가 "$@" 를 넘기지 않아 일회성 태스크 인자(예: 심사 계정 시드 --mode=create)가 사라졌다.
OTEL 켜짐·꺼짐 두 exec 줄 모두 넘긴다 — 인자가 없으면 동작은 그대로다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
회원 탈퇴의 방 정리(leaveAllForWithdrawal)는 외부 나가기 사유 VOLUNTARY 로 통계 이벤트를 내는데,
전체 랭킹의 이탈 반영(removeDeparted)이 받는 사유 목록에 VOLUNTARY 가 없었다. 그래서 탈퇴한 회원의
참여·성공 수가 다음 일일 갱신까지 방 집계에 남아 남은 사람이 보는 순위가 틀어졌다.

- 사유 목록에 VOLUNTARY 를 더한다(심사 계정 리셋의 외부 나가기도 같은 사유다)
- 탈퇴로 나간 멤버가 스냅숏 집계에서 바로 빠지는지 테스트를 더한다
- 스냅숏 고정 테스트는 목록 첫 페이지 대신 myChallenge 로 본다 — 같은 DB 의 다른 테스트가 남긴
  성공률 높은 방이 많으면 첫 50개 밖으로 밀려 순서에 따라 실패했다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
index() 는 가입 경로가 Redis 때문에 실패하지 않게 실패를 삼킨다. 결과를 보고해야 하는 일회성 작업
(심사 계정 시드)은 그 경로로는 색인이 빠져도 성공으로 보게 된다.

- ExploreCircuitBreaker.tryCall: 실패·회로 열림이면 false, Redis 를 끈 환경(enabled=false)은 할 일이 없어 true
- ExploreIndexer.tryIndex: index() 와 같되 결과를 돌려준다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
leaveAllExternally 의 방 하나 몫(마지막 멤버면 방 닫기·방장이면 봇방장·음소거 정리·통계 이벤트)을
leaveLocked 로 뽑고, 방 행을 잠근 뒤 그 한 방만 처리하는 leaveExternally(user, room, reason) 를 연다.
심사 계정 시드 리셋이 실제 유저가 남는 방에서 심사 계정·더미만 뺄 때 쓴다. 기존 동작은 그대로다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
심사 계정 테크 스펙 5-3·5-7·5-8. app.runtime.role=review_seed 로 띄우면 인자를 읽어 한 번 돌고 끝난다
(종료 코드 0 성공 · 2 커밋됐으나 후속 미완 · 1 실패). 코드 평문·해시는 다루지 않고 round·UUID·건수만 출력한다.

create — round 를 실행일(KST)+순번으로 발급하고 새 UUID 심사 계정, 데모 방 2개(수동 독서·자동 만 보),
4주치 판정·실패 상세·이의 인용·점수·알림을 만든다. 방장은 더미(ruleup-demo-NN, 회차 무관 재사용).
더미도 같은 정산을 거친다 — 빼면 1분 점수 보정 배치가 원장 없는 확정 이력을 곧 정산한다.

reset — 서버와 같은 설정의 round·codes 로 이번 회차 계정만 시드 상태로 되돌린다.
- 대상 방은 트랜잭션 밖에서 찾고, 가입 경로와 같은 방 행 잠금 뒤 다시 찾아 늘었으면 중단한다
  (리셋 중 실제 유저 가입 경합). 실제 유저가 있던 방은 지우지 않고 심사 계정·더미만 내보낸다
- 판정·이의·원장·점수 상태·알림함을 지우고 다시 만든다. 요약 행은 버전을 이어 둔다
- 리셋 전 미처리 아웃박스 중 리셋이 지우는 원본에서 나온 사건(점수 입력·감점·이의 정정·티어 고지·
  티어 게이트·주기 결과·감시자 실패 통지·이의 남용 표본)은 표식을 남겨 닫는다. 제재 나가기·자동 강퇴는
  그대로 집행된다. 새 사건 종류는 둘 중 하나로 분류해야 테스트가 통과한다

커밋 뒤 통계·탐색 색인은 best-effort 로 하고(실패하면 종료 2, 5분 보정·탐색 대조가 다시 맞춘다),
아웃박스는 상시 API 서비스의 디스패처가 흘린다.

테스트 픽스처의 더미 subject 는 ruleup-demo-fixture 로 바꾼다 — 시드 테스트가 먼저 돌면 ruleup-demo-01 이
다른 UUID 로 이미 있어 uq_users_oauth_identity 에 걸렸다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Test Coverage

Overall Project 84.5% -0.42% 🍏
Files changed 91.41% 🍏

File Coverage
RuntimeRole.java 100% 🍏
ReviewSeedException.java 100% 🍏
ReviewLoginProperties.java 100% 🍏
ReviewLoginCode.java 100% 🍏
SecurityConfig.java 100% 🍏
ErrorCode.java 100% 🍏
CrossRankingSnapshotService.java 100% 🍏
OAuthLoginResponse.java 99.56% 🍏
ApiErrorResponseCustomizer.java 98.95% 🍏
ReviewLoginAttemptLimiter.java 98.08% -1.92% 🍏
ExploreIndexer.java 96.37% -0.49% ❌
ReviewLoginController.java 95.24% -4.76% 🍏
ReviewSeedDemo.java 95.06% -4.94% 🍏
UserAccountService.java 94.95% 🍏
DeviceTokenService.java 94.24% 🍏
ReviewSeedService.java 93.14% -6.86% 🍏
ReviewAccountPolicy.java 92.6% -7.4% 🍏
ReviewSessionTerminator.java 92.35% -7.65% 🍏
ReviewSeedCommand.java 91.36% -8.64% 🍏
User.java 90.69% 🍏
ChallengeMemberService.java 90.55% -0.39% 🍏
CloudflareRanges.java 90.07% -9.93% 🍏
ReviewCodes.java 89.09% -10.91% 🍏
ReviewLoginService.java 81.82% -18.18% 🍏
AuthService.java 79.25% -0.24% 🍏
AuthController.java 71.43% 🍏
ExploreCircuitBreaker.java 68.54% 🍏
GlobalExceptionHandler.java 62.12% 🍏
DeviceController.java 57.14% 🍏
ReviewSeedTaskRunner.java 0% ❌

@selee1012
selee1012 merged commit dd127c4 into dev Oct 31, 2026
3 checks passed
@selee1012
selee1012 deleted the feature/review-login branch October 31, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant