# 교차정합 감사 — W2 결합 클러스터 + W3/W4 cross-ref (설계 전용) > **HIGH 편차: 1건.** 동결 단일권위(W2-3) **유지됨** — jam_scores/jam_votes/jam_awards/jam_score_stats VIEW 의 평가단위·컬럼·트랙·게이트 계약을 하류 W2-4/5/6 이 재정의 없이 소비. 단 W2-1 본문이 동결 전 표현(jam_entries.id)을 그대로 남겨 동결값(game_id 자연키)과 문서상 모순(HIGH-1, 스키마 영향 없음·문서 정정 권고). 생성: 2026-06-23 / 감사자: cross-consistency audit (Read-only). 코드/설계 수정 0. --- ## 점검 항목별 verdict ### 1. ★평가 단위 일관성 (최고 위험) — **편차(HIGH-1, 문서 표현 한정)** - **동결 권위(W2-3 F8/§64/§67)**: 평가단위 = `(jam_id, game_id)` 자연키 = 활성 출품작. FK 는 `game_id→games`·`jam_id→jams` 직접. 출품검증=앱계층(jam_entries 활성행). - **하류 정합**: jam_scores(W2-3:142-143)·jam_votes(W2-3:185-186)·jam_awards(W2-3:221-222) DDL FK 전부 `(jam_id, game_id)`. W2-4 점수입력(`/jams/{jamId}/games/{gameId}/scores`), W2-5 투표(`castVote(jamId, gameId, voterUserId)`), W2-6 시상(트랙별 gameId 랭크) **전부 game_id 단위로 일치**. game_id 혼용·jam_entries.id 직접 FK **없음** → 평가단위 일관성 **OK**. - `[HIGH] W2-1:43, W2-1:65 → 문제`: W2-1 본문이 "평가 단위 = `jam_entries.id`"(§43 "jam_entries.id 를 평가 단위로 제공", §65 "평가 단위 = jam_entries.id", §196 DDL 주석 "평가 단위 = entry.id — W2-3/4/5/6 참조점")로 **동결값과 정반대 식별자**를 명시. W2-3 은 이 모순을 concern 1·난제1·F8 에서 명시 인지하고 game_id 자연키로 **이미 해소**했으나(W2-3 이 권위), **W2-1 문서는 미정정 잔존**. → 구현자가 W2-1 본문만 읽으면 잘못된 평가단위로 매퍼/FK 설계 가능. `권고`: W2-1 §43/§65/§196 을 "평가단위 = (jam_id, game_id) 자연키, jam_entries 가 active-UNIQUE 로 1:1 대응 보장(W2-3 F8 동결)" 으로 문서 정정. **스키마 영향 0**(W2-1 DDL 자체는 game_id/jam_id 컬럼을 정상 보유, ux_jam_entries_jam_game_active 도 정합) — 순수 본문 표현 drift. ### 2. 동결 스키마 컬럼 정합 — **OK** - W2-3 동결 VIEW 출력 컬럼 = `jam_id/game_id/weighted_total/simple_total/scored_criteria/judge_count`(W2-3:281-284). - W2-4 소비(§97/§130/§207): `weightedTotal/simpleTotal/scoredCriteria/judgeCount` camelCase **정합**. 정렬·alias·큰따옴표 규약 일치. - W2-6 소비(§36/§110-113/§191): JUDGE=`jam_score_stats.weighted_total`, USER_RATING=`game_review_stats.avg_rating`+`review_count>=3`, POPULAR=`jam_votes` count — 전부 동결 컬럼명 일치. `score_value numeric(10,4)` 는 트랙별 **값 규약**만 확정(스키마 변경 0). 컬럼명 drift **없음**. ### 3. 게이트 계약 정합 — **OK (게이트 순서 LOW 1건)** - **isJudge 시그니처**: W2-2 동결 `JamRoleGate.isJudge(session, jamId)`(W2-2:283) ↔ W2-4 호출 `jamRoleGate.isJudge(session, 42)`(W2-4:167) **일치**. - **자기출품 충돌**: W2-2 제공 `isOwnEntry(jamId, gameId, userId)`(W2-2:285, option (b) 별도 메서드) ↔ W2-4 호출 `isOwnEntry(42, 777, userId)`(W2-4:171, "기본 가정 (b)") **일치**. 개인+팀멤버 OR 양경로 커버(W2-2 §307 SQL). enforce 시점=점수입력(W2-2 J7) ↔ W2-4 enforce **정합**. - **평가기간 게이트(F6)**: W2-3 `status='EVAL' AND now∈[eval_start_at,eval_end_at]` → W2-4 `JamEvalWindow.isOpen`(§149), W2-5 컨트롤러 진입부(§148) **동일 조건** 사용. 위반=422 정책 3문서 일치. - `[LOW] W2-4:67/138 vs W2-2:179 → 문제`: 점수입력 게이트 **순서**가 두 문서에서 상이. W2-4 = CSRF→인증→**isJudge(403)**→jam→**평가기간(422)**→출품작→자기출품. W2-2 §179 = CSRF→로그인→**평가기간(422)**→**isJudge(403)**→자기출품. isJudge 와 평가기간의 선후가 swap. `권고`: 무해(둘 다 첫 실패 지점 반환·최종 결과 동일, W2-2 §179 이 "순서는 W2-4 결정"으로 이미 양보) — 구현은 W2-4 순서 채택 명시면 충분. ### 4. 중복 DDL — **OK** - jam_votes `CREATE TABLE` 가 W2-3(§182, 소유)+W2-5(§86) 양쪽 등장. W2-5 의 것은 §79-85 에서 "참조 사본(W2-3 §3 권위, 본 설계가 정의/변경하지 않음, DDL 미수정, 신규 DDL 0)"으로 **명시 인용**이고 본문(컬럼/UNIQUE ux_jam_votes_jam_voter/idx_jam_votes_jam_game)이 동결과 **동일** → 발산하는 2번째 정의 **아님**. - W2-4/W2-6 은 `CREATE TABLE/VIEW` **0건**(AC 텍스트의 부재검증 토큰만). 다른 중복 CREATE 없음. ### 5. 권한키 정합 — **OK** - 카탈로그 = W1(GAME_JAM_MANAGE/POST_WRITE/CONTENT_MODERATE) + W4 신규 BADGE_MANAGE. - **W3-1**: TAG_MANAGE 신규키 도입 **안 함** — `CONTENT_MODERATE` 재사용으로 확정(D4 §60/§483, 본문·게이트 호출부 일관). 미래 분리는 concern 으로만 이관(키 도입 0). - **W3-3**: POST_WRITE 재사용(신규키 0, §55/§69). - **W4 BADGE_MANAGE**: 신규키 1개 추가. W1 AC-T1(카탈로그 카운트 3) 깨짐을 **명시**(W4 §12 concern·§455 AC-T5·§405 — "W1 AC-T1 을 4로 재측정 필요"). PermissionCatalogVerifier values() 자동 시드라 enum +1 만으로 동기. **누락 없음**. ### 6. cross-W 참조 — **OK (1건 전제 정정)** - **W3-4 배너→잼검색**: W3-1 동결 `GET /games/search?jam={jamSlug}`(§65/§234) ↔ W3-4 §126-130 동일 라우트 앵커(+ W3-1 미확정 시 `/jams/{slug}` 폴백 우선순위 확정). **일치**. - **W3-3 POST_WRITE enforcement**: `PermissionGate.has(session, POST_WRITE.name())` 연결(§55/§325). **OK**. - **W2-6 유저평점 트랙 = game_review_stats.avg_rating**(W2-6 §37/§111) — 단방향 읽기(write 0, AC-T2). **OK**. - `정정(편차 아님)`: 감사 전제 "W2-6 유저평점 트랙 = W4 와 **동일 VIEW 소비**"는 **부정확**. W4 REVIEWER 배지는 `game_review_stats` 가 아니라 `reputation_events`(리뷰작성 이벤트 누적 임계, W4 §54/§263)를 소비. 둘은 **독립 메커니즘**(공유 VIEW 아님) → 발산 위험 없음(같은 VIEW 를 다르게 재정의하는 상황이 애초에 없음). 정합 무관. ### 7. 보안 존재성 — **OK (전부 실재·비자명)** - **W2-2 잼-스코프 게이트**: `JamRoleGate`(jam_judges 조회, 전역 PermissionGate 와 별도 축). isOwnEntry 개인+팀멤버 OR-EXISTS(§307). 전역 RBAC 무변경 단언(AC-T7). **실재·비자명**. - **W3-3 SSRF(SsrfSafeFetcher)**: 9항목 체크리스트 실재(§273-284 — scheme allowlist / host resolve / 모든 resolved IP 공인검증(사설·루프백·링크로컬·메타데이터 169.254.169.254) / connect / 연결소켓 peer-IP 재검증(rebinding) / redirect 매홉 재검증 / size cap / timeout / Content-Type). DNS-pin TOCTOU 한계까지 concern 명시. **비자명**. - **W3-5 zip-slip(canonical+심링크)**: `toRealPath()` 경계 재검증 + assertParentNotSymlink(부모경로 walk+isSymbolicLink) + 쓰기후 toRealPath().startsWith 재검증 + 엔트리명 사전거부(절대/UNC/백슬래시/NUL/드라이브/`..`/길이/깊이). ZipEntry mode-bit 직접파싱은 근거와 함께 기각(over-eng). **비자명**. --- ## 부차 관찰 (편차 아님 — 동결 허용범위 내) - `[LOW] W2-5 vote mapper 시그니처 drift`: W2-3 계약(§371-374)은 `castVote/updateVote/hasVoted/countByGame`, W2-5 구현(§231-242)은 `castVote/updateVote/deleteVote/findVotedGameId/countByJam/listCountsByJam`. W2-3 이 해당 시그니처를 "계약 골격(inflate 마킹, 구현 재확인)"으로 명시했고 `countByGame ... (또는 listCounts 집계)`로 listCounts 변형을 예고 → 동결 허용범위 내. W2-6 POPULAR 소스(`listCountsByJam`)와 W2-5 제공이 정합. castVote 인자순서(jamId,gameId,voterUserId)는 W2-3↔W2-5 **완전 일치**. - `[LOW] 점수입력 경로 표현차`: W2-2 §230 의사코드는 `POST /jams/{slug}/scores`(예시), W2-4 실제 계약은 `POST /jams/{jamId}/games/{gameId}/scores`. W2-2 시퀀스는 게이트 소비점 예시일 뿐 경로 권위 아님 → 무해. --- ## 전체 verdict - **HIGH 1**(W2-1 본문 평가단위 표현 정정 — 스키마/계약 영향 0, 문서 drift). - **LOW 3**(게이트 순서 표현차·vote mapper 시그니처 골격 변형·점수입력 경로 예시차 — 전부 동결 허용범위/무해). - **동결 단일권위(W2-3) 유지·하류 소비 정합 — PASS.** jam_scores/jam_votes/jam_awards/jam_score_stats 의 평가단위·컬럼·트랙enum·게이트·단방향 계약을 W2-4/5/6 이 재정의 없이 소비. 중복 DDL·권한키 silent 추가·cross-W 라우트 불일치 **없음**.