bibimbap/docs/changes/2026-06-24-w2-jam-platform.md

12 KiB

kind title session_id resumed_from created_at status related_design related_impl related_verification related_report related_design_summary related_security_checklist
change W2 게임잼 워크스트림 구현 (W2-1~6) 20260624-100749 20260623-104307 2026-06-24 implemented ../../.atp/work-session/20260623-104307/implementation/ ../../.atp/work-session/20260624-100749/implementation/ ../../.atp/work-session/20260624-100749/verification.md ../../.atp/work-session/20260624-100749/report.md ../work-log/2026-06-23-w2-w4-full-design-summary.md ../security/security-remediation-checklist.md

W2 게임잼 워크스트림 구현 변경 이력 (W2-1~6)

상위 설계 인덱스: W2+W3(잔여)+W4 풀설계 통합 요약 §설계 인덱스 W2-1~6. 설계 본문: .atp/work-session/20260623-104307/implementation/W2-*.md · 구현 보고: .atp/work-session/20260624-100749/implementation/W2-*-impl.md · 검증: .atp/work-session/20260624-100749/verification.md.

개요

W1(거버넌스/RBAC) 위에 게임잼 플랫폼 전체를 6개 서브 워크스트림으로 구현했다. 잼 엔티티/라이프사이클부터 심사위원 역할·동결 평가 스키마·심사 평가·인기 투표·시상 집계까지 잼 1회차의 전 수명주기를 커버한다. 평가 스키마(W2-3)는 ⚠️클러스터 권위로 단일 동결 후 하류(W2-4/5/6)가 소비하는 구조다.

서브 커밋 핵심
W2-1 잼 엔티티/라이프사이클 ccf1e42 jams/jam_teams/jam_team_members/jam_entries/jam_status_log + JamController/JamAdminController + InterceptorConfig exclude + GAME_JAM_MANAGE enforcement 연결
W2-2 심사위원 역할 71b6b6f jam_judges(잼 스코프) + JamRoleGate(isJudge) + 자기출품 충돌 isOwnEntry
W2-3 잼 평가 동결 스키마 ⚠️권위 2d5601b jam_criteria/jam_scores/jam_votes/jam_awards + jam_score_stats VIEW. 평가단위 = (jam_id, game_id) 활성 자연키
W2-4 심사위원 평가 09ed6bc 3중게이트(isJudge·평가기간·자기출품) + criterion UPSERT + 가중집계
W2-5 인기투표 5ed06d9 jam_votes 1인1표 UNIQUE + 평가기간 게이트 + 종료후 공개(밴드왜건 회피)
W2-6 시상 집계 a74bf74 3트랙(JUDGE/USER_RATING/POPULAR) 개별수상 + 가중 GRAND + CLOSED 확정 멱등

커밋 범위: ccf1e42(W2-1) → 71b6b6f(W2-2) → 2d5601b(W2-3) → 09ed6bc(W2-4) → 5ed06d9(W2-5) → a74bf74(W2-6). 모두 feat/v2 브랜치.


W2-1 — 잼 엔티티 / 라이프사이클 (ccf1e42)

DDL (신규 5테이블)

테이블 역할 핵심 제약
jams 잼 회차 본체 + 상태/일정 jams_status_check IN(RECRUIT, DEV, EVAL, CLOSED), slug 활성 UNIQUE(ux_jams_slug_active), keyset 인덱스 idx_jams_visible_keyset
jam_teams 팀 출품 컨테이너 jam 스코프
jam_team_members 팀 멤버
jam_entries 출품 조인테이블(다회차·정규화) 개인/팀 XOR(jam_entries_entrant_xor_check + jam_entries_entrant_type_check), 활성 UNIQUE(ux_jam_entries_jam_game_active, partial WHERE is_delete IS NOT TRUE)
jam_status_log 상태 전이 감사 jam_status_log_to_status_check IN 4값(jams 동기)
  • 평가 단위 = (jam_id, game_id) 활성 자연키. jam_entries active-UNIQUE 가 1:1 보장. games 테이블 무변경(허브 회귀 0).
  • DDL 권위: docs/jam-ddl.sql(멱등) + db/schema.sql 동기(jams/jam_teams/jam_team_members/jam_entries/jam_status_log).

런타임 동작

  • JamController — 잼 조회·출품(개인/팀) 무결성(소유 409/비소유 422/비멤버 422/EVAL 422).
  • JamAdminController — 잼 CRUD + 명시 status 전이 + 전이 감사 적재. 쓰기 5핸들러 전수 requireJamManage 게이트, 읽기 console 은 인라인 gate.has(GAME_JAM_MANAGE).
  • InterceptorConfig exclude 보정 — 정적 자산 경로 인터셉터 제외.
  • JamStatus enum(RECRUIT/DEV/EVAL/CLOSED) = jams CHECK = jam_status_log CHECK 3곳 동기.

W2-2 — 심사위원 역할 (71b6b6f)

