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_entriesactive-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).InterceptorConfigexclude 보정 — 정적 자산 경로 인터셉터 제외.JamStatusenum(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_userUNIQUE(멱등 가드).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_statsVIEW 는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_statsVIEW 소비. 신규 매퍼 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_voterUNIQUE(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.jsp2지점 게이트. 진행 중에는 "평가 종료 후 공개" 안내. - 미로그인 vote/cancelVote/mine 401, results 는 공개 200.
JamVotesMapper.listCountsByJamaliasAS "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, 통합/카운트 표현 차이만(기능 결함 아님). |