docs(jam): W2 게임잼 변경 이력 기록 + 설계 구현추적 갱신

- docs/changes/2026-06-24-w2-jam-platform.md 신규 — W2-1~6 전체 변경(테이블·컨트롤러·게이트·보안·검증 190/190 + 커밋 ccf1e42~a74bf74)
- docs/changes/index.md 링크 추가
- work-log 풀설계 요약에 W2 구현 완료 추적 한 줄 + 교차 링크

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
이정수 2026-06-24 14:19:14 +09:00
parent a74bf74d13
commit b222b0106c
3 changed files with 183 additions and 0 deletions

View File

@ -0,0 +1,177 @@
---
kind: change
title: "W2 게임잼 워크스트림 구현 (W2-1~6)"
session_id: 20260624-100749
resumed_from: 20260623-104307
created_at: 2026-06-24
status: implemented
related_design: "../../.atp/work-session/20260623-104307/implementation/"
related_impl: "../../.atp/work-session/20260624-100749/implementation/"
related_verification: "../../.atp/work-session/20260624-100749/verification.md"
related_report: "../../.atp/work-session/20260624-100749/report.md"
related_design_summary: "../work-log/2026-06-23-w2-w4-full-design-summary.md"
related_security_checklist: "../security/security-remediation-checklist.md"
---
# W2 게임잼 워크스트림 구현 변경 이력 (W2-1~6)
상위 설계 인덱스: [W2+W3(잔여)+W4 풀설계 통합 요약](../work-log/2026-06-23-w2-w4-full-design-summary.md) §설계 인덱스 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, 통합/카운트 표현 차이만(기능 결함 아님). |

View File

@ -6,3 +6,4 @@
- [2026-06-18-w3-2-comments-reviews.md](./2026-06-18-w3-2-comments-reviews.md) — W3-2 댓글/리뷰 분리 구현. game_comments 서버 영속화 전환 + game_reviews 도메인 신설. 신규 API 9개(댓글 C1~C4, 리뷰 R1~R5), DDL 2종, 권한(작성자/운영자), cascade 확장. L1 31테스트 PASS. L3 스모크·DDL 적용은 needs_user_verification. 좋아요는 범위밖.
- [2026-06-22-w3-2-comments-reviews-enhancement.md](./2026-06-22-w3-2-comments-reviews-enhancement.md) — W3-2 고도화(코어 위에 얹음). 다축 평점(game_review_axes 6축·육각형 SVG 레이더) + A1~A3 일관성(commentView 통일·작성자 마스킹 QG-2 해결·edited/updatedAt) + B1~B3 목록규모(페이지네이션·본문10자·TextNormalizer) + C1~C6 UX + game_review_stats 집계뷰(C3 재분류 — W2-3 동결 무관 확정). 신규 4파일+변경 13. L1 43/43 GREEN. DDL·L3 스모크 needs_user_verification.
- [2026-06-24-w2-jam-platform.md](./2026-06-24-w2-jam-platform.md) — W2 게임잼 워크스트림 전체(W2-1~6) 구현. 잼 엔티티/라이프사이클(jams/jam_teams/jam_team_members/jam_entries/jam_status_log) + 심사위원 역할(jam_judges 잼스코프) + 평가 동결 스키마(jam_criteria/jam_scores/jam_votes/jam_awards + jam_score_stats VIEW, 평가단위 (jam_id,game_id) 활성 자연키) + 심사 평가(3중게이트 UPSERT 가중집계) + 인기투표(1인1표 UNIQUE·종료후 공개) + 시상 집계(3트랙+가중 GRAND·CLOSED 확정 멱등). GAME_JAM_MANAGE 첫 enforcement 연결·잼스코프 isJudge·평가기간 게이트·CSRF 전수. 6 커밋 ccf1e42~a74bf74. 최종 L1 190/190 GREEN(회귀 0), L2 dev DB contract 전 PASS(격리 throwaway DB). dev DB 마이그레이션·L3 스모크 needs_user_verification.

View File

@ -85,3 +85,8 @@ owner: art
## 다음 단계
설계 합의 후 착수 순서대로 implementation-advisor 진입(코드·마이그레이션). 착수 기능의 concerns(구현 점검) 를 verification-strategies L레벨로 통과시킨다. **이 세션은 설계까지 — 코드 0줄.**
## 구현 추적
- **W2 (W2-1~6) 구현 완료** — 2026-06-24, commit `ccf1e42`(W2-1)~`a74bf74`(W2-6), 6 feat. 최종 L1 190/190 GREEN(회귀 0) + L2 dev DB contract 전 PASS(격리 throwaway DB). 변경 이력: [changes/2026-06-24-w2-jam-platform.md](../changes/2026-06-24-w2-jam-platform.md). dev DB 마이그레이션·L3 스모크는 needs_user_verification.
- W3(잔여)·W4 는 설계만 산출(미구현).