DDL (신규 1테이블)

  • jam_judges별도 잼 스코프 테이블(전역 user_permissions RBAC 무오염). FK 3개(jam/user/assigned_by), ux_jam_judges_jam_user UNIQUE(멱등 가드).
  • docs/jam-judge-ddl.sql + db/schema.sql(line 566~594) 동기.

런타임 동작

  • JamRoleGate.isJudge(jamId, userId) — 잼 스코프 심사위원 자격 판정(미지정/미인증 false).
  • JamRoleGate.isOwnEntry — 자기출품 충돌 규칙: 개인출품 true / 팀멤버출품 true / 타인·타팀·비활성 false (OR-EXISTS, USER·TEAM 양경로).
  • 심사위원 지정/해제는 GAME_JAM_MANAGE 게이트. 누구나(USER role 포함) 지정 가능(전역 role 검사 없음), 미존재 userId 404, 재지정 409.

W2-3 — 잼 평가 동결 스키마 ⚠️클러스터 권위 (2d5601b)

W2-4/5/6 착수 전 필수 동결 묶음. Java 코드 0(스키마만), 신규 매퍼/컨트롤러/테스트 0(하류 소관).

DDL (신규 4테이블 + 1 VIEW)

객체 역할 핵심 제약
jam_criteria 잼별 평가 기준(가중치) ux_jam_criteria_jam_key UNIQUE, weight >= 0 CHECK
jam_scores 심사위원 점수 ux_jam_scores_jam_game_judge_criterion(1심사위원1기준1점) UNIQUE, score BETWEEN 1 AND 5 CHECK
jam_votes 인기 투표 ux_jam_votes_jam_voter(1인1표) UNIQUE
jam_awards 시상 결과 ux_jam_awards_jam_track_game UNIQUE, jam_awards_track_check IN(JUDGE, USER_RATING, POPULAR, GRAND), rank >= 1 CHECK
jam_score_stats (VIEW) 가중집계 per_criterion CTE 선집계 후 jam_criteria LEFT JOIN (fan-out 가드)
  • 평가단위 = (jam_id, game_id) 활성 자연키(orchestrator 동결 컬럼 + game_review_stats join 정합).
  • 유저평점 트랙 = game_review_stats.avg_rating 단방향 소비(6축은 표시전용, 최소 review_count 임계 미달 제외·NULLS LAST).
  • jam_score_stats VIEW 는 game_review_stats 선례 fan-out 버그를 회피(per_criterion 선집계 + NULLIF(SUM(weight),0) 0-division 가드). 손계산·미채점 제외·NULLIF·fan-out 불변 4축 L2 검증 통과.
  • docs/jam-eval-ddl.sql + db/schema.sql(4테이블 line 608/642/685/721 + VIEW line 765) 동기. 기존 game_reviews/game_review_stats 무변경(ALTER 17건 전부 신규 jam_* 제약추가, DROP 0).

W2-4 — 심사위원 평가 (09ed6bc)

런타임 동작 (W2-3 스키마 소비, 신규 DDL 0)

  • submitScores 진입부 3중게이트 + CSRF: CSRF누락 403 / 미인증 401 / 비-isJudge 403 / 잼없음 404 / 평가기간 외 422 / 출품작없음 404 / 자기출품 422 / 정상 200.
  • criterion별 UPSERT(INSERT ... ON CONFLICT) — 동일 (심사위원, 기준, 평가단위) 재채점 시 update(중복행 0).
  • criterion 화이트리스트(잼 소속 아닌 criterion 422) + score 범위 검증(범위 밖 422).
  • 가중 집계는 jam_score_stats VIEW 소비. 신규 매퍼 SQL ${} 0건, JamScoreStatsMapper camelCase alias 전부 AS "..." 인용(§33 케이스폴딩 회피).

W2-5 — 인기투표 (5ed06d9)

런타임 동작 (W2-3 jam_votes 소비, 신규 DDL 0)

  • vote(POST) / cancelVote(DELETE) 1인1표 토글 — ux_jam_votes_jam_voter UNIQUE(jam_id, voter_user_id) 강제. 재투표 update(중복행 0). game_likes(user_key varchar 비권위) 재활용 안 함, 로그인 user_id 기반.
  • 평가기간 게이트(JamEvalWindow.isOpen) 2지점 — 기간 외 vote/cancelVote 422.
  • 종료후 공개(밴드왜건 회피) — CLOSED 또는 eval_end 경과 시만 count 노출. JamVoteController.results + jam-detail.jsp 2지점 게이트. 진행 중에는 "평가 종료 후 공개" 안내.
  • 미로그인 vote/cancelVote/mine 401, results 는 공개 200.
  • JamVotesMapper.listCountsByJam alias AS "gameId"/"voteCount" 인용.

W2-6 — 시상 집계 (W2 종점, a74bf74)

