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

17 KiB

phase agent agent_version generated_at workstream concerns concerns_checked workers_spawned planned_workers actual_workers self_verification references
implementation implementation-advisor 1 2026-06-24T12:30:00+09:00 W2-5-인기투표
true 5 5 5
checklist_passed unused_diagnostics test_compile
true 0 BUILD SUCCESS
design frozen_schema
.atp/work-session/20260623-104307/implementation/W2-5-popular-vote-design.md docs/jam-eval-ddl.sql

구현 보고: W2-5 인기투표 (잼당 1인1표 / 평가기간 게이트 / 표심 은닉→종료 후 공개)

변경 목록

파일 worker 변경 결과 요약
src/main/java/com/pandoli365/bibimbap/mapper/JamVotesMapper.java w-001 신규 @Mapper 6메서드(castVote/updateVote/deleteVote/findVotedGameId/countByJam/listCountsByJam). #{} only, ${} 0건. listCountsByJam alias 큰따옴표 인용.
src/main/java/com/pandoli365/bibimbap/controller/JamVoteController.java w-002 신규 @Controller 4핸들러(POST vote / DELETE cancelVote / GET mine / GET results). 게이트 6단, 토글, 노출 게이트.
src/main/webapp/WEB-INF/views/jam-detail.jsp w-003 수정 인기투표 섹션 + 결과 영역(종료 후만 렌더) + 내 표 + JS(textContent/CSRF 헤더) 추가. 기존 섹션 무변경.
src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java w-004 수정 JamVotesMapper @MockBean 등록(contextLoads 보존). import 1 + 필드 1.
src/test/java/com/pandoli365/bibimbap/controller/JamVoteControllerTest.java w-005 신규 plain Mockito 24테스트. VP-1~8/VP-11/AC-T1/AC-T3 전수.

