docs(w3-w4): W3(잔여)+W4 변경이력·구현추적 + 회고 교훈 4건 반영

- docs/changes/2026-06-29-w3-w4-features.md 신규 — W3-1/3-4/3-3/3-5/W4 5기능 변경(테이블·컨트롤러·매퍼·보안게이트·검증 347/347 + 커밋 35f1dc3~305cc73) + needs_user_verification
- docs/changes/index.md 링크 추가
- work-log 풀설계 요약 구현추적 갱신 — "W3(잔여)·W4 설계만(미구현)" → 11기능 전부 구현 완료 + 교차링크
- verification-strategies.md 회고 교훈 4건 append: (1) 보안 거부 테스트 차별 입증(vacuous 회피) (2) 보안핵심 verification+adversarial 병렬 (3) 표시면 의존 scope-fence 예외 (4) 설계 트레이드오프 1차 결론 못박기 + 프로토콜 개선 권고 5건

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
This commit is contained in:
이정수 2026-06-29 12:54:50 +09:00
parent 305cc73860
commit 31554f8886
4 changed files with 249 additions and 1 deletions

View File

@ -0,0 +1,218 @@
---
kind: change
title: "W3(잔여) + W4 기능 구현 (W3-1·W3-4·W3-3·W3-5·W4)"
session_id: 20260629-100849
created_at: 2026-06-29
status: implemented
related_design_summary: "../work-log/2026-06-23-w2-w4-full-design-summary.md"
related_design: "../../.atp/work-session/20260623-104307/implementation/"
related_impl: "../../.atp/work-session/20260629-100849/implementation/"
related_verification: "../../.atp/work-session/20260629-100849/verification.md"
related_report: "../../.atp/work-session/20260629-100849/report.md"
related_security_checklist: "../security/security-remediation-checklist.md"
related_prev_change: "./2026-06-24-w2-jam-platform.md"
---
# W3(잔여) + W4 기능 구현 변경 이력 (W3-1·W3-4·W3-3·W3-5·W4)
상위 설계 인덱스: [W2+W3(잔여)+W4 풀설계 통합 요약](../work-log/2026-06-23-w2-w4-full-design-summary.md) §설계 인덱스 W3-1/W3-3/W3-4/W3-5/W4.
설계 본문: `.atp/work-session/20260623-104307/implementation/W3-*.md` · `W4-badges-design.md` · 파일 소유권/구현 보고: `.atp/work-session/20260629-100849/implementation/ownership*.md` · 검증: `.atp/work-session/20260629-100849/verification.md` · 세션 보고: `.atp/work-session/20260629-100849/report.md`.
## 개요
W2 게임잼 워크스트림([2026-06-24-w2-jam-platform.md](./2026-06-24-w2-jam-platform.md)) 완료 후, W2-W4 풀설계 11기능 중 미구현으로 남았던 잔여 5기능을 순차 구현했다. W3-2(댓글/리뷰)는 이미 완료 상태였고, 본 작업은 **태그+검색(W3-1) → 메인허브(W3-4) → 포스팅보드(W3-3) → 업로드보안(W3-5) → 배지/평판(W4)** 순서로 각 기능을 `impl → verify → commit` 롤백 경계로 묶어 진행했다(5 feat 커밋).
| 서브 | 커밋 | 핵심 | L1 누적 |
|---|---|---|---|
| W3-1 태그 + 검색 확장 | `35f1dc3` | tags/game_tags/jam_tags/game_views + searchGamesAdvanced(다중태그 AND/OR·정렬키6·방문수·리뷰정렬·잼라우트) + TagController(sanitize/금칙어/CONTENT_MODERATE) | 235/235 |
| W3-4 메인 허브 | `e28fa60` | keyset 페이징(혼합방향 OR분해) + 진행중 잼 배너(`/games/search?jam=`) | 243/243 |
| W3-3 포스팅 보드 | `6047a39` | posts/categories + POST_WRITE enforcement + commonmark/jsoup sanitize + SsrfSafeFetcher(9항목) + 유니티 피드 폴링(@Scheduled) | 287/287 |
| W3-5 업로드 보안 | `08d191f` | ZipSecurity(zip-slip/심링크/zipbomb/포맷/권한게이트) + ATOMIC_MOVE swap + game_upload_audit_log | 318/318 |
| W4 유저 배지/평판 | `305cc73` | badges/user_badges/reputation_events + 자동(임계)·수동(BADGE_MANAGE) 하이브리드 + reputation 감사 + 리뷰작성자 표시 | 347/347 |
커밋 범위: `35f1dc3`(W3-1) → `e28fa60`(W3-4) → `6047a39`(W3-3) → `08d191f`(W3-5) → `305cc73`(W4). 모두 `feat/v2` 브랜치.
신규 DDL 5종: `docs/tag-ddl.sql` · `docs/games-hub-ddl.sql` · `docs/board-ddl.sql` · `docs/game-upload-ddl.sql` · `docs/badge-ddl.sql`(전부 `db/schema.sql` 동기). 신규 pom 의존 2종: jsoup 1.17.2 + commonmark 0.22.0(W3-3).
---
## W3-1 — 태그 + 검색 확장 (`35f1dc3`)
### DDL (신규 4테이블 + games 컬럼 1)
| 객체 | 역할 | 핵심 제약 |
|---|---|---|
| `tags` | 통합 태그 사전 | 정규화 slug 활성 UNIQUE |
| `game_tags` | 게임-태그 조인 | (game_id, tag_id) UNIQUE, `#{}` 바인딩 |
| `jam_tags` | 잼-태그 조인 | (jam_id, tag_id) UNIQUE |
| `game_views` | 방문수 dedupe 원장 | 최근 interval 내 재방문 제외 근거 |
| `games.view_count` | 누적 방문수 캐시 컬럼 | game_views 적재 시 증가 |
- DDL 권위: `docs/tag-ddl.sql`(멱등) + `db/schema.sql` 동기.
### 런타임 동작
- `GamesMapper.searchGamesAdvanced` — 다중태그 **AND/OR** 분리(`GameTagsMapper.listGameIdsByTagsAll`/`...Any`, AND ⊆ OR 불변), 정렬키 6종, 방문수·리뷰(`game_review_stats` 소비) 정렬, 잼 필터 라우트. `SearchCriteria` POJO 로 파라미터 캡슐화.
- `SearchController``GET /games/search` 태그·정렬·잼 파라미터 처리(파라미터 전수 `#{}`). index.jsp SSR.
- `GameViewsMapper.existsRecentView` — 최근 방문 dedupe(설계 dead param `sinceTs` 제거). `GameController` 방문 적재 훅.
- `TagController` — 태그 생성/부착. `TagSanitizer`(normalize + `banned-words.txt` 금칙어) + **CONTENT_MODERATE 게이트**(신규 권한키 미도입, W1 선언키 재사용) + CSRF.
### 의미보존 편차(verification 인지)
- `insertTag``int` + generatedKeys 패턴(설계 표현 대비 의미보존).
- `TagController``GamesMapper` 주입으로 부착 대상 게임 소유자 확인(설계 의도 충족).
### 검증
L1 235/235 GREEN(baseline 190→235, +45 신규테스트, 회귀 0). L2 격리 throwaway DB: alias 케이스폴딩 인용/비인용 대조, NULLS LAST, AND ⊆ OR, ILIKE, interval dedupe. AC-1/2/4/5 + 보안 VP PASS.
---
## W3-4 — 메인 허브 (`e28fa60`)
### DDL
신규 테이블 0(`GamesMapper`/`JamsMapper` 확장만). `docs/games-hub-ddl.sql` 는 keyset 인덱스 등 허브 지원 객체.
### 런타임 동작
- `GamesMapper` keyset 페이징 — **혼합 정렬방향을 OR 분해**한 row-comparison 으로 경계 무중복·무누락 보장(검색·비검색 단일화). 단계1(keyset 더보기) + 단계2(진행중 잼 배너) 통합 구현.
- `JamsMapper.listActive`(limit=1) — 진행중 잼 1건 배너용. `WebMvcController` 가 hub 에 주입.
- 진행중 잼 배너 → W3-1 `GET /games/search?jam={slug}` 라우팅(cross-W 정합).
### 검증
L1 243/243 GREEN(회귀 0, 편차 0). L2: keyset 혼합방향 OR분해 경계 무중복·무누락·tie-break 실측(throwaway DB). AC-T2/T3/T4 + 단계무결성 + 보안 VP PASS. `BibimbapApplicationTests` 무변경(GamesMapper/JamsMapper @MockBean 기존 등록 재사용).
---
## W3-3 — 포스팅 보드 (`6047a39`)
가장 복잡한 워크스트림(SSRF 9항목·md sanitize·다매퍼·@Scheduled·pom 의존). 검증 단계에서 verification + **adversarial SSRF 감사** 2중 검증이 vacuous(거짓통과) SSRF 결함을 포착해 backward 1회 보정(아래 §SSRF 강화)했다.
### DDL (신규 4테이블) + pom 의존
| 객체 | 역할 |
|---|---|
| `post_categories` | 게시판 카테고리 |
| `posts` | 게시글(마크다운 본문) |
| `unity_feed_sources` | 유니티 외부 피드 소스 |
| `unity_feed_items` | 폴링 적재 피드 아이템(dedupe) |
- DDL 권위: `docs/board-ddl.sql`(멱등) + `db/schema.sql`(append) 동기.
- pom 신규 의존: **jsoup 1.17.2**(1.14+ Safelist API 정합) + **commonmark 0.22.0**. 오프라인 빌드용으로 프록시 CA 주입 1회 pre-resolve 후 `~/.m2` 캐시(이후 오프라인 빌드 가능).
### 런타임 동작
- `PostController` — 게시글 작성/조회. **POST_WRITE enforcement 연결**(W1 선언키의 첫 소비처) + CSRF.
- `PostMarkdownService` — commonmark 렌더 → **jsoup Safelist sanitize**(XSS 차단). 검증에서 XSS sanitize SOUND.
- `OgPreviewService` / `UnityFeedPoller``SsrfSafeFetcher` 단일 경유 외부 fetch. `FeedParser`(RSS 2.0 / RFC1123 pubDate).
- `UnityFeedPoller` `@Scheduled` 폴링 + dedupe insert(`SchedulingConfig` `@EnableScheduling` 신규 활성).
- `PostAdminController` / `UnityFeedAdminController` — 카테고리·피드소스 관리(JSP 는 프로젝트 컨벤션 scriptlet + `HtmlUtils.htmlEscape` 로 정렬, JSTL 의존 부재 대응).
### SSRF 9항목 (`SsrfSafeFetcher`)
scheme allowlist · 사설/루프백 IP 차단 · DNS rebinding 방어 · redirect 재검증 · size/timeout 상한 등 9항목. 초기 구현은 `Host` restricted-header 를 통한 fetch 가 전건 empty 가 되어 **테스트가 vacuous(거짓통과)** 였고 CGNAT(100.64/10) 등 차단 범위갭이 있었다. backward 보정에서:
- **option-b hostname-connect 전환** — 사전 resolve 한 IP 를 차단 검사 후 hostname 으로 직접 connect(restricted-header 우회 제거).
- 차단 대역 확장(CGNAT 등).
- `FeedParser` RFC1123 pubDate 파싱 robust 화(선행 요일토큰 요일-날짜 불일치에도 견딤 — orchestrator 마이크로수정).
### 검증
backward: verification FAIL(L1 283, 2fail+1err) + adversarial SSRF 감사 CRITICAL → impl fix1 → 재검증 FAIL(FeedParser 잔존) → orchestrator 마이크로수정 → **L1 287/287 GREEN**. L2: board keyset row-comparison · insertIgnoreDup ON CONFLICT · FK(throwaway DB). adversarial 감사: XSS SOUND, 보정 후 SSRF 우회 차단.
---
## W3-5 — Unity WebGL 업로드 보안 보강 (`08d191f`)
### DDL (신규 1테이블)
- `game_upload_audit_log` — 업로드 감사(outcome CHECK, FK, reject_reason enum). `docs/game-upload-ddl.sql`(멱등) + `db/schema.sql`(append) 동기.
### 런타임 동작
- `ZipSecurity`(+ `ZipRejectException`) — **zip-slip canonical 경계 + 심링크 거부 + zip bomb(getCompressedSize 0 회피·3중상한) + 포맷검증(wasm/data/loader) + 권한게이트**. 거부 사유 enum 11종.
- `GameUploadController` — 게이트 순서 정렬 + 임시추출 후 `ATOMIC_MOVE` swap(원자 교체) + audit 적재. dead code 제거.
- `GameUploadAuditMapper`(+ `GameUploadAudit`/`GameAssetOwner` POJO) — `#{}` only, 컬럼 8 정합.
- `GameAssetCleanupService` — 자산 생명주기 정리(UUID 검증 + 경계, gameRoot 미터치).
### 검증
L1 318/318 GREEN(편차 0). L2: audit insert · outcome CHECK · FK · `findGameByWebglUuid` alias(throwaway DB). **adversarial zip-slip 감사 SOUND** — 심링크 TOCTOU 구조불가, 경로탈출/유니코드/zipbomb/LIKE주입 전수 차단. LOW 2건(split 빈세그먼트·static 트리 `.tmp` 노출)은 open_item.
---
## W4 — 유저 배지 / 평판 (`305cc73`)
### DDL (신규 3테이블) + 권한키
| 객체 | 역할 | 핵심 제약 |
|---|---|---|
| `badges` | 배지 카탈로그 | `badges_type_check` |
| `user_badges` | 유저 보유 배지 | `ux_user_badges_active` 부분유니크(revoked 행 제외 → 회수후 재획득) |
| `reputation_events` | 평판 이벤트 감사 | `reputation_events_type_check` IN(REVIEW_WRITTEN, GAME_UPLOADED), source_ref 부분유니크 dedupe |
- DDL 권위: `docs/badge-ddl.sql`(멱등) + `db/schema.sql`(line 1075/1100/1127) 동기.
- **`PermissionKeys.BADGE_MANAGE` 신규(4번째 권한키)** — W1 권한 카탈로그 카운트 **3→4** 갱신. `PermissionCatalogVerifier``values()` 순회(하드코딩 리스트 아님)라 키 추가에 자동 적응, 카운트 가정 테스트 회귀 0.
### 런타임 동작
- `BadgeService` / `ReputationService` — 자동(임계 도달 시 부여)·수동(grant/revoke) 하이브리드. `BadgeCatalogSeeder` 시드.
- 자동 부여 훅 2곳: `GameReviewController.listReviews` 경로(리뷰 작성) + `GameController` 게임 업로드 경로 → `reputationService.record`. event_type CHECK 2종과 정합.
- `BadgeManageController``/manage/badges` grant/revoke. 진입부 순서 CSRF(403) → 미인증(401) → `BADGE_MANAGE` 게이트(403).
- 표시: 리뷰 작성자 배지 칩(`listReviews` JSON + game-detail.jsp JS, `UserBadgesQueryMapper.listActiveBadgeKeysByUserIds` 배치쿼리로 N+1 차단) + profile.jsp 본인 배지.
### 검증
L1 347/347 GREEN(baseline 318→347, BadgeServiceTest 12 + BadgeManageControllerTest 14 + GameReviewControllerTest 회귀 3). **W1 권한 카탈로그 회귀 0**(런타임 로그 "권한 카탈로그 시드 완료: 4 키", 카운트 가정 테스트 전부 GREEN). L2: 자동부여 멱등 · 회수후 재획득 부분유니크 · reputation dedupe · countActive 집계 · 배치 alias camelCase 인용 · CHECK/FK(throwaway DB). contextLoads 신규 @MockBean 6.
---
## 보안 (프로젝트 보안 원칙 추적)
본 작업에서 신설·연결된 인가/검증/방어 게이트. 모두 단위 + L2 DB 계약(+ 보안핵심 2건은 adversarial 코드추적 감사)으로 검증.
| 게이트 | 적용 W | 내용 |
|---|---|---|
| CONTENT_MODERATE 재사용 | W3-1 | 태그 생성/부착 게이트. W1 선언키 재사용(신규키 0). |
| POST_WRITE enforcement | W3-3 | W1 선언키의 첫 enforcement 연결(이전 소비처 0). 게시글 작성 게이트. |
| BADGE_MANAGE 신규 | W4 | 4번째 권한키 신규. 배지 grant/revoke 게이트(`/manage/badges`). W1 카탈로그 3→4. |
| SsrfSafeFetcher 9항목 | W3-3 | scheme allowlist·사설/루프백 IP 차단·DNS rebinding·redirect 재검증·size/timeout. option-b hostname-connect(restricted-header 우회 제거). |
| 마크다운 sanitize | W3-3 | commonmark 렌더 후 jsoup Safelist sanitize(XSS 차단). adversarial XSS SOUND. |
| zip-slip / 심링크 / zipbomb | W3-5 | ZipSecurity canonical 경계 + 심링크 거부 + 3중상한. adversarial zip-slip SOUND. ATOMIC_MOVE 원자 교체. |
| 방문수 dedupe | W3-1 | game_views interval 기반 재방문 제외(view_count 부풀림 방지). |
| CSRF 전수 | 전 W(상태변경) | 모든 상태 변경 핸들러 CSRF 선검증 → 누락 시 403. |
| MyBatis `${}` 0건 | 전 W | 신규 매퍼 전수 `#{}` 바인딩, 동적 치환 0. |
| §33 alias 케이스폴딩 | W3-1/4, W4 | VIEW/집계/배치 매퍼 camelCase alias 큰따옴표 인용 — Postgres 소문자 폴딩 회피. |
---
## 검증 결과 요약
최종 컨테이너 `./mvnw -o test`(eclipse-temurin:21-jdk + `~/.m2` 마운트, 오프라인) **347/347 GREEN**(baseline 190 → 누적 +157 신규테스트, 회귀 0).
| 서브 | L1 누적 | L2 dev DB contract(격리 throwaway) | 비고 |
|---|---|---|---|
| W3-1 | 235/235 GREEN | PASS(alias 폴딩 대조·NULLS LAST·AND⊆OR·ILIKE·interval dedupe) | +45 신규, 편차 2(의미보존) |
| W3-4 | 243/243 GREEN | PASS(keyset 혼합방향 OR분해 무중복·무누락·tie-break) | 편차 0 |
| W3-3 | 287/287 GREEN | PASS(board keyset·insertIgnoreDup·FK) | backward 1회(verification+adversarial SSRF 2중검증이 vacuous 포착) |
| W3-5 | 318/318 GREEN | PASS(audit·outcome CHECK·FK·alias) | adversarial zip-slip SOUND |
| W4 | **347/347 GREEN** | PASS(멱등·재획득 부분유니크·dedupe·집계·배치 alias·CHECK/FK) | W1 카탈로그 3→4 회귀 0 |
- **L2 비파괴 원칙**: 모든 L2 contract 는 격리 throwaway postgres:16(생성 → DDL 적용 → 실측 → DROP)로 수행. 사용자 dev(`bibimbap`) DB **무접촉**(전 W).
- **2중 검증 가치**: W3-3 에서 verification(테스트 실행)이 통과시킨 vacuous SSRF 를 adversarial 감사(코드추적)가 포착 → option-b 전환으로 실질 차단 확보.
상세는 `.atp/work-session/20260629-100849/verification.md` + `report.md` §verified_by_me 참조.
---
## 미적용 / 이월 (needs_user_verification)
| 항목 | 사유 |
|---|---|
| 실 dev DB(`bibimbap`) DDL 미적용 | L2 는 throwaway DB 로 통과했으나 실 dev DB 에는 미적용(파괴적 조작 회피). 신규 DDL 5종(tag/games-hub/board/game-upload/badge) **+ (W2 carryover) jam DDL 3종 = 총 8종** 을 `db/apply-local-ddl.sh` 로 일괄 적용 필요(§6 수동게이트). L3 선행. |
| L3 런타임 스모크(WAR 기동 후) | ① 검색 `/games/search` 태그·정렬·잼필터 + 방문수 dedupe ② 허브 `/` keyset 더보기 + 진행중 잼 배너 ③ 포스팅 작성(POST_WRITE)/sanitize 렌더/OG 미리보기/유니티 피드 폴링 + **★실 HTTPS 외부 fetch 동작**(option-b hostname-connect 실 TLS — 단위는 HTTP만) ④ 업로드 zip-slip/포맷 거부 + 정상 Unity 빌드 + replaceUuid 교체 ⑤ 배지 자동부여(리뷰·업로드)·수동 grant/revoke(BADGE_MANAGE)·리뷰작성자 칩. 자동 미수행. |
| **W4 표시 deferral** | 리뷰작성자 배지 경로만 구현. **프로필 myBadges 주입·게임카드 creator 배지 미구현**(의도된 deferral). 게임카드 SSR 은 `WebMvcController` hub + `SearchController` 가 담당(W3-4/타 W 산출물)이라 본 작업 범위 밖. 후속 작업에서 WebMvcController/SearchController 수정으로 creator 배지 부착 필요. profile.jsp 본인 배지는 구현됨. |
| W3-3 SSRF 잔여 rebinding TOCTOU | option-b hostname-connect 의 connect 시 재resolve 창(DNS 캐시로 최소화, 위협모델 LOW — URL 제출자=권한자). 하드닝 옵션: `networkaddress.cache.ttl` 설정 검토. |
| W3-5 배포 하드닝 | (1) **업로드 저장루트 static 트리 밖 이전**(LOW: static 트리 `.tmp` 노출 가능) (2) **gameRoot 경로 이중중첩 정합**(`app.upload.game-storage-path=src/main/resources/static/game` → `resolve('game')``.../static/game/game`). 설계 스코프-아웃(canonical 경계는 보장). dev 자산 위치 영향이라 사용자 config 결정 필요. |

