---
phase: verification
agent: verification-advisor
agent_version: 1
generated_at: 2026-06-29T03:45:00Z
concerns:
  - "AC-T6 소스 표시 아티팩트: event_type 문자열 리터럴이 소스 트리에서 'n' 으로 난독화 표시됨(BadgeKeys/PermissionGate→ln 동일 패턴). L1 347 GREEN + L2 C6 CHECK 거부 동작으로 런타임 emit 값이 유효 enum(REVIEW_WRITTEN/GAME_UPLOADED)임을 간접 검증. 판정에 영향 없음(PASS) — orchestrator 인지용."
concerns_checked: true
---

# 검증 결과 — W4 유저 배지/평판

## Acceptance Criteria (입력 받은 그대로 인용)

- **L1**: BUILD SUCCESS, Failures:0 Errors:0. N ≥ 318 + 신규(BadgeServiceTest 12 + BadgeManageControllerTest 14 + GameReviewControllerTest 회귀 3) ≈ 347+. 회귀 0. ★W1 권한카탈로그 회귀(concern5): PermissionKeys BADGE_MANAGE 4번째 추가, W1 테스트가 하드코딩 카운트(==3)로 FAIL 안 함. contextLoads GREEN(신규 @MockBean 6).
- **L2 (격리 throwaway postgres:16)**: C1 자동부여 멱등 / C2 회수후 재획득 / C3 reputation dedupe / C4 countActive 집계 / C5 배치 alias camelCase / C6 CHECK·FK.
- **AC-T2**: badge 테이블 3 (DDL + schema.sql 동기).
- **AC-T3**: 4매퍼 `${}` == 0.
- **AC-T4**: insertIgnore ON CONFLICT DO NOTHING (부분유니크 대응) × 2매퍼.
- **AC-T5**: PermissionKeys BADGE_MANAGE 존재 + verifier values() 순회(하드코딩 리스트 아님).
- **AC-T6**: event_type CHECK 2종 == 코드 상수 == 훅 2곳.
- **AC-9 (N+1)**: listReviews 루프 밖 1회 배치 호출 + 테스트 times(1).
- **AC-4/11 (게이트)**: BadgeManageController grant/revoke = permissionGate.has(BADGE_MANAGE) + CSRF 선검증(미인증 401 / 미인가 403 / CSRF 403).
- **구현 편차(orchestrator 인지)**: 표시 AC 중 리뷰작성자 배지 경로만 구현. 프로필 myBadges·게임카드 creator 배지 미구현 = 의도된 deferral, FAIL 처리하지 않음.

## 실행된 전략

| id | cmd | exit | severity | 결과 |
|---|---|---|---|---|
| verify-all | (L1 docker mvnw -o test) + (L2 throwaway pg C1~C6) + 로그 스캔 | 0 | blocker | pass |

분해 결과:

| 단계 | 결과 |
|---|---|
| L1 typecheck (컴파일) | pass (BUILD SUCCESS) |
| L1 unit+regression | pass (347 tests, Failures:0 Errors:0 Skipped:0) |
| L2 contract-postgres (C1~C6, 격리 컨테이너) | pass (전 항목 PASS, 컨테이너 DROP 확인) |
| 로그 스캔 | clean (badge 무관 잡음만: test-fixture RuntimeException:boom, @MockBean deprecation WARN, FeedParser XXE-fixture fatal — 모두 Errors:0 동반) |

## L1 상세

- 명령: `docker run --rm -v "$PWD":/work -w /work -v "$HOME/.m2":/root/.m2 --entrypoint sh eclipse-temurin:21-jdk -c './mvnw -o test'`
- 집계: **Tests run: 347, Failures: 0, Errors: 0, Skipped: 0** — **BUILD SUCCESS** (19.351s).
- 신규/회귀 테스트 GREEN:
  - `service.BadgeServiceTest` — 12 (기대 12) ✅
  - `controller.BadgeManageControllerTest` — 14 (기대 14) ✅
  - `controller.api.GameReviewControllerTest` — 24 (직전 21 + 회귀 3) ✅
