---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-24T13:10:00+09:00
workstream: W2-6-시상 집계/결과
concerns: []
concerns_checked: true
workers_spawned: 9
planned_workers: 12
actual_workers: 9
self_verification:
  checklist_passed: true
  test_compile: PASS   # ./mvnw -o test-compile EXIT=0 (openjdk@21)
  unused_diagnostics: PASS  # javac -Xlint:all on 9 new/modified main sources → W2-6 unused 0 (third-party jar warnings only)
references:
  design: .atp/work-session/20260623-104307/implementation/W2-6-award-aggregation-design.md
  ownership: .atp/work-session/20260624-100749/implementation/W2-6-ownership.md
---

# 구현 보고 — W2-6 시상 집계 / 결과 (W2 종점)

## 변경 목록

### 신규 생성 (production)
| 파일 | worker | 결과 요약 |
|---|---|---|
| jam/AwardTrack.java | advisor 직접(w-002로 계획) | enum JUDGE/USER_RATING/POPULAR/GRAND + isValid. jam_awards CHECK 4값 1:1 |
| data/JamAwardData.java | advisor 직접(w-001로 계획) | jam_awards 행 POJO + 표시조인 필드(gameName/thumbnailUrl/entrantName). scoreValue=BigDecimal |
| jam/RankScores.java | advisor 직접 | standardCompetitionRank(1,2,2,4) + rankScore (N-rank+1)/N. byOrder(정렬)+byScore(동점판정) 2비교자 |
| mapper/JamAwardsMapper.java | advisor 직접 | insert/deleteByJamTrack/listByJam/listByJamWithGame. 일반 테이블 POJO 직접 alias, `#{}` only |
| mapper/JamReviewRatingMapper.java | advisor 직접(w-003로 계획) | USER_RATING — jam_entries LEFT JOIN game_review_stats. review_count>=3 + NULLS LAST. 집계 VIEW → `AS "avgRating"`/`"reviewCount"` 큰따옴표 |
| jam/JamAwardService.java | advisor 직접 | 3트랙 독립 랭크 + GRAND 정규화 가중합 + NULL/미달 제외 + 멱등 @Transactional recompute |
| controller/JamAwardAdminController.java | w-004(code-writer) | POST /admin/jams/{jamId}/awards/compute. 게이트→CSRF→404→F6 422→recompute→200 awardCounts |
| controller/JamAwardController.java | w-005(code-writer) | GET /jams/{slug}/results. 공개, byTrack 그룹핑, "jam-results" 뷰 |
| WEB-INF/views/jam-results.jsp | w-006(code-writer) | GRAND+3트랙 랭킹 표. HtmlUtils.htmlEscape 전수. 미산정 "결과 준비 중" |

### 수정 (기존 — 설계 근거 명시)
| 파일 | worker | 결과 요약 + 근거 |
|---|---|---|
| controller/JamController.java | w-007(code-writer) | detail 모델 `awardsSummary` 추가 + 생성자 JamAwardsMapper 주입. **AW-DETAIL(설계 §파일영향맵 W2-1 협의 수정). 기존 5핸들러·model 키 무변경(추가만)** |
| WEB-INF/views/jam-detail.jsp | w-008(code-writer) | 출품작 섹션 앞 "수상 결과" 배지 + 전체 결과 링크. **AW-DETAIL. awardsSummary 빈 가드 → 미산정 잼 회귀 0** |
| test/BibimbapApplicationTests.java | w-009(code-writer) | JamAwardsMapper/JamReviewRatingMapper @MockBean 2건 추가(§30 contextLoads) |
| test/controller/JamControllerTest.java | advisor 직접(계획외) | **JamController 6번째 생성자 인자(JamAwardsMapper) 추가로 인한 회귀 컴파일 깨짐 수정.** @Mock jamAwardsMapper + 생성자 호출 1줄 갱신. AW-DETAIL 변경의 불가피한 동반 수정(아래 §설계와의 차이) |

### 신규 테스트 (구현 산출물 — "(검증)" 태그여도 advisor 작성 의무)
| 파일 | worker | 결과 요약 |
|---|---|---|
| test/jam/JamAwardServiceTest.java | w-010(code-writer) | 7 테스트: 3트랙 DESC rank·GRAND 정규화·1트랙 가용 포함·NULL weightedTotal 제외·empty·동점 competition·멱등 delete선행 |
| test/controller/JamAwardAdminControllerTest.java | w-011(code-writer) | 7 테스트: 401/403/CSRF403/404/F6 422/CLOSED 200/eval종료 200 |
| test/controller/JamAwardControllerTest.java | w-012(code-writer) | 5 테스트: missing/hidden redirect·빈 결과·트랙 그룹핑·익명 접근 |