View File

@ -6,4 +6,5 @@
- [2026-06-18-w3-2-comments-reviews.md](./2026-06-18-w3-2-comments-reviews.md) — W3-2 댓글/리뷰 분리 구현. game_comments 서버 영속화 전환 + game_reviews 도메인 신설. 신규 API 9개(댓글 C1~C4, 리뷰 R1~R5), DDL 2종, 권한(작성자/운영자), cascade 확장. L1 31테스트 PASS. L3 스모크·DDL 적용은 needs_user_verification. 좋아요는 범위밖.
- [2026-06-22-w3-2-comments-reviews-enhancement.md](./2026-06-22-w3-2-comments-reviews-enhancement.md) — W3-2 고도화(코어 위에 얹음). 다축 평점(game_review_axes 6축·육각형 SVG 레이더) + A1~A3 일관성(commentView 통일·작성자 마스킹 QG-2 해결·edited/updatedAt) + B1~B3 목록규모(페이지네이션·본문10자·TextNormalizer) + C1~C6 UX + game_review_stats 집계뷰(C3 재분류 — W2-3 동결 무관 확정). 신규 4파일+변경 13. L1 43/43 GREEN. DDL·L3 스모크 needs_user_verification.
- [2026-06-29-w3-w4-features.md](./2026-06-29-w3-w4-features.md) — W3(잔여)+W4 5기능 구현(W3-1·W3-4·W3-3·W3-5·W4). 태그+검색(tags/game_tags/jam_tags/game_views·다중태그 AND/OR·정렬키6·방문수 dedupe·CONTENT_MODERATE) + 메인허브(keyset 혼합방향 OR분해·진행중 잼 배너) + 포스팅보드(posts/categories·POST_WRITE enforcement·commonmark/jsoup sanitize·SsrfSafeFetcher 9항목 option-b·@Scheduled 유니티피드) + 업로드보안(ZipSecurity zip-slip/심링크/zipbomb·ATOMIC_MOVE·game_upload_audit_log) + 배지/평판(badges/user_badges/reputation_events·자동임계+수동 BADGE_MANAGE 하이브리드·리뷰작성자 칩). 신규 DDL 5종 + pom jsoup/commonmark. 5 커밋 35f1dc3~305cc73. 최종 L1 347/347 GREEN(회귀 0), L2 dev DB contract 전 PASS(격리 throwaway DB), adversarial SSRF/zip-slip 감사(W3-3 vacuous SSRF 포착→option-b 전환). 실 dev DB DDL 8종 일괄 적용·L3 스모크·W4 표시 deferral(게임카드 creator 배지) needs_user_verification.
- [2026-06-24-w2-jam-platform.md](./2026-06-24-w2-jam-platform.md) — W2 게임잼 워크스트림 전체(W2-1~6) 구현. 잼 엔티티/라이프사이클(jams/jam_teams/jam_team_members/jam_entries/jam_status_log) + 심사위원 역할(jam_judges 잼스코프) + 평가 동결 스키마(jam_criteria/jam_scores/jam_votes/jam_awards + jam_score_stats VIEW, 평가단위 (jam_id,game_id) 활성 자연키) + 심사 평가(3중게이트 UPSERT 가중집계) + 인기투표(1인1표 UNIQUE·종료후 공개) + 시상 집계(3트랙+가중 GRAND·CLOSED 확정 멱등). GAME_JAM_MANAGE 첫 enforcement 연결·잼스코프 isJudge·평가기간 게이트·CSRF 전수. 6 커밋 ccf1e42~a74bf74. 최종 L1 190/190 GREEN(회귀 0), L2 dev DB contract 전 PASS(격리 throwaway DB). dev DB 마이그레이션·L3 스모크 needs_user_verification.