신규 DDL 0건(W2-3 jam_votes 동결 소비). schema.sql / docs/*-ddl.sql 무수정.

Bash 단계 (advisor 직접)

  • ./mvnw -o clean test-compile -Dmaven.compiler.showWarnings=true -Dmaven.compiler.compilerArgument=-Xlint:all (JAVA_HOME=openjdk@21) → BUILD SUCCESS (EXIT 0). 신규 4 Java + 1 test modify 전부 컴파일 통과. 테스트 실행 안 함(test-compile 라이프사이클 — verification 영역).
  • unused 진단 게이트(별도): 전체 100 WARNING 중 W2-5 신규 파일(JamVotesMapper/JamVoteController/JamVoteControllerTest) 매치 0건. unused/this-escape/unchecked/dead-param 0. 전체 100 은 기존 코드베이스 경고(@MockBean deprecation 등, W2-5 경로 매치 0 — W2-4 보고서와 동일 기존 경고군).
  • 집합 전수 AC grep 게이트(상세 아래 §집합 전수 체크): AC-T1 핸들러 2 + CSRF 2 + 게이트 2 / AC-T2 ${ 0 / AC-T4 jam_votes DDL 변경문 0 — 전부 PASS.

§33 인용 alias 적용 결과 (인기집계 매퍼)

  • JamVotesMapper.listCountsByJamList<Map<String,Object>> 반환(집계 GROUP BY COUNT) → Map resultType 이라 alias 큰따옴표 인용 적용: SELECT game_id AS "gameId", COUNT(*) AS "voteCount" FROM jam_votes WHERE jam_id = #{jamId} GROUP BY game_id ORDER BY COUNT(*) DESC, game_id ASC (선례: JamScoreStatsMapper 가 동일하게 AS "gameId" 큰따옴표 — Postgres lowercase 폴딩 회피로 Map 키 voteCount 보존 → 컨트롤러 ((Number) row.get("voteCount")) 정상.)
  • 단일 스칼라 반환 findVotedGameId(Long), countByJam(long) 은 alias 불요 — 미적용(정상).
  • 그 외 POJO 직접매핑 매퍼 없음(JamVotesMapper 는 Map 1 + 스칼라 2 + 쓰기 3).

1인1표 / 평가기간 / 종료후공개 게이트 구현 위치

  • 1인1표(P2/G2/AC-2): DB ux_jam_votes_jam_voter UNIQUE(jam_id, voter_user_id)(W2-3 동결) + 컨트롤러 토글(JamVoteController.vote line 77~87): findVotedGameId → null=castVote(INSERT) / 동일 game=멱등 no-op / 다른 game=updateVote(game_id 교체). 표 추가 INSERT 안 함 → UNIQUE 충돌 없음.
  • 평가기간 게이트(P4/G4/AC-4): JamVoteController.vote line 68, cancelVote line 120 — !JamEvalWindow.isOpen(jam, OffsetDateTime.now()) → 422 "투표 기간이 아닙니다.". W2-4 산출 JamEvalWindow.isOpen(EVAL + now∈[evalStart,evalEnd]) 재사용(설계 §게이트연동 4 + JamScoringController 선례 동형). 상태변경 핸들러 2건 전부 보유.
  • 미로그인 차단(P5/G5/AC-5): vote line 58, cancelVote line 110, myVote line 141 — sessionUserId(session) null → 401. (results 는 공개 — session 미수신.)
  • CSRF(AC-8): vote line 54, cancelVote line 106 — !CsrfTokens.isValid(request) → 403 errorBody. 매퍼 접근 전(게이트 1).
  • 종료후공개(P6/G6/AC-6 밴드왜건 회피): JamVoteController.results line 170~188 — publiclyVisible = "CLOSED".equals(status) || (evalEndAt != null && now > evalEndAt). true=listCountsByJam(출품작별 count)+total / false=countByJam(총합만)+results:null, open:true(표심 은닉). JSP(jam-detail.jsp line 403/447) 동일 게이트 — 진행 중 출품작별 결과 영역 미렌더, 안내문만. 컨트롤러+JSP 2지점 모두 게이트 보유(AC-T3).
  • 본인 표 상시 노출(P6/AC-11): myVote line 151 — findVotedGameId(진행 중에도 본인 노출). 토글 응답에도 votedGameId 포함(집계는 미포함).

설계와의 차이 (근거 기록)

차이1 — listCountsByJam alias 큰따옴표 인용 (설계 concern 3 과의 의도적 deviation)

  • 설계 concern 3 / §W2-6계약 / §시그니처 주석은 "집계 VIEW 아님 → 큰따옴표 alias 불요(COUNT(*) AS voteCount)" 라 명시했다.
  • 그러나 본 구현은 AS "voteCount" 큰따옴표 인용을 적용했다.
  • 근거: 케이스폴딩 위험의 기준은 "VIEW 여부"가 아니라 "Map resultType 여부"다. listCountsByJamList<Map<String,Object>> 반환 → MyBatis 가 컬럼-프로퍼티 자동 대소문자 매칭을 하지 않고 DB 가 반환한 컬럼 라벨 문자열을 Map 키로 그대로 쓴다. Postgres 는 비인용 alias 를 lowercase 로 폴딩(voteCountvotecount)하므로 컨트롤러 row.get("voteCount") 가 null 이 되어 total 합산이 0 이 되는 결함이 발생한다. POJO 직접매핑(JamScoresMapper.listByJudge 등)은 MyBatis 케이스 무시 매칭으로 안전하지만 Map 은 그렇지 않다.
  • 프로젝트 동일 선례 JamScoreStatsMapper(Map resultType)도 AS "gameId" 큰따옴표 인용 → 본 구현이 그 선례 및 누적 교훈 §33(인용 alias)과 일치. 작업 지시 ★1("인기투표 집계 매퍼가 Map/집계면 인용")이 명시적으로 이 경로를 지시 — 작업 지시 > 설계 concern.
  • 설계 정본의 SQL 의미(컬럼/집계/정렬)는 불변. alias 인용은 L2 DB-방언 계약 안전 강화이며 API 응답 키(gameId/voteCount)도 설계 §외부계약과 동일. 기능/계약 변경 0, 무결성 강화만.

차이2 (없음) — concern 1 dead method/parameter 재확인 결과

  • 설계 concern 1 은 castVote/updateVote/findVotedGameId 분리가 ON CONFLICT upsert 채택 시 dead 가능성을 경고했다. 본 구현은 분기 전략(findVotedGameId 사전조회)을 채택(upsert 아님)했고, 6개 매퍼 메서드 전부 컨트롤러에서 실사용 확인:
    • castVote(vote 신규), updateVote(vote 변경), deleteVote(cancelVote), findVotedGameId(vote 토글 분기 + myVote), countByJam(results 진행 중 총합), listCountsByJam(results 종료 후).
    • dead method 0, dead parameter 0(컴파일 lint 경고 0 으로 교차 확인).
  • 따라서 concern 1 은 "구현이 분기 전략 채택 + 전메서드 실사용" 으로 해소 — 시그니처 inflate 없음. (settled, 잔여 concern 아님.)

자기표 허용(P8) / 표 변경 허용(P7)

  • 설계 정본대로 구현. 자기 출품작 투표 422 분기 미추가(P8 허용), 표 변경 updateVote 경로 구현(P7). 설계 일치.

Verification 을 위한 힌트

acceptance criteria — 설계 §검증 포인트 verbatim echo

L레벨 매핑(verification-strategies): 투표 토글·1인1표·평가기간 게이트·미로그인 플로우 = L1+L2+L3. 신규 매퍼 SQL/alias·집계 GROUP BY = L1+L2(dev DB contract). 신규 컨트롤러·매퍼 의존 = full ./mvnw -o test 의무(§30, @MockBean).

  • VP-1 (AC-2 1인1표, L1+L2): 같은 voter 가 같은 잼에 2회 castVote → 2번째 UNIQUE(jam_id,voter_user_id) 위반(DB 강제, L2 dev contract 실측). 다른 game 으로 POST → updateVote 로 행 1개 유지(changed:true), 표 수 불변.
  • VP-2 (AC-4 평가기간 게이트, L1+L3): jam.status!='EVAL'(RECRUIT/DEV/CLOSED) 또는 now∉[eval_start,eval_end] 시 POST/DELETE → 422 + mapper 미호출. EVAL+window 내만 200.
  • VP-3 (AC-5 미로그인, L1): 세션 userId 없음 → POST/DELETE/mine 401. results 는 미로그인도 200(공개 조회).
  • VP-4 (AC-6 밴드왜건 은닉, L1): 진행 중(EVAL) /vote/results → results:null, open:true(출품작별 count 미노출). 종료 후(CLOSED 또는 now>eval_end) → results 배열 노출. JSP 동일 분기 렌더 확인.
  • VP-5 (AC-8 CSRF, L1): POST/DELETE CSRF 누락 → 403 + mapper 미호출(deleteCommentRejectsMissingCsrfBeforeMapperAccess 패턴 준용 — verification §CSRF-before-mapper).
  • VP-6 (DB-방언 계약, L2): JamVotesMapper 반환 키(votedGameId/voteCount/gameId)가 컨트롤러/JSP 조회 키와 정합. listCountsByJam GROUP BY count 가 샘플 데이터와 일치(snake→camel 직접 alias, 집계 VIEW 아님 → 큰따옴표 미사용 확인).
    • ※ 구현 정정: 본 구현은 Map resultType 이라 큰따옴표 인용 적용(§설계와의 차이 차이1). 검증 시 매퍼 alias 가 AS "gameId"/AS "voteCount" 큰따옴표임을 확인 — Map 키 케이스폴딩 회피.
  • VP-7 (contextLoads, L1): BibimbapApplicationTests 에 JamVotesMapper @MockBean 등록 후 PASS(§30). 누락 시 NoSuchBeanDefinitionException.
  • VP-8 (AC-7 변경/취소, L1): 투표→다른 game POST(changed:true, 표 수 1 유지)→DELETE(votedGameId:null, 행 0)→재투표(changed:false) 시퀀스 정합.

집합 전수 체크 AC — 설계 §집합 전수 체크 AC verbatim echo + 구현 충족 결과

self-audit(시점): 아래 카운트는 본 W2-5 가 신규 생성하는 정적 산출물(컨트롤러 상태변경 핸들러·매퍼 메서드)이며 verification 시점까지 본 워크스트림 외 변경 주체 없음(시점 안정). 자기 트리처럼 증가하는 대상 아님. jam_votes 동결 스키마 카운트는 W2-3 verification 소관(본 설계는 소비만 — 중복 검증 회피). self-audit(표현): 단일 리터럴 grep 취약성을 피해 상태변경 핸들러 집합/게이트 호출 같은 구조적 불변식에 앵커. 매퍼 ${ 0건만 리터럴(부재 검증은 리터럴 정당).

  • AC-T1 투표 상태변경 핸들러 전수 2건 CSRF + 평가기간 게이트 — JamVoteController 의 상태변경 핸들러(POST vote / DELETE cancel) 전수 2건이 ① CsrfTokens.isValid 선검증 ② 평가기간 게이트(jam.status=='EVAL' AND now∈window) 둘 다 보유. 검증: 상태변경 핸들러(@PostMapping/@DeleteMapping) 열거 == 2 AND 각 진입부에 CSRF + 게이트 존재(수동 판정 — @PostMapping/@DeleteMapping 핸들러 열거 후 각 본문 확인, 리터럴 grep 단독 의존 회피). GET(/mine, /results)은 상태변경 아님 → 게이트 비대상(읽기). 핸들러 추가 시 게이트 누락 = 평가기간 우회 보안결함 → FAIL. 이 전수 AC 가 P4 게이트의 핵심 가드.
    • 구현 충족: @PostMapping("/jams/{slug}/vote")(line 44) + @DeleteMapping("/jams/{slug}/vote")(line 97) = 2건. CsrfTokens.isValid 호출 2회(line 54, 106), JamEvalWindow.isOpen 호출 2회(line 68, 120) — 두 핸들러 각 진입부 보유. GET 2건은 게이트 비대상. PASS.
  • AC-T2 투표 매퍼 ${ 0건 — JamVotesMapper(1파일)에 ${ 매치 0: grep -c '\${' JamVotesMapper.java == 0 (AC-9, ${} 동적치환 금지). 부재 검증이라 리터럴 정당.
    • 구현 충족: grep -c '\${' JamVotesMapper.java == 0. (컨트롤러도 0.) PASS.
  • AC-T3 결과 노출 게이트 불변식(밴드왜건 회피)/vote/results 핸들러와 jam-detail.jsp 결과 영역 둘 다 노출 게이트(status=='CLOSED' OR now()>eval_end_at)를 보유: 진행 중 출품작별 count 비노출(results:null/JSP 막대 미렌더). 검증: 컨트롤러 results 분기 + JSP 조건 렌더 전수 2지점 모두 게이트 존재(수동 판정 — 시각 비교는 런타임이라 리터럴 grep 부적합, 의미 불변식 점검). 1지점이라도 무조건 count 노출 시 밴드왜건 회피(P6/G6) 위반 → FAIL.
    • 구현 충족: 컨트롤러 results line 170~172(publiclyVisible = "CLOSED".equals(status) || (evalEndAt != null && now>evalEndAt)) + JSP line 403/447(resultsPublic = "CLOSED".equals(status) || evalEnded; resultsPublic 일 때만 #vote-results 렌더, 아니면 "평가 종료 후 공개" 안내). JS 측 2차 게이트(line 520/583 resultsPublic + line 589 data.open 분기). 2지점(+JS) 모두 게이트 보유. PASS.
  • AC-T4 jam_votes 무변경 불변식(동결 소비) — 본 W2-5 산출물에 jam_votes 의 DDL 변경문(ALTER TABLE/CREATE INDEX/CREATE TABLE) 0건: 본 설계는 신규 docs/*-ddl.sql 파일을 만들지 않고 schema.sql 도 수정하지 않음. 검증: 본 워크스트림 diff 에 jam_votes 대상 ALTER/CREATE 0건(읽기/쓰기 매퍼 SQL 의 INSERT/UPDATE/DELETE/SELECT 는 무방, DDL 변경문만 0). 동결 소비 계약(W2-3) 위반(투표가 스키마 손대기) 즉시 검출.
    • 구현 충족: W2-5 산출물에 jam_votes ALTER/CREATE TABLE/CREATE INDEX 0건(grep 무출력). 매퍼는 INSERT/UPDATE/DELETE/SELECT 만. docs/jam-eval-ddl.sql / schema.sql 무수정. PASS.

이번 변경으로 영향받는 테스트 파일

  • src/test/java/com/pandoli365/bibimbap/controller/JamVoteControllerTest.java (신규, 24테스트)
  • src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java (contextLoads — JamVotesMapper @MockBean 등록 후 PASS 필요, VP-7)
  • full ./mvnw -o test 권장(§30): 신규 컨트롤러+매퍼 의존 contextLoads + 단위 24 + dev DB contract(L2: listCountsByJam GROUP BY + alias 케이스폴딩) 실측.

grounding 불일치 / concerns 처리

grounding 실측 결과 (불일치 0)

  • W2-3 jam_votes 스키마: docs/jam-eval-ddl.sql §3 line 87~117 직접 확인 — id/jam_id/game_id/voter_user_id/created_at + ux_jam_votes_jam_voter UNIQUE + idx_jam_votes_jam_game. 매퍼 컬럼명 일치.
  • W2-4 JamEvalWindow.isOpen(JamData, OffsetDateTime): jam/JamEvalWindow.java 직접 확인 — EVAL + now∈[evalStart,evalEnd] 경계포함. 평가기간 게이트 재사용 정합.
  • W2-1 JamController: getBySlug/sessionUserId(line 244~260)/response(line 270~275)/CsrfTokens 패턴//jams/{slug}/entries·/teams 매핑 직접 확인 — JamVoteController /vote* 경로 충돌 0.
  • JamEntriesMapper.exists(jamId, gameId): 직접 확인(line 65) — 활성 출품작 검증 정합.
  • JamData getter: getStatus/getEvalStartAt/getEvalEndAt(OffsetDateTime)/getId/getSlug — JamsMapper alias(eval_end_at AS evalEndAt) + JamScoringController 사용처로 교차 확인. JSP getEvalEndAt() OffsetDateTime 반환 정합(worker 의존경고 해소).
  • CsrfTokens.isValid/errorBody/HEADER_NAME/SESSION_ATTRIBUTE/getOrCreate: security/CsrfTokens.java 직접 확인.
  • game_likes 무참조: JamVotesMapper/JamVoteController 에 game_likes 참조 0 — 설계 P3/G3(별개) 준수.
  • BibimbapApplicationTests @MockBean: 직접 확인(JamScoresMapper 등 기등록 패턴) — JamVotesMapper 추가 정합.

concerns 처리 (설계 5개 concern)

  • concern 1 (시그니처 inflate / dead method): 해소 — 분기 전략 채택, 6메서드 전부 실사용, lint 경고 0. (§설계와의 차이 차이2.)
  • concern 2 (@MockBean §30): 충족 — BibimbapApplicationTests 에 JamVotesMapper @MockBean 등록(w-004), contextLoads test-compile 통과. VP-7 가드.
  • concern 3 (집계 alias 큰따옴표 불요 주장): 정정 적용 — Map resultType 이라 큰따옴표 인용 채택(§설계와의 차이 차이1 + ★1). 기능 무변경.
  • concern 4 (결과 노출 게이트 컨트롤러/JSP 분기): 구현 — 컨트롤러 results + JSP resultsPublic 2지점 게이트(AC-T3 PASS).
  • concern 5 (자기표 허용): 설계대로 — 자기 출품작 422 분기 미추가(P8 허용).
  • 신규 concern: 없음(빈 리스트). 설계 5 concern 전부 구현 단계에서 해소/정정/충족.

기존 파일 수정 + 근거

  • jam-detail.jsp(W2-1 산출) 수정 — 설계 §파일영향맵 "수정" 명시 대상. 투표 섹션+JS 추가만, 기존 hero/운영안내/출품작/팀/출품폼/팀폼 무변경(회귀 0). 근거: 설계 G1(읽기=잼 상세 JSP 재사용, 투표 폼/결과 영역 추가).
  • BibimbapApplicationTests 수정 — 설계 §파일영향맵 "수정(검증)" + concern 2 §30 의무. JamVotesMapper @MockBean 1개 추가만.

미해결

  • 없음. 설계 오픈질문 0, concern 5개 전부 처리, 집합 전수 AC 4개 PASS, test-compile BUILD SUCCESS, unused 진단 0.
  • (참고 — verification 영역, 차단 아님) JSP 결과 영역은 게임명 대신 "게임 ID N" 텍스트 표기(/vote/results 응답에 gameName 미포함, W2-6 소비 형태 안정화 전 Map 유지 — 설계 §시그니처 주석과 정합). 막대 그래프 대신 텍스트 count(설계 "1차는 종료 후 count 단순 노출" §비목표와 정합). 후속 개선 후보이며 AC 위반 아님.