## Bash 단계 (advisor 직접)
- `./mvnw -o test-compile` (JAVA_HOME=openjdk@21) → **EXIT=0** (1차 회귀 1건: JamControllerTest 구생성자 → 수정 후 GREEN)
- `javac -Xlint:all` 신규/수정 main 9파일 (m2 jar 클래스패스) → W2-6 unused 변수/파라미터/import **0** (third-party tomcat-embed-core 소스 + 어노테이션프로세서 warning 4건만 — W2-6 코드 무관)
- 집합 전수 AC self-audit grep (아래 §전수 체크)

## 설계와의 차이 (planned 12 → actual 9 worker, advisor 직접 6파일)

**worker 전환 사유 (계량 근거)**: 의존 코어 3파일(JamAwardsMapper/RankScores/JamAwardService)은 상호 강결합 + GRAND 정규화 알고리즘(순위점수·가용트랙 가중평균·동점 competition)의 정밀도 비용이 커서 병렬 worker 분산 이득 < 정합 리스크 → advisor 직접. POJO(JamAwardData)·enum(AwardTrack)도 ≤50줄 사소 단일편집이라 직접. 합 6파일을 advisor 직접 작성, 나머지 9파일(컨트롤러2·JSP2·수정2·테스트3)을 code-writer 병렬 spawn.
- advisor 직접 작성 파일: AwardTrack.java, JamAwardData.java, RankScores.java, JamAwardsMapper.java, JamReviewRatingMapper.java, JamAwardService.java + (계획외 회귀수정) JamControllerTest.java

**JamVoteCountMapper 미생성 (설계 concern 해소)**: 설계 §파일영향맵은 "JamVoteCountMapper(POPULAR) — W2-5 JamVotesMapper 와 별개 or 재사용, 구현 시 협의"로 명시. 기존 `JamVotesMapper.listCountsByJam(jamId)`(W2-5)가 이미 `[{gameId,voteCount}]` GROUP BY count + 큰따옴표 alias + @MockBean 등록됨 → **재사용**. 신규 매퍼 3→2, 신규 @MockBean 3→2. AC-T5 카운트는 2(설계 3가정에서 1 감소 — JamVotesMapper 기존 등록분이 POPULAR 소비 충당).

**JamScoreStatsMapper.listStatsByJam 재사용(JUDGE)**: 설계 명시(W2-4 소유 재사용). 신규 0.

**JamReviewRating Row POJO 미생성**: 설계 시그니처는 `JamReviewRatingRow` 언급. 기존 집계 소비 패턴(JamScoreStatsMapper/JamVotesMapper)이 `List<Map<String,Object>>`이므로 동형 채택 → row POJO 0(inflate 방지, design concern 1 정합).

**JamControllerTest 회귀 수정(계획외)**: AW-DETAIL 가 JamController 생성자에 6번째 인자(JamAwardsMapper)를 추가 → 기존 W2-1 테스트 컴파일 깨짐. 설계가 AW-DETAIL 을 "W2-1 협의 수정"으로 승인했으므로 그 동반 테스트 수정은 필수(테스트 작성/수정은 implementation 산출물). @Mock 1 + 생성자 호출 1줄만 변경, 기존 테스트 로직·단언 무변경.

## 설계 "검증 포인트" verbatim echo (verification-advisor 입력)

> L레벨 매핑: 산정 알고리즘(3트랙·GRAND·NULL·동점·멱등) = **L1**(JamAwardServiceTest 단위) + 3트랙 join/정렬/NULL = **L2(dev DB contract)**. 인가/게이트/F6 422 = **L1+L3**. 신규 컨트롤러·매퍼 의존 = full `./mvnw -o test` 의무(§30).