런타임 동작 (W2-3 jam_awards 소비)

  • 3트랙 개별수상: JUDGE(weightedTotal NULL 제외) / USER_RATING(review_count >= 3 임계·NULLS LAST) / POPULAR(득표 > 0). 미달·NULL 트랙 제외.
  • 가중 GRAND — 가용 트랙만 가중 정규화·결합.
  • CLOSED 게이트: status != CLOSED 시 산정 422. 산정/확정 쓰기 CSRF 403 + GAME_JAM_MANAGE 게이트.
  • 멱등 recompute: deleteByJamTrack → 재insert(@Transactional). 2회 실행 시 jam_awards 중복 0, 결과 동일.
  • JamAwardController 공개 결과 노출. 집계 매퍼(JamReviewRatingMapper/JamVotesMapper/JamScoreStatsMapper) camelCase alias 전부 AS "..." 인용.

보안 (프로젝트 보안 원칙 추적)

W2 에서 신설된 인가/검증 게이트. 모두 단위 + L2 DB 계약 레벨로 검증 완료.

게이트 적용 W 내용
GAME_JAM_MANAGE enforcement W2-1/2/6 W1 선언키의 첫 enforcement 연결(이전 소비처 0). 잼 CRUD·전이·심사위원 지정·시상 산정 쓰기 핸들러 전수 게이트. 무권한 SUBADMIN 403 / 미인증 401.
잼 스코프 isJudge W2-2/4 전역 RBAC 와 분리된 잼 회차 단위 심사위원 자격. 비-isJudge 평가 403.
자기출품 충돌 isOwnEntry W2-2/4 심사위원이 본인/본인팀 출품작 평가 시 422(개인·팀멤버·팀오너 차단).
평가기간 게이트(JamEvalWindow) W2-4/5 EVAL 기간 밖 평가/투표 422.
1인1표 UNIQUE W2-5 jam_votes UNIQUE(jam_id, voter_user_id) DB 레벨 강제.
CSRF 전수 W2-1/2/4/5/6 모든 상태 변경 핸들러 CsrfTokens.isValid → 누락 시 403 + mapper 미호출.
MyBatis ${} 0건 전 W 신규 매퍼 전수 #{} 바인딩, 동적 치환 0.
§33 alias 케이스폴딩 W2-4/5/6 VIEW/집계 매퍼 camelCase alias 큰따옴표 인용(AS "gameId") — Postgres 소문자 폴딩 회피. game_review_stats BUG-2 재발 0.

검증 결과 요약

최종 ./mvnw -o test (W2-6 종점) 190/190 GREEN (Failures 0 / Errors 0 / Skipped 0). 회귀 0.

서브 L1 누적 카운트 L2 dev DB contract 비고
W2-1 97/97 GREEN PASS(격리 throwaway DB) 1차 컴파일 BLOCKER(response() 미정의 24건) → 보정 후 R2 PASS
W2-2 119/119 GREEN PASS(a/b/c 실측) JamRoleGateTest 8 + JamJudgeAdminControllerTest 14
W2-3 119/119 GREEN(회귀 무변경) PASS(DDL/제약거부/VIEW fan-out 가드/alias) Java 0·신규테스트 0 정상
W2-4 148/148 GREEN PASS(UPSERT/alias 폴딩/가중집계) JamScoringControllerTest 19 + JamEvalWindowTest 10
W2-5 171/171 GREEN PASS(UNIQUE 토글/§33 케이스폴딩) JamVoteControllerTest 23
W2-6 190/190 GREEN PASS 4/4(멱등/3트랙 손계산/alias/GRAND) JamAwardServiceTest 7 + JamAwardAdminControllerTest 7 + JamAwardControllerTest 5
  • L2 비파괴 원칙: 모든 L2 contract 는 격리 throwaway DB(생성 → DDL 적용 → 실측 → DROP)로 수행. 사용자 dev(bibimbap) DB 무접촉.
  • 신규 5매퍼(W2-1) 등 전 신규 매퍼 @MockBean 등록 — contextLoads PASS(NoSuchBeanDefinitionException 0).

상세는 .atp/work-session/20260624-100749/verification.md 참조.


미적용 / 이월 (needs_user_verification)

항목 사유
dev DB jam_* 테이블 마이그레이션 L2 는 throwaway DB 로 통과했으나 사용자 dev(bibimbap) DB 에는 jam_* 테이블 미적용(파괴적 조작 회피). 실 dev 런타임(L3) 전 db/apply-local-ddl.sh 별도 수행 필요.
L3 런타임 인가 E2E WAR 기동 후 권한 403·CLOSED 422·종료후 공개 렌더·전이 감사 적재는 자동 미수행. 사용자 스모크 권장.
W2-6 GRAND 가중계수 단위검증 verification-advisor 역할상 구현파일 미열람 — L1 JamAwardServiceTest 7 GREEN 에 위임(L2-4 는 "가용 트랙만 정규화" 원칙만 검증).
테스트 카운트 미세 차이(무해) W2-2 JamJudgeAdminControllerTest 예측 16 vs 실측 14, W2-5 JamVoteControllerTest 예측 24 vs 실측 23 — 전부 GREEN·회귀 0, 통합/카운트 표현 차이만(기능 결함 아님).