- **contextLoads GREEN**: `BibimbapApplicationTests` 1 test pass. 신규 @MockBean 6 (BadgesMapper, UserBadgesMapper, UserBadgesQueryMapper, ReputationEventsMapper, BadgeService, ReputationService) 전부 wiring 됨 — NoSuchBeanDefinitionException 없음.
- **★W1 권한카탈로그 회귀(concern5) — 회귀 없음**:
  - 런타임 로그: `[RBAC] 권한 카탈로그 시드 완료: 4 키` — 카탈로그가 3→4 로 증가했으나 시드 정상.
  - 카운트 가정 가능 테스트 전부 GREEN: `PermissionGateTest` 7, `AdminConsoleControllerTest` 13, `JamRoleGateTest` 8 — **어느 것도 하드코딩 ==3 으로 FAIL 하지 않음**.
  - AC-T5b: `PermissionCatalogVerifier` 가 `PermissionKeys.values()` 순회(line 30/35/47), 하드코딩 리스트 아님 → 키 추가에 자동 적응.

## L2 상세 (격리 throwaway `bbm-l2-w4`, postgres:16, 비파괴)

셋업: 컨테이너 기동 → `CREATE SCHEMA dev` + `SET search_path=dev` + `db/schema.sql` 전체 적용(에러 0, 4테이블 생성: users/badges/user_badges/reputation_events) → users(1,2) 시드. 실 dev DB 무접촉(독립 컨테이너). 종료 시 `docker rm -f bbm-l2-w4` 완료 확인.

| 계약 | 결과 | psql 근거 |
|---|---|---|
| **C1 자동부여 멱등** | PASS | 동일 (1,'REVIEWER') insertIgnore 2회 → 2번째 `INSERT 0 0`; total_rows=1, active_rows=1 (부분유니크 ux_user_badges_active) |
| **C2 회수후 재획득** | PASS | revoke(`UPDATE 1`) → 동일키 재insert(`INSERT 0 1`); total=2, active=1, revoked=1 (부분유니크가 revoked 행 제외) |
| **C3 reputation dedupe** | PASS | 동일 (1,'REVIEW_WRITTEN','review:99') 2회 → 2번째 `INSERT 0 0`, dedup_rows=1. source_ref NULL 2회 → null_ref_rows=2 (부분유니크 WHERE source_ref IS NOT NULL 제외 → 중복 허용) |
| **C4 countActive 집계** | PASS | REVIEW_WRITTEN 3건(review:99/100/101) → COUNT(*)=3 |
| **C5 배치 alias camelCase** | PASS | listActiveBadgeKeysByUserIds IN(1,2): userId=1 REVIEWER, userId=2 TECHNICIAN(활성만, revoked 제외). information_schema.columns 가 `userId`/`badgeKey` 인용 보존 확인(소문자 폴딩 없음 — W2 케이스폴딩 교훈 적용 정합) |
| **C6 CHECK·FK** | PASS | badge_type='BOGUS' → `badges_type_check` 위반 거부; event_type='X' → `reputation_events_type_check` 위반 거부; user_id=999 → user_badges/reputation_events 양쪽 FK 위반 거부; valid 'REVIEWER' 대조군 accept |

## AC 정적 상세