View File

@ -187,6 +187,30 @@ design 파일 영향맵에서 신규 테스트 파일이 owner 칸에 `(검증)`
> 근거: W2 세션(20260624) — W2-1 `JamAdminController``response(HttpStatus,String)` 44회 호출하나 정의 누락(`JamController` 는 보유) → L1 컴파일 24 errors. backward 보정으로 헬퍼 1개 추가 후 통과.
### 보안 거부 테스트는 정상경로 성공과 쌍으로 차별 입증 (vacuous 회피)
보안 방어(SSRF·인증 게이트 등)의 "거부" 단위테스트가 `assertTrue(result.isEmpty())` 류만 검사하면, 방어 로직이 깨져 **항상 empty 를 반환**해도 테스트가 통과한다(vacuous PASS — 거부를 입증하지 못함). 특히 graceful `catch(Exception)→empty` 패턴은 정상경로 예외(예: 잘못된 헤더 설정)를 삼켜 정상 fetch 실패를 숨긴다. → **정상 입력 성공(present) 테스트와 차단 입력 empty 테스트를 쌍으로** 두어, 성공테스트가 GREEN 이어야 차단테스트의 empty 가 "차단되어 empty"임이 구별 입증된다.
> 근거: W3-3 세션(20260629) — `SsrfSafeFetcher``Host` restricted-header 로 모든 fetch 가 예외→empty. SSRF 거부 테스트 17건이 전부 vacuous PASS, 정상경로 테스트만 RED 로 노출. adversarial 감사가 근본원인 진단.
### 보안핵심 기능은 verification + adversarial 코드감사 병렬
SSRF·zip-slip·경로 boundary·sanitize 같은 보안핵심은 verification-advisor(테스트 실행 판정)만으로 부족하다. 작성자 테스트가 못 짠 우회벡터(IP 대체인코딩·IPv6 변형·DNS rebinding·redirect 우회·심링크 TOCTOU·LIKE 와일드카드)는 **코드를 직접 추적하는 adversarial 감사**가 잡는다. 두 검증을 병렬로 걸고, 감사가 BYPASS/CRITICAL 을 내면 backward.
> 근거: W3-3 SSRF(감사가 Host-header vacuous + CGNAT 100.64/10 미차단 포착 — verification 단독으로는 무력 배포할 뻔) / W3-5 zip-slip(감사 SOUND — 심링크 TOCTOU 가 ZipInputStream 구조상 불가임을 코드추적으로 확정).
### 표시면이 선행 기능 컨트롤러에 의존하면 scope-fence 예외 선언
기능 A 의 UI 표시가 선행 기능 B 의 컨트롤러/뷰(예: 목록·카드 SSR)에 배선돼야 할 때, "B 산출물 접근 금지" scope-fence 를 그대로 두면 A 의 표시 AC 가 미완으로 남는다. orchestrator 는 표시면 의존을 사전 식별해 **해당 컨트롤러를 A 의 명시적 편집 허용 대상으로 fence 예외 선언**하거나, 표시 AC 를 의도적 deferral 로 분리·문서화한다.
> 근거: W4 세션(20260629) — 배지 표시 AC(프로필·게임카드)가 WebMvcController/SearchController(W3-1/3-4) SSR 에 의존하나 scope-fence 로 차단 → 리뷰작성자 칩만 구현, 프로필·게임카드는 deferral 로 분리.
### 설계 트레이드오프 concern 은 1차 결론까지 못박기
설계가 "구현에서 A 또는 B 가 필요할 수 있다"식 트레이드오프를 concern 으로 열어두면, 1차 구현이 검증 안 된 분기를 고를 수 있다. design 산출 시 **트레이드오프를 1차 권장안 + 조건부 전환 기준까지 선결정**으로 좁혀 첫 구현이 정답 분기를 잡게 한다.
> 근거: W3-3 SSRF — 설계 concern 이 "IP핀닝 또는 connect후 peer검증" 을 열어둠 → 1차가 IP핀닝(Host override) 선택, HTTPS SNI 깨짐 + restricted-header 미검증. fix1 에서 option-b(hostname-connect)로 전환.
---
## 프로토콜 개선 권고 (외부 번들 — 미적용)
@ -198,3 +222,7 @@ design 파일 영향맵에서 신규 테스트 파일이 owner 칸에 `(검증)`
- **agent-team-protocol §6 게이트 분류**: "근거 문서 확인 없이 freeze 인접 = 고위험 추론 금지" 조항 추가 권고.
- **design-advisor 파일영향맵 표기**: 신규 `*Test.java` 의 owner 를 `(검증)` 용도 라벨과 분리해 `owner=implementation` 으로 명시 필요(단계 경계 오독 유발 — 구조적). 근거: W2 세션(20260624) W2-2 테스트 미작성 갭.
- **implementation-advisor 체크리스트**: 신규 컨트롤러가 호출하는 공유 헬퍼/심볼은 정의 존재를 grounding(`rg`)으로 선확인하는 조항 추가(test-compile 비신뢰). 근거: W2 세션(20260624) W2-1 `response()` 헬퍼 누락 컴파일 FAIL.
- **검증 레지스트리 "버그 범주→L 레벨" 표**: 보안핵심(SSRF·zip-slip·경로 boundary·sanitize) 행 추가 — "L1+L2 + adversarial 코드감사 병렬" 명시. 근거: W3-3 세션(20260629) SSRF vacuous 포착.
- **verification-advisor 체크리스트**: 보안 거부 단위테스트는 정상경로 성공(present) 테스트와 쌍으로 차별 입증(distinguishing assertion) 확인 조항. 근거: W3-3 vacuous PASS.
- **orchestrator scope-fence 조항(구조적)**: 단일기능 표시면이 선행기능 컨트롤러에 의존할 때 cross-feature display wiring fence 예외 선언 또는 deferral 분리. 근거: W4 배지 표시 AC 갭.
- **design-advisor 조항**: 트레이드오프 concern 은 1차 권장안 + 조건부 전환 기준까지 선결정으로 좁힘. 근거: W3-3 SSRF IP핀닝 오선택.

