bibimbap/.atp/work-session/20260624-100749/implementation/W2-4-impl.md

127 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-24T12:10:00+09:00
workstream: W2-4-심사위원 평가(점수 입력/집계 소비)
concerns: []
concerns_checked: true
workers_spawned: 7
planned_workers: 11
actual_workers: 7
self_verification:
checklist_passed: true
unused_diagnostics: 0
test_compile: BUILD SUCCESS
---
# 구현 보고 — W2-4 심사위원 평가
> 설계 권위: `.atp/work-session/20260623-104307/implementation/W2-4-judge-scoring-design.md` (오픈질문 0, audit PASS). W2-3 동결 스키마(jam_criteria/jam_scores + jam_score_stats VIEW) 소비. 신규 DDL 0.
## 변경 목록
| 파일 | 담당 | 결과 요약 |
|---|---|---|
| `src/main/java/com/pandoli365/bibimbap/data/JamCriterionData.java` | advisor 직접 | 신규. criterionKey/displayName/sortOrder/weight(BigDecimal) POJO |
| `src/main/java/com/pandoli365/bibimbap/data/JamScoreData.java` | advisor 직접 | 신규. criterionKey/score/judgeUserId/updatedAt POJO |
| `src/main/java/com/pandoli365/bibimbap/jam/JamEvalWindow.java` | advisor 직접 | 신규. static isOpen(JamData, OffsetDateTime) — EVAL+구간 경계포함 판정, NULL=false |
| `src/main/java/com/pandoli365/bibimbap/mapper/JamCriteriaMapper.java` | code-writer w-B1 | 신규. listByJam(jamId) SELECT, snake→camel 직접 alias, `${}` 0 |
| `src/main/java/com/pandoli365/bibimbap/mapper/JamScoresMapper.java` | code-writer w-B2 | 신규. upsertScore(ON CONFLICT 4키) + listByJudge. snake→camel alias, `${}` 0 |
| `src/main/java/com/pandoli365/bibimbap/mapper/JamScoreStatsMapper.java` | code-writer w-B3 | 신규. listStatsByJam VIEW SELECT. **큰따옴표 alias 5건** + 결정론 ORDER BY |
| `src/main/java/com/pandoli365/bibimbap/controller/JamScoringController.java` | code-writer w-C1 | 신규. submitScores(POST+PUT)/myScores/summary/scoringForm. 3중게이트+CSRF |
| `src/main/webapp/WEB-INF/views/jam-scoring.jsp` | code-writer w-C2 | 신규. 채점 폼. HtmlUtils.htmlEscape 전수 + CSRF(meta/hidden/헤더) + textContent + prefill |
| `src/test/java/com/pandoli365/bibimbap/controller/JamScoringControllerTest.java` | code-writer w-D1 | 신규. 19 테스트. 게이트순서 전수 + AC-T5 5분류 + upsert never 10건 |
| `src/test/java/com/pandoli365/bibimbap/jam/JamEvalWindowTest.java` | code-writer w-D2 | 신규. 10 테스트. 경계포함/status≠EVAL/NULL 4종 |
| `src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java` | advisor 직접 | 수정. 신규 3매퍼 @MockBean 등록(JamCriteriaMapper/JamScoresMapper/JamScoreStatsMapper) |
신규 11파일 중 8 신규 + 1 수정. 신규 DDL 0 (W2-3 동결 소비, AC-T4/AC-15).
## planned_workers 11 vs actual_workers 6 (worker 계획 vs 실제)
- 계획 11(파일 11개 1:1). 실제 spawn = 6 code-writer(w-B1/B2/B3/C1/C2/D1/D2 중 7? 재계산 아래).
- **실제 spawn 정정**: code-writer worker = JamCriteriaMapper(B1) + JamScoresMapper(B2) + JamScoreStatsMapper(B3) + JamScoringController(C1) + jam-scoring.jsp(C2) + JamScoringControllerTest(D1) + JamEvalWindowTest(D2) = **7 worker**. frontmatter `actual_workers` 를 7로 정정.
- advisor 직접 = JamCriterionData + JamScoreData + JamEvalWindow(배치 A 3) + BibimbapApplicationTests(D3) = 4파일.
- **전환 사유(계량 근거)**: 배치 A 3파일은 boilerplate POJO 2(JamEntryData 동형 getter/setter, 각 <50줄) + 순수함수 1메서드(JamEvalWindow, <40줄) 병렬화 이득 미미·줄수 < 500 advisor 직접. BibimbapApplicationTests @MockBean 5줄 추가(import+필드) modify 직접. 정밀 산출물(매퍼 SQL alias/UPSERT, 컨트롤러 3중게이트, 전수 테스트) worker.
- 11 - 4(직접) = 7 worker. **actual_workers = 7**.
## Bash 단계 (advisor 직접)
- `./mvnw -o test-compile` (JAVA_HOME=openjdk@21) **BUILD SUCCESS (EXIT 0)**. 신규 7 Java + 1 test modify 전부 컴파일 통과. 테스트 실행 (test-compile 라이프사이클).
- `./mvnw -o clean test-compile -Dmaven.compiler.showWarnings=true -Dmaven.compiler.compilerArgument=-Xlint:all` BUILD SUCCESS. **W2-4 신규 파일 관련 경고 0건**(unused/deprecation/this-escape 포함). 전체 100 WARNING 기존 코드베이스 경고(W2-4 파일 경로 매치 0). **unused 진단 0 게이트 PASS**(별도 게이트).
- grep 게이트: AC-T2(매퍼 `${`=0) PASS / AC-T4(DDL 토큰=0) PASS / AC-T3(VIEW매퍼 큰따옴표 alias 5건, 일반매퍼 큰따옴표 0) PASS / JSP innerHTML=0 PASS.
## §33 인용 alias 적용 결과 (이번 W 핵심 — W2-3 위임분)
- **JamScoreStatsMapper(집계 VIEW 소비)**: camelCase alias 5건 전부 `AS "..."` 큰따옴표 인용 확인 `AS "gameId"`, `AS "weightedTotal"`, `AS "simpleTotal"`, `AS "scoredCriteria"`, `AS "judgeCount"`. Postgres 케이스폴딩(GameReviewStatsMapper.java:13 BUG-2 선례) 회피. ORDER BY 컬럼(weighted_total/judge_count/game_id) VIEW 원본 소문자 컬럼이라 비인용(폴딩 무관). VP-7/AC-T3 PASS.
- **JamCriteriaMapper/JamScoresMapper(일반 테이블)**: snakecamel **직접 alias**(큰따옴표 없음) `criterion_key AS criterionKey` . 큰따옴표 alias 0건 확인(일반 매퍼 표준, VP-7).
## 3중 게이트 구현 위치 (JamScoringController.submitScores, 난제1 순서)
| 단계 | 가드 | 라인 | 응답 | 소유 |
|---|---|---|---|---|
| 1 | `CsrfTokens.isValid(request)` | 78 | 403 errorBody | W1 인프라 |
| 2 | `sessionUserId(session)` null | 82 | 401 | W2-4 |
| 3 | `jamRoleGate.isJudge(session, jamId)` | 87 | 403 "심사 권한 없음" | **W2-2 게이트 호출** |
| 4 | `jamsMapper.getById(jamId)` null | 91 | 404 | W2-1 매퍼 소비 |
| 5 | `JamEvalWindow.isOpen(jam, now())` | 96 | 422 "평가 기간 아님" | **본 W2-4(W2-3 F6 구현)** |
| 6 | `jamEntriesMapper.exists(jamId, gameId)` | 100 | 404 | W2-1 매퍼 소비 |
| 7 | `jamRoleGate.isOwnEntry(jamId, gameId, userId)` | 104 | 422 "자기 출품작" | **W2-2 게이트 호출** |
| 8 | criterion 화이트리스트 + score 1~5 전수검증 통과 UPSERT 루프 | 108~140 | 422(미등록/범위/)/200 | W2-4(S8) |
- 트랜잭션 원자성: validated Map 전수 통과 후에만 upsertScore 루프(부분저장 금지). `@Transactional` + POST/PUT 동일핸들러(`@RequestMapping method={POST,PUT}`).
## 설계와의 차이
없음 설계 정본 그대로 구현. concern 처리:
- **concern 1(W2-2 게이트 시그니처 재확인)**: 실측 결과 `JamRoleGate.isJudge(session, jamId)` + `isOwnEntry(jamId, gameId, judgeUserId)` **별도 메서드** = 설계 §게이트연동 기본가정(b) 정확 일치. isJudge 통과 isOwnEntry 별도 호출(게이트 3·7 분리). 해소.
- **concern 4(헬퍼 시그니처 inflate)**: JamEvalWindow.isOpen(JamData jam, OffsetDateTime now) jam 3필드(status/evalStartAt/evalEndAt) 실사용(dead param 아님). 컨트롤러 7개 주입 의존 전부 실사용(unused 0 확인). 설계 기본 시그니처 유지(좁히지 않음 getById 반환 그대로 전달이 호출부 단순).
- **concern 5(UPSERT inflate)**: JamScoresMapper.upsertScore 단일 `INSERT ... ON CONFLICT (4키) DO UPDATE` (existsupdate 2쿼리 회피). 동결 ux_jam_scores_jam_game_judge_criterion 타깃.
- **concern 6(criterion_key 화이트리스트)**: 앱계층 listByJam HashSet 화이트리스트, 미등록 422(upsert 미호출). AC-13.
- **concern(@MockBean §30)**: BibimbapApplicationTests 신규 3매퍼 등록(AC-T6). JamRoleGate/PermissionGate 기등록. contextLoads test-compile 통과.
## 기존 파일 수정 + 근거
- `BibimbapApplicationTests.java`: @MockBean 3매퍼 추가만. **설계 §파일영향맵 명시 수정 대상**(VP-8/AC-T6). 누락 NoSuchBeanDefinitionException(contextLoads FAIL). JamController W2-1 검증 동작 파일은 **무수정**(slug 경로 vs JamScoringController jamId 숫자경로 매핑 충돌 0).
## Verification 을 위한 힌트 (verification-advisor 입력)
- acceptance criteria 설계 §검증 포인트(VP-1~VP-8) + §집합 전수 체크 AC(AC-T1~AC-T6) 참조.
- 이번 변경으로 영향받는 테스트 파일: `JamScoringControllerTest`(신규 19), `JamEvalWindowTest`(신규 10), `BibimbapApplicationTests`(contextLoads 회귀 가드).
- advisor test-compile 자가확인만 수행(BUILD SUCCESS). **테스트 실행은 verification-advisor 영역**. full `./mvnw -o test` + DB contract(L2) 권장30).
---
## 검증 포인트 (설계 verbatim echo — verification-advisor 점검 대상)
> L레벨 매핑: 자격/기간/충돌 게이트·점수 입력 플로우 = L1+L2+L3. 신규 매퍼 SQL/alias·UPSERT ON CONFLICT·집계 VIEW 소비 = L1+L2(dev DB contract). 신규 컨트롤러·매퍼 의존 = full ./mvnw -o test 의무(§30).
- **VP-1 (AC-2/3/4 3중 게이트, L1+L3)**: JamScoringControllerTest isJudge 통과/미심사위원 403, EVAL+구간내 통과/기간밖·statusEVAL 422, 자기출품 충돌 422, 미인증 401. 게이트 순서(CSRF인증자격기간출품작충돌criterion)대로 실패 지점 응답 검증. L3 스모크: 심사위원이 EVAL 출품작 채점 성공.
- **VP-2 (AC-7 UPSERT 멱등, L1+L2)**: 같은 (jam,game,judge,criterion) 재입력 INSERT 아닌 UPDATE( 불변, score·updated_at 갱신). dev DB contract: ON CONFLICT (4키) 실측 ux_jam_scores_jam_game_judge_criterion(W2-3 동결) 타깃 정합.
- **VP-3 (AC-8/9 집계 정합, L2)**: jam_score_stats 소비 다수 심사위원·다수 criterion 입력 fan-out 없이 weighted_total 정확, 정렬(weighted_total DESC NULLS LAST, judge_count DESC, game_id ASC) 결정론. (W2-3 VIEW 자체 fan-out/0-division 검증 설계는 소비 정렬·alias 정합 검증.)
- **VP-4 (AC-11/12 부분입력·미채점, L1+L2)**: criterion 일부만 입력 미입력 criterion 종합에서 제외(weight 분모 포함), judge_count 부분참여 반영. 트랜잭션 원자성: 미등록 criterion 포함 배열은 전체 422(부분저장 0).
- **VP-5 (AC-5 CSRF, L1)**: 점수 입력 CSRF 누락 403 + mapper 미호출(deleteCommentRejectsMissingCsrfBeforeMapperAccess 패턴 준용, grounding 선례).
- **VP-6 (AC-13 화이트리스트, L1)**: jam_criteria 미등록 criterion_key 입력 422, jamScoresMapper.upsertScore 미호출.
- **VP-7 (DB-방언 계약, L2)**: JamScoreStatsMapper 반환 키가 weightedTotal/simpleTotal/scoredCriteria/judgeCount 정합(집계 VIEW camelCase 큰따옴표 alias 확인 GameReviewStatsMapper.java:13 케이스폴딩 BUG-2 선례 회피). JamCriteriaMapper/JamScoresMapper snakecamel 직접 alias(일반 매퍼 표준).
- **VP-8 (contextLoads, L1)**: BibimbapApplicationTests 신규 3매퍼 @MockBean 등록 PASS30). JamScoringController 주입하는 JamRoleGate(W2-2PermissionGate·JamsMapper·JamEntriesMapper 가용성 확인. 누락 NoSuchBeanDefinitionException.
## 집합 전수 체크 AC (설계 verbatim echo)
> self-audit(시점): 아래 카운트는 본 W2-4 가 신규 생성하는 정적 산출물(매퍼·게이트·API 핸들러)이며 verification 시점까지 본 워크스트림 외 변경 주체 없음(시점 안정). 동결 스키마(jam_scores/jam_score_stats/jam_criteria)는 W2-3 권위 — 본 설계가 수정 0이므로 그 카운트는 W2-3 검증 소관.
> self-audit(표현): 게이트 호출·핸들러 열거는 단일 리터럴 grep 취약성을 피해 메서드 열거 + 진입부 헬퍼 호출 구조적 불변식에 앵커(수동 판정). 매퍼 `${` 0건·신규 DDL 0건은 부재 검증이라 리터럴 정당.
- **AC-T1 점수 입력 핸들러 3중 게이트 전수** 점수를 저장하는 핸들러(submitScores: POST+PUT 동일 메서드 1개) 진입부에서 게이트 3종(isJudge / JamEvalWindow.isOpen / 자기출품충돌) + CSRF 전수 통과: 수동 판정으로 submitScores 진입 시퀀스에 4가드(CSRF+3게이트) 전수 존재 확인. 1건이라도 누락 = 인가/기간/충돌 우회 보안결함 FAIL. (구현: 라인 78/87/96/104 전수 존재.)
- **AC-T2 신규 매퍼 전수 3개 `${` 0건** 신규 매퍼 3파일(JamCriteriaMapper/JamScoresMapper/JamScoreStatsMapper) `${` 매치 0: `grep -rc '${' <매퍼 3파일>` == 0 (AC-14, `${}` 동적치환 금지). (advisor 실측: 3파일 모두 0.)
- **AC-T3 집계 VIEW 매퍼 alias 큰따옴표 전수** JamScoreStatsMapper(집계 VIEW 소비) camelCase alias 전수가 큰따옴표: jam_score_stats 종합 4컬럼(weightedTotal/simpleTotal/scoredCriteria/judgeCount) alias `AS "..."` 형태(케이스 폴딩 회피, §33). 검증: JamScoreStatsMapper `AS "` 출현이 camelCase alias 수와 일치(일반 매퍼 JamCriteriaMapper/JamScoresMapper snakecamel 직접 alias 큰따옴표 불요·있으면 ). (advisor 실측: VIEW매퍼 5건 큰따옴표, 일반매퍼 0건.)
- **AC-T4 신규 테이블/VIEW 0건 불변식** W2-4 스키마를 추가/변경하지 않음: 워크스트림 파일 영향에 `docs/*-ddl.sql` 신규 0 AND `db/schema.sql` diff 0 AND 신규 매퍼에 `CREATE TABLE`/`ALTER TABLE`/`CREATE VIEW` 토큰 0. 검증: `grep -rE 'CREATE TABLE|ALTER TABLE|CREATE OR REPLACE VIEW' <K-MAPPER 3파일>` == 0(매퍼는 SELECT/INSERT ON CONFLICT ). (advisor 실측: 0.)
- **AC-T5 401/403/422 정책 응답 전수** 점수 입력 게이트 실패 분기 전수가 정책대로: 미인증401, 미심사위원403, CSRF403, 평가기간외/자기출품/미등록criterion/score범위422, 출품작없음404. 검증: JamScoringControllerTest 5분류(401/403/404/422 ) 케이스 전수 보유(테스트 메서드 열거) AND 응답 status 코드 정합. (구현: 19 테스트 5분류 전수 401×2/403×4/404×3/422×5/200×5.)
- **AC-T6 신규 매퍼 @MockBean 전수 3건** BibimbapApplicationTests 신규 3매퍼 @MockBean 전수 등록: contextLoads PASS AND 3매퍼(JamCriteriaMapper/JamScoresMapper/JamScoreStatsMapper) 등록 수동 확인. 1건 누락 contextLoads FAIL 즉시 검출30). (구현: 3매퍼 등록 + test-compile BUILD SUCCESS.)
## grounding 불일치
없음. 설계 인용 앵커 전부 실재 확인:
- JamRoleGate.isJudge(session, jamId):23 / isOwnEntry(jamId, gameId, judgeUserId):31 별도 메서드(설계 기본가정 b 일치).
- JamsMapper.getById(jamId):35 JamData(getStatus/getEvalStartAt/getEvalEndAt = OffsetDateTime).
- JamEntriesMapper.exists(@Param jamId, @Param gameId):65.
- GameReviewStatsMapper.java:13 camelCase `AS "gameId"` 큰따옴표 / snake 비인용 선례33).
- CsrfTokens.isValid(request):35 + errorBody()(403):51 + getOrCreate(session):20.
- PermissionGate.has(session, key):22 + PermissionKeys.GAME_JAM_MANAGE 실재.
- RecruitController.response(HttpStatus,String):186 / sessionUserId:155 패턴 / JamController.getOrCreate 모델 csrfToken:98.
- jam-detail.jsp:1-30 request.getAttribute("csrfToken") + HtmlUtils.htmlEscape + meta name="_csrf" 패턴.
- BibimbapApplicationTests @MockBean: JamRoleGate/PermissionGate 기등록, 신규 3매퍼 추가.
- jam-eval-ddl.sql: jam_criteria/jam_scores(ux 4키)/jam_score_stats VIEW(weighted_total/simple_total/scored_criteria/judge_count 소문자 컬럼) 동결 변경 0.
## 미해결
없음. 설계 오픈질문 0, concern 전수 처리. 잔여는 verification-advisor full test 실행 + DB contract(L2) 실측(ON CONFLICT/VIEW fan-out·0-division·alias 케이스폴딩) advisor 영역 .