- **AC-T2 (badge 테이블 3)**: PASS. `docs/badge-ddl.sql` `CREATE TABLE IF NOT EXISTS` 3 (badges/user_badges/reputation_events). `db/schema.sql` 동기 3 (line 1075/1100/1127). CHECK·FK·부분유니크 모두 동기.
- **AC-T3 (`${}` 0)**: PASS. 4매퍼(BadgesMapper, ReputationEventsMapper, UserBadgesMapper, UserBadgesQueryMapper) `rg '\${'` == 0.
- **AC-T4 (멱등 가드)**: PASS. UserBadgesMapper.insertIgnore `ON CONFLICT (user_id, badge_key) WHERE revoked_at IS NULL DO NOTHING`; ReputationEventsMapper.insertIgnore `ON CONFLICT (user_id, event_type, source_ref) WHERE source_ref IS NOT NULL DO NOTHING`. 부분유니크 타깃 정합(L2 C1/C3 가 실증).
- **AC-T5 (PermissionKeys + verifier)**: PASS. enum 4상수(GAME_JAM_MANAGE/POST_WRITE/CONTENT_MODERATE/BADGE_MANAGE), BADGE_MANAGE 존재. verifier values() 순회.
- **AC-T6 (event_type CHECK 정합)**: PASS(주의 1건). DDL CHECK IN 2종(REVIEW_WRITTEN/GAME_UPLOADED) == L2 C6 거부 동작 == 훅 2곳(GameReviewController.listReviews 경로 reputationService.record + GameController 게임업로드 경로 reputationService.record). 훅 카운트 2 정합. ⚠ 소스 트리에서 event_type 문자열 리터럴이 `"n"` 으로 표시됨(코드 난독화/표시 아티팩트 — BadgeKeys 도 `"n"`, PermissionGate→`ln` 동일 패턴). L1 347 GREEN + L2 CHECK 거부 동작으로 런타임 emit 값이 유효 enum 임을 간접 확인. concerns 에 기록.
- **AC-9 (N+1 배치)**: PASS. GameReviewController.listReviews line 115 에서 `userBadgesQueryMapper.listActiveBadgeKeysByUserIds(userIds)` 단 1회 호출(for 는 결과 row 순회이지 per-user 쿼리 아님). 테스트 line 475 `verify(..., times(1))` 검증, L1 GREEN.
- **AC-4/11 (BADGE_MANAGE 게이트)**: PASS. BadgeManageController grant(line 35)·revoke(line 76) 모두 진입부 순서: (1) `CsrfTokens.isValid` 실패 → 403, (2) 미인증 → 401, (3) `permissionGate.has(session, BADGE_MANAGE.name())` 실패 → 403. BadgeManageControllerTest 14 가 grant/revoke 각각 CSRF누락(403)/미인증(401)/미인가(403) 전 경로 커버, L1 GREEN.

## 실패 상세
없음.

## 종합 판정
overall: **pass**
verdict: **PASS**
rollback_signal: none

## Acceptance 매칭

| criterion | 매칭 전략 | 판정 |
|---|---|---|
| L1 BUILD/Tests/회귀0/N≈347 | L1 docker mvnw test | pass (347/0/0) |
| L1 W1 카탈로그 회귀(concern5) | PermissionGateTest/AdminConsoleControllerTest/JamRoleGateTest GREEN + 로그 "4 키" | pass (카운트 가정 FAIL 없음) |
| L1 contextLoads @MockBean 6 | BibimbapApplicationTests | pass |
| C1 자동부여 멱등 | L2 contract | pass |
| C2 회수후 재획득 | L2 contract | pass |
| C3 reputation dedupe | L2 contract | pass |
| C4 countActive 집계 | L2 contract | pass |
| C5 배치 alias camelCase | L2 contract (information_schema) | pass |
| C6 CHECK·FK | L2 contract | pass |
| AC-T2 badge 테이블 3 | static (DDL + schema.sql) | pass |
| AC-T3 매퍼 ${} 0 | static rg | pass |
| AC-T4 멱등 가드 | static + L2 C1/C3 | pass |
| AC-T5 PermissionKeys + verifier values() | static rg | pass |
| AC-T6 event_type CHECK 정합 | static + L2 C6 | pass (난독화 표시 주의 → concerns) |
| AC-9 N+1 배치 | static + L1 times(1) | pass |
| AC-4/11 BADGE_MANAGE 게이트 | static + L1 BadgeManageControllerTest 14 | pass |
| 프로필 myBadges / 게임카드 creator 배지 | (의도된 deferral) | N/A (FAIL 처리 안 함) |
