--- 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>`이므로 동형 채택 → 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 케이스폴딩).