- **VP-1 (AC-1~5 산정 코어, L1)**: JamAwardServiceTest — ① 3트랙 각 정렬·rank 정확(weightedTotal/avgRating/voteCount DESC) ② GRAND rankScore (N-rank+1)/N + 가용트랙 가중합 정확 ③ 1트랙만 가용 출품작이 GRAND 에 포함(분모=그 트랙 weight) ④ 모킹 소스로 결정적.
- **VP-2 (AC-6 NULL/미달, L1+L2)**: 리뷰 0개(avg_rating NULL) 출품작 → USER_RATING 트랙 제외(rank 없음). review_count==3 경계 포함, ==2 제외. 채점 0 출품작 JUDGE 제외. 득표 0 출품작 POPULAR rankScore 모집단 제외. GRAND 는 그래도 가용트랙으로 산정. dev DB contract 샘플 실측.
- **VP-3 (AC-5 동점, L1)**: weightedTotal 동일 2작품 → 같은 rank(competition 1,2,2,4), tiebreak game_id ASC 결정적. GRAND 동점도 동일. 재산정 시 같은 결과(멱등).
- **VP-4 (AC-8 멱등, L1+L2)**: recompute 2회 호출 → jam_awards 행이 중복 누적 아님(deleteByJamTrack 선행). UNIQUE(jam_id,track,game_id) 위반 없음. 부분 실패 시 트랜잭션 롤백(트랙 비는 상태 회피).
- **VP-5 (AC-7/9 게이트·F6, L1+L3)**: JamAwardAdminControllerTest — ADMIN 통과 / SUBADMIN+GAME_JAM_MANAGE 통과 / SUBADMIN 무키 403 / 미인증 401 / CLOSED 아님+eval진행중 422 / CSRF 누락 403(mapper 미호출). L3 스모크: CLOSED 잼 산정 → 결과 페이지 노출.
- **VP-6 (AC-12/13 alias·단방향, L2)**: JamReviewRatingMapper 반환 키 avgRating/reviewCount 정합(game_review_stats 큰따옴표 케이스폴딩 BUG-2 선례 회피, GameReviewStatsMapper.java:14 `AS "avgRating"` 동형). 산정 SQL 이 game_reviews/jam_scores/jam_votes 에 write 0(SELECT 만). 6축 컬럼 미참조.
- **VP-7 (contextLoads, L1)**: BibimbapApplicationTests 에 신규 3매퍼 @MockBean 등록 후 PASS(§30). 누락 시 NoSuchBeanDefinitionException.
  → **본 구현 정정**: 신규 매퍼는 **2개**(JamAwardsMapper/JamReviewRatingMapper). POPULAR 는 기존 JamVotesMapper(@MockBean 기등록) 재사용 → 신규 @MockBean 2건. JamScoreStatsMapper 도 기존 @MockBean. contextLoads 보존.

## 설계 "집합 전수 체크 AC" verbatim echo + 적용 결과

- **AC-T1 시상 트랙 전수 4종 산정 경로 정합 불변식** — AwardTrack enum 멤버 수 == jam_awards_track_check CHECK IN 항목 수(W2-3 동결) == JamAwardService insert award_track 집합 == 4(JUDGE/USER_RATING/POPULAR/GRAND).
  → **PASS**: enum 멤버 grep 4. JamAwardService.recompute 가 insertTrack(JUDGE/USER_RATING/POPULAR) + insertGrand(GRAND) 4 산정 경로 전수 호출(AwardTrack.values() 루프로 delete, 명시적 4 insert 경로).
- **AC-T2 산정 소스 매퍼 전수 SELECT-only 단방향 불변식** — 산정 소스에 game_reviews/jam_scores/jam_votes 대상 INSERT/UPDATE/DELETE 0.
  → **PASS**: grep `INSERT/UPDATE/DELETE (game_reviews|jam_scores|jam_votes)` in JamReviewRatingMapper/JamAwardsMapper → none. JamAwardsMapper 는 jam_awards 대상 INSERT/DELETE 만(허용). JamScoreStatsMapper/JamVotesMapper.listCountsByJam 은 SELECT/기존(W2-4/5).
- **AC-T3 시상 신규 매퍼 `${` 0건** — 신규 매퍼 `${` 매치 0.
  → **PASS**: 기능적 `${...}` 동적치환 0. (Javadoc 주석 텍스트 `` `${}` 0 `` 2건은 문서 — 파라미터 바인딩은 전부 `#{...}`.)
- **AC-T4 집계 VIEW 소비 매퍼 alias 큰따옴표 전수** — JamReviewRatingMapper(game_review_stats)·JamScoreStatsMapper(jam_score_stats) camelCase alias 큰따옴표.
  → **PASS**: JamReviewRatingMapper `AS "gameId"`/`AS "avgRating"`/`AS "reviewCount"`(집계 VIEW 소비). JamScoreStatsMapper(W2-4) `AS "weightedTotal"` 기존. 일반 테이블 매퍼 JamAwardsMapper 는 반대로 비인용 직접 alias(scoreValue) — VIEW vs 테이블 구분 정합(혼용 아님). JamVotesMapper.listCountsByJam 도 큰따옴표(`"voteCount"`).
- **AC-T5 신규 시상 매퍼 @MockBean 전수 등록** — contextLoads PASS + 매퍼별 등록.
  → **PASS(정정)**: 신규 매퍼 2개 모두 @MockBean 등록(JamAwardsMapper/JamReviewRatingMapper, line 104/107). POPULAR 소비 JamVotesMapper + JUDGE 소비 JamScoreStatsMapper 는 기존 @MockBean. test-compile GREEN.
- **AC-T6 jam_awards 단방향 소비 무결성(신규 DDL 0)** — docs/ W2-6 신규 *-ddl.sql 0, db/schema.sql 변경 0.
  → **PASS**: `git status docs/ db/` 신규 ddl/schema 0. jam_awards 는 W2-3 docs/jam-eval-ddl.sql 소유, W2-6 변형 0.