View File

@ -89,4 +89,5 @@ owner: art
## 구현 추적
- **W2 (W2-1~6) 구현 완료** — 2026-06-24, commit `ccf1e42`(W2-1)~`a74bf74`(W2-6), 6 feat. 최종 L1 190/190 GREEN(회귀 0) + L2 dev DB contract 전 PASS(격리 throwaway DB). 변경 이력: [changes/2026-06-24-w2-jam-platform.md](../changes/2026-06-24-w2-jam-platform.md). dev DB 마이그레이션·L3 스모크는 needs_user_verification.
- W3(잔여)·W4 는 설계만 산출(미구현).
- **W3(잔여)·W4 (W3-1·W3-4·W3-3·W3-5·W4) 구현 완료** — 2026-06-29(session 20260629-100849), commit `35f1dc3`(W3-1)~`305cc73`(W4), 5 feat. W3-2(댓글/리뷰)는 선행 완료. 최종 L1 347/347 GREEN(baseline 190→347, +157, 회귀 0) + L2 dev DB contract 전 PASS(격리 throwaway DB) + adversarial SSRF/zip-slip 감사(W3-3 vacuous SSRF 포착→option-b 전환). 변경 이력: [changes/2026-06-29-w3-w4-features.md](../changes/2026-06-29-w3-w4-features.md). 실 dev DB DDL 8종 일괄 적용·L3 스모크·W4 표시 deferral(게임카드 creator 배지)은 needs_user_verification.
- **풀설계 11기능 전부 구현 완료** (W2-1~6 + W3-1/3/4/5 + W4). W3-2 는 별도 선행. 잔여 미구현 설계 0.