## §33 인용 alias 적용 (3트랙 집계 매퍼) + 멱등 + NULL/임계 제외 + CLOSED 게이트 위치

- **§33 인용 alias**:
  - JamReviewRatingMapper(game_review_stats 집계 VIEW, Map resultType) → `AS "gameId"`/`AS "avgRating"`/`AS "reviewCount"` 큰따옴표. (케이스폴딩 회피, GameReviewStatsMapper.java:13-15 선례 동형)
  - JamScoreStatsMapper(jam_score_stats 집계 VIEW, 기존 W2-4) → `AS "weightedTotal"` 기존.
  - JamVotesMapper.listCountsByJam(GROUP BY count, Map resultType, 기존 W2-5) → `AS "gameId"`/`AS "voteCount"` 기존.
  - JamAwardsMapper(일반 테이블, POJO 직접매핑) → 비인용 직접 alias(`AS jamId`/`AS scoreValue`). POJO 직접매핑은 비인용 OK(혼용 아님 — VIEW/Map 만 인용).
- **멱등 보장 메커니즘**: JamAwardService.recompute @Transactional 경계. 진입 시 `for (AwardTrack t : values()) deleteByJamTrack(jamId, t.name())` 4트랙 전부 초기화 → 재INSERT. 두 번 돌려도 결과 동일, UNIQUE(jam_id,award_track,game_id) 위반 없음(매 트랙 선삭제). 부분 실패 시 단일 트랜잭션 롤백(트랙 비는 상태 회피).
- **NULL/임계 제외**:
  - JUDGE — JamAwardService.trackPopulation 이 weightedTotal NULL(채점 0) 행 제외(score NULL → continue).
  - USER_RATING — JamReviewRatingMapper SQL `WHERE st.review_count >= 3`(F5 임계 미달 제외) + 리뷰 0개는 LEFT JOIN NULL → review_count NULL → 임계 제외. NULLS LAST.
  - POPULAR — JamVotesMapper GROUP BY 결과(득표>0 출품작만; 0표 행 없음).
  - GRAND — insertGrand 가 가용 트랙만 분자·분모 포함(weightSum 누적), 모집단=최소1트랙 union. 부당 0점 회피.
- **CLOSED(F6) 게이트 위치**: JamAwardAdminController.computeAwards — getById 후 `"CLOSED".equals(jam.getStatus()) || (evalEndAt!=null && now().isAfter(evalEndAt))` 아니면 422. (앱계층 런타임 게이트, DB CHECK 강제 아님 — 설계 concern 정합)

## grounding 불일치 / concerns 처리 / 미해결
- **grounding 불일치 0**: jam_awards 스키마(docs/jam-eval-ddl.sql §4 track CHECK 4값·rank>=1·ux UNIQUE·idx), jam_score_stats VIEW 컬럼(weighted_total), game_review_stats VIEW(avg_rating/review_count, line 174-201 `AS "avg_rating"`), jam_votes(W2-5 listCountsByJam), JamLifecycle/JamAdminController.requireJamManage 게이트 헬퍼, PermissionGate.has(2-arg), CsrfTokens, BibimbapApplicationTests @MockBean — 전부 실측 후 인용.
- **concerns 처리**: design concerns 5건 전부 검토 — (1)dead parameter: javac -Xlint:all unused 0 확인(award/trackPopulation/RankScores 비교자 전부 사용). (2)신규 매퍼 @MockBean + full test: 2건 등록, test-compile GREEN(full test 는 verification 몫). (3)집계 VIEW alias 큰따옴표: JamReviewRatingMapper 적용. (4)임계 N=3/가중 1/3 상수: JamAwardService 상수로 위치(가변화 후속). (5)F6 422 + @Transactional 멱등: recompute @Transactional deleteByJamTrack 선행. → concerns: [] (반환 차단 사유 없음).
- **미해결**: 없음. (후속 비스코프: 자동 산정 스케줄러·잼별 가변 가중치 — 설계 비목표.)

## Verification 을 위한 힌트
- acceptance criteria: design.md "검증 포인트" VP-1~VP-7 + "집합 전수 체크 AC" AC-T1~AC-T6 참조.
- 영향받는 테스트 파일(신규): JamAwardServiceTest, JamAwardAdminControllerTest, JamAwardControllerTest.
- 회귀 위험 테스트(수정): JamControllerTest(생성자 6arg), BibimbapApplicationTests(@MockBean 2 추가).
- full `./mvnw -o test` 실행 시 baseline 171 + W2-6 신규(서비스 7 + admin 7 + public 5 = 19) GREEN 기대. JDK: /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home (PATH 미등록 — JAVA_HOME export 필요).
- L2(dev DB contract): JamReviewRatingMapper 3트랙 join·NULL·review_count>=3 경계·NULLS LAST 실측 권장(집계 VIEW alias 케이스폴딩).
