Compare commits

..

11 Commits

Author SHA1 Message Date
이정수 fa9a888c5b docs(graph): src+docs scope → full 단일 scope 통합
src↔docs 교차 엣지 단절 문제로 분리 scope 폐기.
merge-graphs로 2038노드/4685엣지 full 그래프 생성.
규모 비대화 시 재검토 노트 work-session에 기록.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-30 10:32:51 +09:00
이정수 2c48184fa7 chore(atp): retrospective 섹션 + memory_candidates 기록
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:24:33 +09:00
이정수 7047e06791 docs(verification): 회고 교훈 2건 반영 (20260629 세션)
- 버그범주 표: HTTP 상태/예외 매핑 변경 → L1 + 런타임 스모크 의무
- 구조교훈: HTTP 상태변경 런타임 스모크(전역 핸들러 가림 주의) + multi-worker advisor 산출 git diff 교차검증

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:24:22 +09:00
이정수 f6617a951d chore(atp): report 종료섹션(검증/graph_refresh/user_signals)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:19:17 +09:00
이정수 b5b2e6d5d3 docs(graph): src scope 재생성 메타 갱신 (source_commit 31dc2ff)
abstracts(4클래스)·GameCatalog 노드 제거 + GameController 404/ResponseStatusException
엣지·ApiExceptionControllerAdvice 핸들러 반영(1840노드/4403엣지/79커뮤니티).
docs scope 는 코드구조 무관·우선순위 낮음으로 차기 일괄 재생성 표기.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:19:17 +09:00
이정수 31dc2ff964 chore(atp): work-session 20260629-175705 기록(B2·B4·FE 3트랙)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:42 +09:00
이정수 aae7629562 docs: B2·B4·FE 반영 + OWASP/multipart 가이드·dev prefetch 노트
- security-remediation-checklist §B2·§B4 완료조건 [x] 갱신
- 신규: owasp-dependency-check-guide, multipart-size-risk, changes/work-log 기록
- local-dev-setup: SNAPSHOT→GA 후 online plugin prefetch 함정 노트
- analysis: 제거된 dead code(abstracts/GameCatalog/header.jspf) stale 정정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:42 +09:00
이정수 4f4811eb73 feat(ui): 검색영역 접이식 정리 + 다크모드 입력 대비·터치타깃 개선
- index: 고도화검색을 <details> 접이식으로(prefill 시 open) → 검색 진입점 위계 정리
- 다크모드 입력 테두리 대비 강화(border rgba(255,255,255,0.26)): login·signup·index·game-detail
- 체크박스 터치타깃 1.15rem(login·signup), summary 44px

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:42 +09:00
이정수 e51ab0136c chore(deps): Spring Boot 3.5.16 고정 + 세션쿠키·로그 하드닝 + OWASP DC
- spring-boot 3.5.14-SNAPSHOT → 3.5.16(GA), spring-snapshots repo/pluginRepo 제거
- OWASP dependency-check-maven 12.2.2 plugin(executions 없음=빌드 phase 비bind)
- 세션쿠키: base http-only=true·same-site=lax / application-live.properties secure=true
- MyBatis 로그 TRACE→WARN(base), application-dev.properties 만 TRACE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:42 +09:00
이정수 d143a5cedd fix(game): 없는 게임 ID 404 전환 + 프로토타입 dead code 제거
- abstracts/(4파일)·GameCatalog·fragments/header.jspf 삭제(외부참조 0 확인)
- GameController.gameDetail(): GameCatalog fallback(redirect/정적뷰) 제거 →
  DB 미존재 게임 ID = HTTP 404(ResponseStatusException)
- ApiExceptionControllerAdvice: ResponseStatusException 핸들러 추가(404 status
  보존 — 기존 Exception 핸들러가 500으로 가리던 것 수정)
- 회귀 가드 gameDetailThrowsNotFoundWhenGameMissing 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:23 +09:00
이정수 5e8816ed4a fix(test): BibimbapApplicationTests에 GameLikesMapper @MockBean 보강
GameController 가 GameLikesMapper 를 주입받으나(B3 좋아요 영속화) 테스트
컨텍스트 @MockBean 목록에 누락돼 contextLoads 가 NoSuchBeanDefinitionException
으로 실패하던 사전결함 수정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
2026-06-29 19:10:23 +09:00
40 changed files with 1324 additions and 198 deletions

View File

@ -0,0 +1,40 @@
/atp:task B2(dead code 제거) · B4(의존성/세션/운영 하드닝) · FE(프론트엔드 디자인 검토·수정) 3트랙을 진행한다. B2와 B4는 독립 파일이므로 병렬 처리하고, FE는 frontend-design 스킬 + claude-in-chrome 으로 실 화면을 검토한 뒤 수정한다.
## 공통 컨텍스트
- bibimbap: Java 21 / Spring Boot / JSP / MyBatis WAR. 화면은 `/WEB-INF/views/*.jsp`.
- dev 앱이 docker compose 로 구동 중(localhost:8080, db 5433). 코드·JSP 변경 후에는 `docker compose up -d --build app` 재배포가 필요하다(가이드: docs/development/local-dev-setup.md).
- 기준 문서: `docs/security/security-remediation-checklist.md` (B2=§B2, B4=§B4), `docs/analysis/2026-06-16-project-analysis.md`.
- 프로파일: dev + live. `spring.profiles.active=@app.profile@` (Maven 필터). `src/main/resources/{dev,live}/`.
## 트랙 1 — B2 프로토타입 dead code 제거 (독립)
1. `rg "abstracts|GameCatalog|header.jspf"` 로 실제 참조를 재확인한다.
2. `src/main/java/com/pandoli365/bibimbap/abstracts/` 패키지를 삭제(컴파일 무파손 확인).
3. `src/main/webapp/WEB-INF/jsp/fragments/header.jspf` 삭제 — 모든 JSP include 가 `/WEB-INF/views/header.jsp` 인지 먼저 확인.
4. `GameCatalog``GameController` 의 GameCatalog fallback(라인 ~111,116) 제거.
5. **없는 게임 ID 접근 동작 = HTTP 404** (결정 확정). redirect 아님.
6. 제거된 프로토타입 흐름을 최신 구조로 문서 갱신.
- 검증: `mvn test` 회귀 0. 완료 조건은 체크리스트 §B2 "완료 조건" 충족.
## 트랙 2 — B4 의존성/세션/운영 하드닝 (독립)
1. **Spring Boot**: `pom.xml` `spring-boot.version` `3.5.14-SNAPSHOT` → 최신 stable 3.5.x release 로 고정(정확 버전은 research 로 확정 — Maven Central 기준 최신 패치).
2. **snapshot repo 제거**: `pom.xml` `<repositories>`/`<pluginRepositories>` 의 spring-snapshots(라인 ~211-229) 삭제. SNAPSHOT 제거 후 불필요.
3. **세션 쿠키 하드닝**:
- base `application.properties`: `server.servlet.session.cookie.http-only=true`, `server.servlet.session.cookie.same-site=lax`.
- live 전용(`application-live.properties` 신규): `server.servlet.session.cookie.secure=true`.
4. **MyBatis TRACE 로그 격하**: 현재 base `logging.level.org.apache.ibatis=TRACE` → base 는 `WARN`, dev 전용(`application-dev.properties`)에서만 TRACE 유지. 민감 파라미터 노출 점검.
5. **CVE 스캔 도구 = OWASP Dependency-Check** (결정 확정. 오프라인·외부서비스 무의존). maven plugin 추가 + 스캔 결과·예외 기준을 `docs/security/` 에 기록.
6. **multipart 1GB = 유지** (사용자 결정). 변경하지 않되 DoS·디스크 고갈 리스크를 `docs/security/` 에 명시.
- 검증: `mvn test` 통과 + snapshot repo 없이 빌드 재현 가능. 완료 조건은 체크리스트 §B4 "완료 조건" 충족(단 multipart 항목은 "유지+리스크 문서화"로 종결).
## 트랙 3 — 프론트엔드 디자인 검토·수정 (frontend-design 스킬 + 브라우저)
- frontend-design 스킬을 로드하고, claude-in-chrome 으로 구동 중인 localhost:8080 의 실 화면을 직접 시각 검토한다(코드만 읽지 말 것).
- 대상: 핵심 사용자 동선 우선 — index(메인 허브)·game-detail·login·signup·profile. 이후 확장 가능.
- 검토 축(전부): ① 디자인 일관성·완성도(타이포·간격·색·컴포넌트, theme-init.jsp 다크/라이트) ② 반응형·모바일(breakpoint·터치타깃·가로스크롤) ③ 접근성 a11y(대비비·ARIA·포커스링·키보드 동선) ④ 시각 리프레시(룩앤필 현대화). 다른 축이 보이면 확장.
- 산출: (1) 화면별 findings 보고(스크린샷 + 축별 문제·심각도) → 사용자 합의 → (2) JSP 수정 → (3) 브라우저 재확인(L3 스모크).
- 데스크탑/모바일 뷰포트 모두 캡처.
## 실행 순서 / 게이트
- 트랙1·2 병렬(독립 파일, 충돌 0) → 각자 `mvn test`/빌드 검증.
- 트랙3 은 frontend-design 스킬 로드 후 브라우저 검토부터. 수정 전 findings 합의 게이트 1회.
- 세션 종료 전: 코드 변경 트랙 전부 L1(`mvn test`) 통과, JSP 변경은 재배포 후 브라우저 스모크. graph-refresh-checker 호출. git 잔여 커밋(작업 브랜치 표준 승인, push 는 명시 요청 시만).
- 비파괴 원칙: 삭제 전 참조 재확인, 파일 이동은 mv. 파괴적 조작은 사용자 확인 게이트.

View File

@ -0,0 +1,24 @@
---
schema_version: 2
sid: 20260629-174807
started_at: 2026-06-29T17:48:07+09:00
user_request: |
B4(의존성/세션/운영 하드닝) 및 B2(프로토타입 dead code 제거) 작업을 독립적으로 진행.
추가로 프론트엔드 디자인을 검토 및 수정.
tracks:
- id: B2
name: 프로토타입 dead code 제거
independent: true
- id: B4
name: 의존성/세션/운영 하드닝
independent: true
- id: FE
name: 프론트엔드 디자인 검토 및 수정
scope: 미정 (수렴 필요)
invocations: []
decisions: []
open_items: []
---
# Advisor Invocation Decision Log
# 각 advisor 호출/스킵 판단 즉시 1줄 append

View File

@ -0,0 +1,33 @@
# FE 디자인 검토 findings (1라운드)
검토 환경: claude-in-chrome, localhost:8080 (docker dev), 데스크탑 1440 캔버스, 다크+라이트.
검토 화면: index / login / signup / game-detail (다크·라이트). profile = 로그인 필요(보류).
viewport 한계: resize_window 가 실제 viewport(innerWidth=2560 retina)에 미반영 → **모바일은 CSS @media 코드 점검으로 검증**. 전 핵심 화면 @media 보유(반응형 마크업 존재).
theme: theme-init.jsp + header.jsp 토글 핸들러 정상(localStorage `bibimbap-theme` 영속 + 페이지이동 유지 실측 확인).
## findings (축별 심각도)
### F1. index 검색영역 이중구조 — [완성도/정보구조 · 중]
상단 큰 "게임·제작자 검색" 바 + [검색], 바로 아래 "게임이름·소개 검색"/"제작자(개발자) 검색" 2칸 + 정렬 드롭다운 + [상세 검색]. 검색 진입점이 시각적으로 둘로 중복 → 어느 걸 써야 할지 혼란. 위계/그룹핑 정리 필요(상세검색을 접이식 or 명확한 1차/2차 구분).
근거: index.jsp 검색 폼 영역.
### F2. 다크모드 입력 필드 테두리 대비 부족 — [a11y/완성도 · 중]
login/signup 다크에서 input border 가 배경(거의 검정)과 동일 톤 → 필드 경계 식별 곤란. 라이트모드는 옅은 회색 테두리로 양호. 다크 전용 input border 색 상향 필요(WCAG 비텍스트 대비 3:1 목표).
근거: login.jsp / signup.jsp + 다크 토큰.
### F3. 와이드스크린 콘텐츠 정렬·여백 — [레이아웃 · 하~중]
콘텐츠 max-width 좁고 상단 정렬 → 1440px+ 에서 하단 여백 과다 + 카드 그리드 좌측 쏠림(카드 1개일 때 특히 휑함). 콘텐츠 적은 현 데이터 탓도 있음 → 우선순위 낮음. 그리드 중앙정렬/최소 채움 검토.
### F4. game-detail — [정보 · 양호]
제목+3분할 카드(번호/제작자/좋아요) · WebGL 플레이 · 제작자 한마디 · 리뷰(3.8★) 구성 완성도 높음. 큰 이슈 없음. (모바일 3분할 카드 wrap 거동만 코드 확인)
### F5. 모바일 반응형 — [반응형 · 확인필요]
자동 viewport 제어 불가로 시각 미확인. 수정 단계에서 index/header/game-detail/login/profile 의 @media breakpoint·터치타깃(체크박스 작음 F2 연계)·가로스크롤 코드 점검으로 검증.
## 수정 우선순위 제안
P1: F2(다크 input 대비) — a11y, 저위험 토큰 수정.
P2: F1(검색영역 위계) — 정보구조, JSP 구조 손봄(중위험).
P3: F5(모바일 점검) — 코드 검증 후 필요 시 수정.
P4: F3(와이드 여백) — 데이터 적은 현 상태 영향 작음, 후순위.
전반: 현 디자인 완성도 양호(다크 주황 액센트·카드 UI 일관). 과한 비주얼 리프레시보다 **a11y·정보구조 정리** ROI 우선.

View File

@ -0,0 +1,42 @@
---
phase: documentation
agent: documentation-advisor
agent_version: 1
generated_at: 2026-06-29T18:30:00+09:00
concerns:
- "ADR 검토: D-B2-404(없는 게임 ID=HTTP 404)·D-B4-cve(OWASP 비bind)·D-B4-cfgloc(profile properties 신규)는 되돌리기 어려운 결정 성격이 있으나, API 동작 정의·플러그인 설정 수준으로 아키텍처 원칙보다는 구현 결정에 가깝다고 판단. security-remediation-checklist + changes 문서로 충분히 기록. ADR 불발행."
- "design-advisor가 이번 세션 architecture/ 갱신 수행하지 않음 — 구조 변화(abstracts 패키지 제거)가 아키텍처 문서화가 필요한 수준이나 기존 architecture/ 문서가 없어(index에 카테고리만 존재) 신규 작성 대신 analysis 문서 stale 갱신으로 처리."
concerns_checked: true
---
# 문서화 보고
## 작성/수정된 문서
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|---|---|---|---|---|
| docs/security/security-remediation-checklist.md | security | 수정 (B2·B4 체크박스 완료·구현결과·완료조건 갱신) | security/index.md | changes/2026-06-29-b2-b4-fe-hardening.md |
| docs/security/owasp-dependency-check-guide.md | security | 신규 | security/index.md | security-remediation-checklist.md, multipart-size-risk.md |
| docs/security/multipart-size-risk.md | security | 신규 | security/index.md | security-remediation-checklist.md, owasp-dependency-check-guide.md, maintenance/post-deploy-verification-checklist.md |
| docs/security/index.md | security | 수정 (신규 2문서 링크 추가) | — | — |
| docs/changes/2026-06-29-b2-b4-fe-hardening.md | changes | 신규 | changes/index.md | security/security-remediation-checklist.md, security/owasp-dependency-check-guide.md, security/multipart-size-risk.md, development/local-dev-setup.md |
| docs/changes/index.md | changes | 수정 (신규 문서 링크 추가) | — | — |
| docs/work-log/2026-06-29-b2-b4-fe-session.md | work-log | 신규 | work-log/index.md | changes/2026-06-29-b2-b4-fe-hardening.md, security/ 3문서 |
| docs/work-log/index.md | work-log | 수정 (신규 문서 링크 추가) | — | — |
| docs/analysis/2026-06-16-project-analysis.md | analysis | 수정 (Top#4·5·6 stale → "제거됨(2026-06-29)" 갱신, 패키지표 2항목) | — | changes/2026-06-29-b2-b4-fe-hardening.md |
| docs/development/local-dev-setup.md | development | 수정 (offline prefetch 함정 노트 추가) | — | — |
## 의사결정 기록 위치
- **D-B2-404** (없는 게임 ID = HTTP 404): `docs/security/security-remediation-checklist.md §B2 구현 결과`, `docs/changes/2026-06-29-b2-b4-fe-hardening.md §B2`
- **D-B4-cve** (OWASP Dependency-Check 오프라인 비bind): `docs/security/owasp-dependency-check-guide.md`, `docs/security/security-remediation-checklist.md §B4`
- **D-B4-multipart** (1GB 유지, 리스크 문서화): `docs/security/multipart-size-risk.md`
- **D-B4-cfgloc** (application-{profile}.properties 신규): `docs/security/security-remediation-checklist.md §B4 구현결과`, `docs/changes/2026-06-29-b2-b4-fe-hardening.md §B4`
- **D-test-mockfix** (GameLikesMapper @MockBean): `docs/changes/2026-06-29-b2-b4-fe-hardening.md §B2`
- **FE 결정** (details 접이식·다크모드 대비·터치타깃): `docs/changes/2026-06-29-b2-b4-fe-hardening.md §FE`
## 추후 문서화가 필요한 항목
- OWASP Dependency-Check 첫 실행 결과(스캔 보고서) 및 CVE 예외 결정이 나오면 security/ 에 결과 문서 추가 권고.
- multipart 리버스프록시 상한 설정이 완료되면 maintenance/ 또는 architecture/ 에 프록시 설정 기록 권고.
- application-live.properties의 cookie.secure=true가 실환경에 배포·검증되면 security-remediation-checklist §B4 완료조건에 런타임 확인 기록 권고.

View File

@ -0,0 +1,51 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-29T09:30:00Z
concerns: []
concerns_checked: true
workers_spawned: 3
planned_workers: 3
actual_workers: 3
---
# 구현 보고 — B2 GameController 404 전환 + B4 의존성/설정 하드닝
## 변경 목록
| 파일 | worker | 결과 요약 |
|---|---|---|
| src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java | code-writer (w-001) | GameCatalog import + fallback 블록 제거, game==null → `throw new ResponseStatusException(HttpStatus.NOT_FOUND)` |
| pom.xml | code-writer (w-002) | spring-boot.version 3.5.14-SNAPSHOT→3.5.16, spring-snapshots repo/pluginRepo 블록 전체 삭제, OWASP dependency-check-maven 12.2.2 plugin 추가(executions 없음) |
| src/main/resources/application.properties | code-writer (w-003) | ibatis 로그 TRACE→WARN, 세션 쿠키 http-only/same-site=lax 2줄 추가 |
| src/main/resources/application-dev.properties | code-writer (w-003) | 신규 생성 — dev 한정 ibatis TRACE |
| src/main/resources/application-live.properties | code-writer (w-003) | 신규 생성 — live 한정 쿠키 secure=true |
## 핵심 diff 요점
- **B2**: gameDetail() 의 game==null 경로 22줄(GameCatalog 기반 fallback 정적 뷰) → 1줄 404 throw. import `com.pandoli365.bibimbap.game.GameCatalog` 제거, import `org.springframework.web.server.ResponseStatusException` 추가. HttpStatus 는 기존 라인15에 이미 존재(중복 추가 안 함).
- **B2 advisor 추가 수정**: fallback 제거로 `List.of()` 사용처가 사라져 `import java.util.List` 가 unused 가 됨 → advisor 가 직접 제거(아래 "advisor 직접 편집" 참조).
- **B4 pom**: SNAPSHOT 제거로 spring 스냅샷 저장소 불필요 → repositories/pluginRepositories 래퍼 포함 전체 삭제. OWASP plugin 은 phase 비bind(수동 `mvn dependency-check:check` 전용, 평소 빌드 비영향).
- **B4 properties**: base 는 보수적(WARN + 쿠키 공통 하드닝), profile 별 오버라이드로 분리(dev=SQL TRACE, live=쿠키 Secure). Spring Boot 표준 application-{profile}.properties 자동로드 사용 — 기존 spring.config.import 기반 {profile}/db.properties 와 공존, db.properties 는 미변경.
## advisor 직접 편집 (worker 범위 외 후속)
- GameController.java 28라인 `import java.util.List;` 제거.
- 사유: w-001 가 설계대로 fallback 블록을 제거하자 `List.of()` 호출이 사라져 `List` 가 unused import 가 됨. 1라인 단일 편집이라 worker 재spawn 없이 advisor 가 직접 처리. (설계 변경 아님 — 설계가 의도한 제거의 직접 귀결)
## Bash 단계 (advisor 직접, 검증 아님 — 정적 grep 확인만)
- `rg -c GameCatalog GameController.java` → 0건 (참조 완전 제거 확인)
- `rg -c "3.5.14-SNAPSHOT|spring-snapshots" pom.xml` → 0건
- `ls application-{dev,live}.properties` → 2개 신규 파일 존재 확인
- 각 import 심볼 사용 count 점검 → unused import 0 (List 제거 후 LinkedHashMap/Locale/Map/ResponseEntity/ResponseStatusException/HttpStatus 모두 사용처 있음)
## 설계와의 차이
없음. planned_workers(3) == actual_workers(3). 모든 트랙이 설계대로 구현됨. unused import 제거는 설계가 의도한 dead code 삭제의 직접 귀결이며 설계 변경에 해당하지 않음.
## Verification 을 위한 힌트
- **B2 acceptance**: DB 미존재 game ID 로 `GET /game/{id}` 요청 시 HTTP 404 반환(기존엔 redirect:/ 또는 정적 fallback 뷰). DB 존재 시 정상 game-detail 뷰는 불변.
- **B2 영향 테스트**: GameController gameDetail 관련 테스트(존재 시) — game==null 케이스 기대값이 404 로 바뀜.
- **B4 pom**: `mvn -o ...` 또는 일반 빌드 시 spring-boot 3.5.16 의존성 해소 가능 여부. OWASP plugin 은 빌드 phase 비bind 이므로 평소 빌드 비영향.
- **B4 properties**: dev profile 활성 시 ibatis TRACE 로그, live profile 활성 시 쿠키 secure 적용 여부.
- acceptance criteria 전체는 design.md 의 검증 포인트 참조.
## 잔여 우려 (concerns)
- 없음. (GameCatalog.java 파일 자체 삭제는 본 advisor 범위 밖 — orchestrator 가 GameController 참조 0 확인 후 별도 수행. 본 작업으로 참조는 0건 끊김.)

View File

@ -0,0 +1,17 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-29T18:30:00+09:00
session: fe-design-fixes
---
# 파일 소유권 맵 (FE 디자인 수정 세션)
| 파일 | 담당 worker | worker id | 변경 유형 | 의존 |
|---|---|---|---|---|
| src/main/webapp/WEB-INF/views/login.jsp | code-writer | w-001 | modify | - |
| src/main/webapp/WEB-INF/views/signup.jsp | code-writer | w-002 | modify | - |
| src/main/webapp/WEB-INF/views/index.jsp | code-writer | w-003 | modify | - |
| src/main/webapp/WEB-INF/views/profile.jsp | code-writer | w-004 | modify | - |
| src/main/webapp/WEB-INF/views/game-detail.jsp | code-writer | w-005 | modify | - |

View File

@ -0,0 +1,78 @@
---
phase: verification
agent: verification-advisor
agent_version: 1
generated_at: 2026-06-29T09:33:00Z
concerns:
- "L2 skipped: dev DB contract harness 미구축 (레지스트리 open item). L2 미실행은 본 트랙(dead code 제거 + pom 하드닝) 판정에 영향 없음."
concerns_checked: true
---
# 검증 결과
## Acceptance Criteria (입력 받은 그대로 인용)
- **AC-1 (B2 컴파일 무파손)**: BUILD 컴파일 단계 통과. abstracts/GameCatalog/header.jspf 제거 + GameController 404 전환(ResponseStatusException) 후 컴파일 에러 0. unused import 등 컴파일 경고로 인한 실패 0.
- **AC-2 (회귀 0)**: 기존 테스트 전부 통과 — `Tests run: N, Failures: 0, Errors: 0` (skip 은 허용). 실패/에러 테스트 있으면 테스트명+사유 보고.
- **AC-3 (B4 snapshot repo 없이 빌드 재현)**: pom 에서 spring-snapshots repository/pluginRepository 삭제 + spring-boot 3.5.16 GA 임에도 의존성 resolve 성공 + BUILD SUCCESS. SNAPSHOT 잔존 resolve 시도 0.
## 실행된 전략
| id | cmd | exit | severity | 결과 |
|---|---|---|---|---|
| verify-all (L1) | `docker compose exec -T app mvn -B test` | 1 | blocker | FAIL |
분해 결과:
| 단계 | 결과 |
|---|---|
| L1 typecheck (compile) | pass — 컴파일 에러 0, BUILD 단계 통과 |
| L1 unit+regression | FAIL — Tests run: 360, Failures: 0, Errors: 1, Skipped: 0 |
| L2 contract | skipped: harness 미구축 |
| 로그 스캔 (SNAPSHOT resolve) | clean — spring-snapshot 도메인 Downloading 0건 |
## 실패 상세
### BibimbapApplicationTests.contextLoads (ERROR)
- **실패 지점**: `BibimbapApplicationTests.contextLoads`
- **루트 원인**: `NoSuchBeanDefinitionException``GameController` 생성자 파라미터 4번(0-indexed) 에 주입할 `GameLikesMapper` 빈이 없음.
```
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'gameController' ...
constructor parameter 4:
No qualifying bean of type 'com.pandoli365.bibimbap.mapper.GameLikesMapper' available
```
테스트 컨텍스트 설정:
```
spring.autoconfigure.exclude=
DataSourceAutoConfiguration,
MybatisAutoConfiguration
```
MyBatis AutoConfiguration이 exclude된 상태이므로 `GameLikesMapper` 빈이 등록되지 않아 `GameController` DI 실패 → `ApplicationContext` 로드 실패.
- **재현 명령**:
```
docker compose exec -T app mvn -B test -Dtest=BibimbapApplicationTests
```
- **진단**: B4/B2 구현 중 `GameController``GameLikesMapper` 의존성이 **신규 추가**됐으나, `BibimbapApplicationTests``@MockBean` 목록에 `GameLikesMapper`가 추가되지 않았다. 레지스트리 §"신규 컨트롤러/매퍼 의존 변경 시 full test 의무" 패턴과 정확히 일치하는 회귀.
## 종합 판정
```
overall: fail
rollback_signal: none
```
`rollback_signal: none` 근거: 파괴적 조작(스키마 변경·마이그레이션) 없음. 단순 테스트 설정 누락(`@MockBean` 미추가)으로 인한 컨텍스트 로드 실패. 구현 파일 복원이 아닌 테스트 파일 수정으로 해소 가능.
## Acceptance 매칭
| criterion | 매칭 전략 | 판정 |
|---|---|---|
| AC-1 (B2 컴파일 무파손) | L1 compile 단계 | **PASS** — 컴파일 에러 0. BUILD 단계 통과 확인. |
| AC-2 (회귀 0) | L1 unit+regression | **FAIL**`BibimbapApplicationTests.contextLoads` ERROR 1건. `GameLikesMapper` @MockBean 미등록. |
| AC-3 (B4 SNAPSHOT repo 제거) | SNAPSHOT resolve 스캔 | **PASS** — spring-snapshot 도메인 Downloading 0건. central(GA)에서만 resolve 성공. spring-boot 3.5.16 GA 의존성 다운로드 완료. |

View File

@ -0,0 +1,292 @@
---
schema_version: 2
sid: 20260629-175705
resumed_from: 20260629-174807
started_at: 2026-06-29T17:57:05+09:00
user_request: |
B2(dead code 제거) · B4(의존성/세션/운영 하드닝) · FE(프론트엔드 디자인 검토·수정) 3트랙.
B2·B4 독립 파일이라 병렬, FE 는 frontend-design 스킬 + claude-in-chrome 실화면 검토 후 수정.
확정 결정값: 없는 게임 ID = HTTP 404 / CVE = OWASP Dependency-Check(오프라인) / multipart 1GB 유지(리스크 문서화).
tracks:
- id: B2
name: 프로토타입 dead code 제거
independent: true
files: [abstracts/*, fragments/header.jspf, game/GameCatalog.java, controller/api/GameController.java]
- id: B4
name: 의존성/세션/운영 하드닝
independent: true
files: [pom.xml, application.properties, application-dev.properties, application-live.properties]
- id: FE
name: 프론트엔드 디자인 검토·수정
scope: index·game-detail·login·signup·profile (확장 가능)
invocations:
- agent: research-advisor
model: sonnet
result: 'B2 삭제 안전성(abstracts·header.jspf 참조0, GameCatalog=GameController 1곳) + B4 버전(SB 3.5.16, OWASP DC 12.2.2) 확정. B4 가정불일치(application-dev/live.properties 부재) 발견.'
- agent: implementation-advisor
model: sonnet
result: 'B2 GameController 404 전환(import/fallback 제거) + B4 pom(3.5.16·snapshot repo 제거·OWASP plugin 비bind)·application.properties(쿠키·로깅 WARN)·application-dev/live.properties 신규. 5파일.'
- agent: verification-advisor
model: sonnet
result: 'AC-1 컴파일 PASS / AC-3 snapshot없이빌드 PASS / AC-2 FAIL(BibimbapApplicationTests GameLikesMapper @MockBean 누락=직전 B3 사전결함). orchestrator 1줄 보강 후 재검 360 tests 0 fail PASS.'
- agent: implementation-advisor
model: sonnet
result: 'FE F2 다크 input 대비(login·signup·index·game-detail)·F1 index 검색 details 접이식·F5 터치타깃. profile=입력컨트롤 없음 해당없음. (advisor summary 불완전했으나 실변경 적용 확인)'
decisions:
- id: D-B2-404
text: 없는 게임 ID 접근 = HTTP 404 (redirect 아님). 사용자 확정. → ResponseStatusException(NOT_FOUND).
- id: D-B4-cve
text: CVE 스캔 = OWASP Dependency-Check maven plugin (오프라인). 사용자 확정. → executions 없이 비bind(수동 mvn dependency-check:check), autoUpdate=false.
- id: D-B4-multipart
text: multipart 1GB 유지, DoS/디스크 리스크만 docs/security 명시. 사용자 확정.
- id: D-B4-cfgloc
text: 'B4 설정 위치 = Spring 표준 application-{profile}.properties 신규(base 공통+profile특화). db.properties 는 DB 전용 유지. 사용자 합의(가정불일치 plan-gate).'
- id: D-FE-scope
text: 'FE 범위 = 전체(P1~P4+모바일). 사용자 합의. F1 검색위계=details 접이식.'
- id: D-test-mockfix
text: 'BibimbapApplicationTests GameLikesMapper @MockBean 누락(직전 B3 사전결함) 1줄 보강 — L1 그린 종료조건.'
open_items:
- 'dev override(spring-boot:run -o offline) 함정: SNAPSHOT→GA 버전 변경 후 첫 기동 전 online prefetch 필요 → local-dev-setup.md 노트화 (documentation 단계).'
---
# Advisor Invocation Decision Log
# 각 advisor 호출/스킵 판단 즉시 1줄 append
- advisor: requirements-advisor
decision: skip
rationale: '사용자가 트랙별 요구·확정값(404/OWASP DC/multipart 유지)까지 명시 — 요구 명확, 오픈질문 0'
checked_at: 2026-06-29T17:57:05+09:00
- advisor: graphify-lookup-advisor
decision: skip
rationale: '트랙1·2는 코드 사실 재확인(rg/실파일) 이 정확 — graph 인덱스보다 research-advisor 직접 탐색 우선'
checked_at: 2026-06-29T17:57:05+09:00
- advisor: research-advisor
decision: call
rationale: 'B2 참조 재확인(삭제 안전성) + B4 Spring Boot 최신 stable 3.5.x 정확버전·OWASP DC plugin 설정 외부조사 필요'
checked_at: 2026-06-29T17:57:05+09:00
## verified_by_me
- **L1 (unit+regression)**: `mvn test`**361 tests, 0 Failures, 0 Errors, BUILD SUCCESS** (docker dev 컨테이너). 신규 회귀 가드 `GameLikeControllerTest.gameDetailThrowsNotFoundWhenGameMissing` 포함. 직전 B3 사전결함(BibimbapApplicationTests GameLikesMapper @MockBean 누락) 보강 포함.
- **B4 빌드 재현 (AC-3)**: spring-snapshots repo 제거 + spring-boot 3.5.16 GA — central GA만으로 resolve, snapshot Downloading 0건. BUILD SUCCESS.
- **런타임 스모크 (재배포 후)**:
- B2 404: `GET /game/99999` → HTTP 404 `{"status":404,"message":"not found"}`, `GET /game/3` → 200. (ApiExceptionControllerAdvice ResponseStatusException 핸들러로 status 보존)
- B4 쿠키: `Set-Cookie: JSESSIONID=...; Path=/; HttpOnly; SameSite=Lax` 확인.
- FE: index 검색 `<details>` 접이식 펼침/접힘 동작, 다크모드 input 테두리 대비(zoom 확인 — 명확), signup·game-detail 렌더 정상(JSP 무파손).
- 로그 스캔: clean (BUILD SUCCESS, FeedParserTest prolog Fatal Error 는 XXE 방어 정상 로그).
## needs_user_verification
- **모바일 실기기/뷰포트**: 브라우저 자동화 resize 가 실제 viewport(innerWidth 2560 고정)에 미반영 → 모바일 시각 미확인. CSS @media(640/760/480 등)·터치타깃(체크박스 1.15rem, summary 44px) 코드 반영됨. 실기기/devtools 디바이스모드 1회 확인 권장.
- **live profile 쿠키 secure**: `application-live.properties` `cookie.secure=true` 는 HTTPS 운영 배포 시 Set-Cookie Secure 확인(dev=http 환경 미검증).
- **OWASP DC 첫 스캔**: `mvn dependency-check:check` 는 NVD 데이터 1회 온라인 다운로드(autoUpdate=false 라 사전 채운 dataDirectory 필요). 오프라인 운영 전 NVD 캐시 준비.
## graph_refresh
판정: **partial-stale** (graph-refresh-checker, src+docs).
- **src scope → 재생성 완료**: `/graphify src/` AST 재생성(1840노드/4403엣지/79커뮤니티, 이전 1857/4419/92). abstracts 4클래스 + GameCatalog 노드 제거, GameController 404/ResponseStatusException 엣지·ApiExceptionControllerAdvice 핸들러 반영. graph.json 내 abstracts/GameCatalog 잔존 0 확인. docs/graph/src 동기. index.md frontmatter(source_commit 31dc2ff)+src 행 갱신.
- **docs scope → 차기 이월(투명 기록)**: 코드구조 무관(DDL 무변경, 가이드 md semantic 증분), graph-refresh-checker "우선순위 낮음" 판정, 자기참조(docs/graph 2.5MB) 제외 처리 비용. index.md docs 행에 미반영 md 3건 명시 + 별도 `/graphify docs/` 권고.
## user_signals
positive:
- '계획 합의 게이트(AskUserQuestion 3축) 1회에 즉시 수락 — 옵션·Recommended 제시가 적중.'
negative: []
# 세션 중 부정 시그널(지적·반복요청·오류지적) 없음. 명시 피드백은 합의 답변 3건뿐.
## Summary
3트랙 완료. 8커밋(메인스트림 아님 feat/v2, push 미수행).
- **B2**: abstracts(4)·header.jspf·GameCatalog 삭제 + 없는 게임 ID HTTP 404(ResponseStatusException, advice 핸들러 status 보존). 회귀 가드 추가.
- **B4**: Spring Boot 3.5.16 + snapshot repo 제거 + OWASP DC 12.2.2(비bind) + 세션쿠키 하드닝(HttpOnly/SameSite=Lax, live secure) + ibatis 로그 WARN(dev TRACE) + multipart 1GB 유지(리스크 문서화).
- **FE**: index 검색 `<details>` 접이식 위계 정리 + 다크 input 대비 강화 + 체크박스 터치타깃(4화면).
검증: mvn test 361 PASS, 런타임 404/쿠키/FE 브라우저 스모크 확인, snapshot 없이 빌드 재현. graph src 재생성.
부수: 직전 B3 사전결함(BibimbapApplicationTests mock 누락) 보강. dev override offline prefetch 함정 문서화.
ended_at: 2026-06-29T19:16:00+09:00
## Retrospective
```yaml
Retrospective:
signals:
positive:
- quote_or_paraphrase: "계획 합의 게이트(AskUserQuestion 3축) 1회에 즉시 수락"
about: >
orchestrator 가 B4 설정 위치 불일치(application-dev/live.properties 부재)를
plan-gate 에서 옵션+Recommended 형식으로 제시했고, 사용자가 재질의 없이
1라운드에 D-B4-cfgloc 확정. 옵션 정렬 + Recommended 명시 패턴이 유효함.
negative:
- quote_or_paraphrase: "(B3 직전 세션) GameLikesMapper @MockBean 누락 — 이번 verification 에서 표면화"
about: >
B3 에서 GameController 에 GameLikesMapper 의존 추가 후 BibimbapApplicationTests
@MockBean 미갱신. verification-strategies 규약("신규 컨트롤러/매퍼 의존 변경 시
full test 의무")이 B3 implementation 단계에서 지켜지지 않아, 다음 세션(B2/B4)
검증 단계에서야 발견됨.
structural: true
- quote_or_paraphrase: "404 전역핸들러 가림 — mvn test 통과 후 런타임 스모크에서 발견"
about: >
ResponseStatusException(404) 이 기존 @ExceptionHandler(Exception.class) 에
HTTP 500 으로 먹혔으나 L1(단위/회귀)은 PASS. 런타임 브라우저 스모크에서야 발견.
"HTTP 상태코드 변경 AC 는 L1 만으로 검증 불충분" 이라는 갭.
structural: true
- quote_or_paraphrase: "advisor summary 불완전 종료 — FE implementation-advisor 가 'Worker C만 완료, A/B 대기'로 보고했으나 실제론 4파일 적용됨"
about: >
advisor 의 최종 summary 가 실제 적용 상태와 불일치. orchestrator 가 git diff 로
실변경을 직접 확인해 진행했으나, summary 신뢰성 문제는 구조적.
structural: true
- quote_or_paraphrase: "graphify 스킬이 graphify-out/ 에 출력 — src/ 소스트리 오염, 수동 삭제"
about: >
graphify 스킬의 출력 디렉토리가 graphify-out/ 고정인데, 프로젝트 관례는
docs/graph/<scope>/. 소스트리 루트에 graphify-out/ 이 생성돼 수동 삭제.
structural: false
what_went_well:
- "plan-gate 에서 B4 가정불일치(profile properties 부재)를 research 단계에서 선제 발견해 사용자 옵션+Recommended 로 제시 → 1라운드 결정 수렴."
- "OWASP Dependency-Check executions 없이 비bind 로 설계(평소 빌드 비영향) + autoUpdate=false 오프라인 친화 설정 — research-advisor 의 사실조사 품질이 설계 결정을 뒷받침."
- "B2 dead code 삭제 안전성: abstracts 패키지 외부 참조 0, header.jspf include 0 를 rg 전수 + 상속 키워드 보강으로 더블체크. 삭제 후 컴파일 무파손."
- "graph src 재생성을 세션 내에서 완료(partial-stale 판정 → 재생성 → index.md 갱신), docs scope 이월은 근거 명시 후 투명 기록."
what_to_improve:
- >
[구조적] B3 에서 매퍼 의존 추가 후 @MockBean 미갱신: verification-strategies 규약이
이미 존재하나 이전 세션 implementation 단계에서 지켜지지 않았음. implementation-advisor
체크리스트에 "컨트롤러 매퍼 의존 변경 시 BibimbapApplicationTests @MockBean 목록 동기화
확인" 단계를 명시적 항목으로 포함 필요.
- >
[구조적] HTTP 상태코드 변경(404 등) AC 는 런타임 스모크 없이 L1 만으로 검증 불충분.
@ExceptionHandler 계층 우선순위는 단위테스트로는 탐지 안 됨. 검증 레지스트리에
"HTTP status 변경 변경 포함 트랙 → 런타임 스모크 의무" 행 추가 필요.
- >
[구조적] advisor 최종 summary 가 실제 파일 적용 상태를 정확히 반영하지 못할 수 있음.
orchestrator 는 implementation-advisor 완료 보고를 git diff/실파일 확인으로 교차검증하는
습관이 필요(특히 multi-worker 세션).
- >
[단발] graphify 스킬 출력 경로(graphify-out/)와 프로젝트 관례(docs/graph/<scope>/)
불일치. 스킬 호출 전 출력 경로를 프로젝트 관례로 사전 지정하거나, 호출 후 이동하는
절차가 필요. src/ 소스트리 오염 방지.
memory_candidates:
- name: http-status-change-requires-runtime-smoke
type: feedback
description: >
HTTP 상태코드 변경(4xx/5xx 전환) 트랙은 L1 단위테스트만으로 검증 불충분
@ExceptionHandler 계층 우선순위는 런타임에서만 드러남.
body_draft: |
## Why
ResponseStatusException(404) 이 기존 @ExceptionHandler(Exception.class) 에 HTTP 500
으로 가려진 사례(세션 20260629-175705 B2). L1(mvn test) 은 PASS 했으나 런타임
브라우저 스모크에서야 발견. @ExceptionHandler 계층 우선순위 + ResponseStatusException
전파 경로는 단위테스트 mock 환경에서 재현되지 않는다.
## How to apply
- HTTP 상태코드 변경이 포함된 트랙(4xx 신규 도입, 에러 핸들러 수정 등)은
검증 레지스트리 "버그 범주 → L 레벨" 표에서 "런타임 스모크 의무" 로 분류.
- 런타임 스모크: 해당 AC 엔드포인트에 curl/브라우저로 기대 상태코드 직접 확인.
- verification-advisor 는 이 분류 트랙에 대해 "L1 PASS = 검증 완료" 로 종료하지 않고
L3 스모크 항목을 needs_user_verification 에 포함.
rationale_for_saving: >
@ExceptionHandler 계층 가림은 단위테스트로 탐지 불가 — L1 GREEN 이 "404 동작함"을
보장하지 않는다는 비자명한 패턴. 재발 가능성 높음(향후 에러 핸들러 변경마다).
signal_source: negative
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
memory_optional: true
- name: plan-gate-option-recommended-pattern
type: feedback
description: >
가정불일치(현실-설계 gap)를 plan-gate 에서 옵션+Recommended 형식으로 제시하면
사용자가 1라운드에 수렴하는 패턴이 이번 세션에서 검증됨.
body_draft: |
## Why
B4 트랙에서 research-advisor 가 "application-dev/live.properties 파일 없음"
가정불일치를 선제 발견. orchestrator 가 plan-gate AskUserQuestion 을
"옵션A / 옵션B / Recommended: B" 형식으로 구조화 → 사용자가 3축 질문을 1회에 수락.
재질의 0, 트랙 지연 0.
## How to apply
- research 단계에서 가정불일치·ambiguity 를 발견하면 설계/구현 단계로 넘기지 말고
plan-gate(AskUserQuestion)에서 표면화.
- 질문 형식: 옵션 나열 + 각 옵션 1줄 설명 + Recommended 명시.
- 한 번에 묻기: 독립 결정 여러 개를 한 메시지로 묶어 라운드 수를 최소화.
rationale_for_saving: >
1라운드 수렴 패턴이 실증(재질의 0). 비자명한 선택(가정불일치를 plan-gate에서 처리)이
검증된 긍정 패턴으로 재현 가치 있음.
signal_source: positive
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
memory_optional: true
- name: implementation-advisor-summary-cross-check
type: feedback
description: >
multi-worker implementation 세션에서 advisor 최종 summary 가 실파일 적용 상태와
불일치할 수 있음 — orchestrator 는 git diff 로 교차검증 필요.
body_draft: |
## Why
FE implementation-advisor 가 "Worker C만 완료, A/B 대기" 로 불완전 종료 보고했으나
실제론 4파일이 적용돼 있었음(세션 20260629-175705). summary 의존 시 롤백 오판
또는 불필요한 재작업 위험.
## How to apply
- implementation 완료 보고를 받은 직후 orchestrator 는 `git diff --name-only HEAD~1`
또는 `git status` 로 실변경 파일 목록을 교차 확인한다.
- summary 와 실파일 불일치 시 advisor 에 보정 요청(재summary)하거나
orchestrator 가 직접 diff 기반으로 진행.
- multi-worker(workers_spawned >= 2) 세션에서 특히 의무화.
rationale_for_saving: >
advisor summary 신뢰성 갭은 구조적이며 재발 가능. 단발 실수가 아니라
multi-worker 세션 구조에서 발생 가능한 패턴.
signal_source: negative
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
memory_optional: true
- name: graphify-output-path-project-convention
type: project
description: >
graphify 스킬 출력 경로(graphify-out/) 와 프로젝트 관례(docs/graph/<scope>/) 불일치.
스킬 호출 시 출력 경로 사전 지정 또는 호출 후 이동 필요.
body_draft: |
## 사실
graphify 스킬은 출력을 graphify-out/ 고정 디렉토리에 생성한다.
bibimbap 프로젝트 관례는 docs/graph/<scope>/ (예: docs/graph/src/, docs/graph/docs/).
스킬 호출 위치에 따라 src/ 루트 아래 graphify-out/ 이 생성돼 소스트리 오염.
세션 20260629-175705 에서 src/graphify-out 수동 삭제.
## How to apply
- /graphify 호출 전: 스킬 문서(~/.claude/skills/graphify/SKILL.md) 에서
출력 경로 파라미터 확인 후 docs/graph/<scope>/ 로 지정.
- 또는 스킬 완료 후 즉시 mv graphify-out/ docs/graph/<scope>/.
- docs/graph/index.md frontmatter 에 source_commit 갱신.
rationale_for_saving: >
스킬-프로젝트 관례 불일치는 스킬 호출마다 재발하는 구조적 함정.
코드/git log 에서 유도 불가(관례 vs 스킬 기본 경로).
signal_source: negative
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/local-dev-setup.md
memory_optional: true
protocol_feedback:
- target: implementation-advisor (체크리스트)
description: >
[구조적] 컨트롤러 매퍼 의존 추가/변경 후 BibimbapApplicationTests @MockBean 동기화
확인을 implementation 단계 체크리스트 필수 항목으로 포함. 현재 verification-strategies
에 규약이 있으나 implementation 단계 체크리스트에는 없어 이전 세션에서 누락됨.
근거: B3 @MockBean 미갱신 → B2/B4 세션 verification FAIL.
- target: verification-strategies.md (버그 범주 → L 레벨 표)
description: >
[구조적] "HTTP 상태코드 변경(4xx 신규·에러 핸들러 수정)" 행 추가 — "L1 + 런타임 스모크
(curl/browser 기대 status 직접 확인)" 의무. @ExceptionHandler 계층 우선순위는
L1 단위테스트로 탐지 불가.
근거: ResponseStatusException(404) 이 @ExceptionHandler(Exception.class) 에 500 으로
가려진 사례(세션 20260629-175705).
- target: agent-team-protocol (orchestrator 체크리스트 또는 §implementation 완료 처리)
description: >
[구조적] multi-worker implementation 세션 완료 시 orchestrator 의
git diff 교차검증을 의무 단계로 명시. advisor summary 만 신뢰하면 실변경과 다를 수 있음.
"summary != diff 시 처리 경로(재summary 요청 또는 diff 기반 진행)" 명시.
근거: FE advisor summary 불완전 종료 사례(세션 20260629-175705).
- target: graphify skill (SKILL.md 또는 호출 가이드)
description: >
[단발 → 패턴] graphify-out/ 고정 출력 vs 프로젝트 docs/graph/<scope>/ 관례 불일치.
스킬에 출력 경로 파라미터 지원 추가 또는, 호출 가이드에 "프로젝트 관례 경로로
이동 후 index.md 갱신" 절차 명시 권고.
applied_changes: []
```

View File

@ -0,0 +1,166 @@
---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-29T09:04:35Z
concerns: []
source_confidence: high
workers_spawned: 2
---
# 조사 결과 — B2 dead code 삭제 안전성 + B4 의존성 업그레이드 사실조사
## 주제
bibimbap (Java21/SpringBoot/JSP/MyBatis WAR) 의 두 작업트랙:
- B2: dead code(abstracts 패키지, GameCatalog, header.jspf) 삭제 안전성
- B4: Spring Boot 최신 stable 버전 + OWASP Dependency-Check maven plugin(오프라인) 도입을 위한 사실조사
전부 read-only. 설계·구현·코드수정 없음.
---
## 트랙 B2 — dead code 삭제 안전성 (코드 사실 / 조사포인트 1)
### 포인트 1-A: abstracts 패키지
- 경로: `src/main/java/com/pandoli365/bibimbap/abstracts/`
- 요약: 4개 파일 존재 — `ErrorResult.java`, `Request.java`, `Result.java`, `Service.java`.
- `ErrorResult` extends `Result` (status만 받는 생성자)
- `Request` abstract, `IsReceivedAllField()` 기본 true 반환
- `Result` abstract, status/message 필드
- `Service<Req extends Request, Res extends Result>` 제네릭 abstract, `ChackService()`/`StartService()`
- 외부 참조 확인:
- `rg "bibimbap\.abstracts|abstracts\."` → 4개 파일 자기 자신의 package 선언(라인1)만 hit. 패키지 밖 import/참조 **0건**.
- **(advisor 보강)** `rg "extends (Service|Request|Result)|new ErrorResult|StartService|ChackService|IsReceivedAllField"` → hit는 전부 abstracts 패키지 **내부**의 정의/상호참조(Service.java가 ErrorResult/Request를 쓰고, ErrorResult가 Result를 extends)뿐. abstracts **외부**에서 `extends Service`/`extends Request`/`extends Result` 하는 구체 클래스 **0건**.
- 관련 파일:라인:
- `abstracts/ErrorResult.java:3``public class ErrorResult extends Result`
- `abstracts/Service.java:5``public abstract class Service<Req extends Request, Res extends Result>`
- `abstracts/Service.java:13,15``(Res) new ErrorResult(401)` (패키지 내부 사용)
- 신뢰도: 확인됨 (rg 전수 + 직접 코드읽기 + advisor 상속 키워드 보강검증)
### 포인트 1-B: GameCatalog.java 참조
- 경로: `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java`
- 요약: `rg "GameCatalog"` 결과 참조 파일 2개뿐 — GameCatalog.java 자신 + GameController.java. 다른 컨트롤러·서비스 참조 **0건**.
- 관련 파일:라인:
- `controller/api/GameController.java:5``import com.pandoli365.bibimbap.game.GameCatalog;`
- `controller/api/GameController.java:150,155,157-162` — gameDetail() fallback 블록에서 사용
- 신뢰도: 확인됨
### 포인트 1-C: header.jspf include 여부
- 경로: `src/main/webapp/WEB-INF/jsp/fragments/header.jspf`
- 요약: `rg "header\.jspf|fragments/header"`**0 hit**. `rg "include.*header"` → 0 hit (이 패턴으로는). header.jsp를 include하는 JSP는 총 **23개**이며 전부 `<jsp:include page="/WEB-INF/views/header.jsp"/>` 형태로 통일. `header.jspf`를 include하는 사례 **0건**.
- 관련 파일:라인: game-detail.jsp:1201, index.jsp:727, login.jsp:205 등 23개 전부 `/WEB-INF/views/header.jsp`
- 신뢰도: 확인됨
### 포인트 1-D: GameController.gameDetail() fallback 로직 (404 변경용 정확 라인)
- 경로: `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java`
- 요약: `gameDetail()` 메서드(137~173라인) 현재 흐름:
- **140~148**: `gamesMapper.getGame(id)` DB조회 → `game != null`이면 정상 뷰 반환
- **150**: `game == null` 진입. `if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE || !GameCatalog.isValidId((int) id))`
- **151**: 위 조건 true이면 `return "redirect:/";`
- **154~172**: `GameCatalog.isValidId()` true이면 `GameCatalog.toIndex(intId)`로 인덱스 구해 `NAMES/CREATORS/LIKE_COUNTS/CREATOR_NOTES/GIT_URLS` 배열값을 model 적재 후 `"game-detail"` 뷰 반환
- 현재 코드 사실: **DB에 없는 game ID에 대해 404를 반환하는 분기는 이 메서드에 존재하지 않는다.** GameCatalog.isValidId 통과 여부에 따라 (a) fallback 정적 뷰("game-detail") 또는 (b) 루트 redirect("redirect:/") 둘 중 하나로 귀결.
- "GameCatalog 제거 후 없는 게임 ID를 404로 바꾸려면 고쳐야 할 줄" (사실 식별, 설계 아님):
- 150~151의 GameCatalog.isValidId 분기 + 154~172의 GameCatalog 기반 fallback 블록 전체가 제거 대상. game==null인 경로(150 진입 이후 전부)를 404 응답으로 대체해야 함. 즉 라인 150~172가 변경 영역.
- 신뢰도: 확인됨
---
## 트랙 B4 — Spring Boot / OWASP DC 버전 + 설정 (조사포인트 2,3,4)
### 포인트 2: Spring Boot 3.5.x 최신 stable(GA) 버전
- 요약: 현재 pom의 `3.5.14-SNAPSHOT`은 미출시 스냅샷. 3.5.x 계열 최신 **GA = 3.5.16** (2026-06-25 출시).
- v3.5.14 GA 2026-04-23 / v3.5.15 GA 2026-06-10 / v3.5.16 GA 2026-06-25(최신)
- 3.5.x 위로 4.0.x/4.1.x 별개 계열 존재(이번 트랙 범위 밖)
- 출처:
- https://spring.io/blog/category/releases — "Spring Boot 3.5.16 available now" (2026-06-25)
- https://github.com/spring-projects/spring-boot/releases — v3.5.16/3.5.15/3.5.14 확인
- 신뢰도: 확인됨 (spring.io 1차 + GitHub releases)
### 포인트 3-A: OWASP dependency-check-maven 최신 버전
- 요약: **12.2.2** (최신 GA).
- 동명 disambiguation: 원 저장소 `jeremylong/DependencyCheck`는 2025-09-27 아카이브됨. 관리 이관처 `dependency-check/DependencyCheck`. **Maven groupId는 동일하게 `org.owasp` 유지** — central.sonatype.com에서 `org.owasp:dependency-check-maven:12.2.2` latest로 확인됨(다른 groupId 아님).
- 현재 pom.xml에 OWASP DC plugin 없음(검색 0건).
- 출처:
- https://central.sonatype.com/artifact/org.owasp/dependency-check-maven/versions — 12.2.2 latest
- https://github.com/dependency-check/DependencyCheck/releases — v12.2.2 GA
- https://github.com/jeremylong/DependencyCheck — 이관 안내(2025-09-27 아카이브)
- 신뢰도: 확인됨 (Maven Central 1차 + GitHub releases)
### 포인트 3-B: 오프라인/NVD 무의존 설정 방법
- 요약(공식 configuration 문서 발췌 기준 — 전수 옵션 목록 아님):
- `autoUpdate` (기본 true): **false** 설정 시 NVD 자동 업데이트 비활성화 — 오프라인 핵심
- `dataDirectory` (기본 `~/.m2/.../dependency-check-data/`): 사전 다운로드한 로컬 NVD 캐시 위치 지정
- `nvdApiKey` (선택): **없어도 동작 가능**. 단 키 없으면 API 요청 간격이 8000ms로 늘어남(온라인 업데이트 시에만 영향, autoUpdate=false면 무관)
- `nvdDatafeedUrl`: 내부 NVD 미러 서버 지정(에어갭 지원). 별도 인프라 필요 → 상세는 범위 밖
- `failBuildOnCVSS` (기본 11=사실상 비활성, 0-10 범위): 특정 CVSS 이상 시 빌드 실패
- `skipProvidedScope` (기본 false): true면 provided scope 의존성 분석 생략
- 명시적 `--offline` 플래그는 **없음**. 오프라인 패턴 = `autoUpdate=false` + 사전채워진 `dataDirectory`. 한번 온라인에서 데이터 받아두면 이후 오프라인 재사용 가능.
- pom plugin 설정 예시(오프라인 친화, advisor가 검토할 후보 스니펫):
```xml
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.2</version>
<configuration>
<autoUpdate>false</autoUpdate>
<dataDirectory>${user.home}/.m2/repository/org/owasp/dependency-check-data</dataDirectory>
<failBuildOnCVSS>7</failBuildOnCVSS>
<skipProvidedScope>true</skipProvidedScope>
</configuration>
</plugin>
```
- 출처: https://jeremylong.github.io/DependencyCheck/dependency-check-maven/configuration.html
- 신뢰도: 확인됨 (옵션 존재·역할). 단 `failBuildOnCVSS=7`/`skipProvidedScope=true`의 구체 값 선택은 advisor 설계 결정(조사 산출 아님). 옵션 목록은 "공식 문서 발췌"이지 "전수 열거"가 아님.
### 포인트 4: 현 설정파일 정확 라인 (코드 사실)
#### 4-A: pom.xml 구조 (`/Users/wemadeplay/workspace/stz/bibimbap/pom.xml`)
- `spring-boot.version` property: **라인 29**, 값 `3.5.14-SNAPSHOT`
- `<repositories>` 블록: **라인 210~219** — spring-snapshots 1개(`https://repo.spring.io/snapshot`, releases disabled)
- `<pluginRepositories>` 블록: **라인 220~229** — spring-snapshots 1개(동일 URL, releases disabled)
- `<build><pluginManagement><plugins>`: 라인 100~155 — 9개 플러그인(clean/compiler/deploy/failsafe/install/resources/surefire/war/spring-boot)
- `<build><plugins>` (실행): **라인 157~208** — spring-boot-maven-plugin(157~169) + maven-compiler-plugin(170~207) 2개만. **OWASP plugin 추가 위치 = 이 블록 내**.
- profiles 빌드: dev(256~266), live(293~302)에 각각 spring-boot-maven-plugin profile용 추가
- 신뢰도: 확인됨
#### 4-B: application.properties (`src/main/resources/application.properties`, 31라인)
- `spring.profiles.active`: **라인 1**`@app.profile@` (Maven 필터링 치환)
- 세션 쿠키 설정(`server.servlet.session.cookie.*`): **0건 (없음)**
- `logging.level.org.apache.ibatis`: **라인 30**`TRACE`
- multipart: **라인 15~17**`spring.servlet.multipart.max-file-size=1GB`, `max-request-size=1GB`, `server.tomcat.max-http-form-post-size=1GB`
- (참고) 라인 21에 `spring.config.import`로 profile별 db.properties 로드
- 신뢰도: 확인됨
#### 4-C: profile별 properties 파일 존재 여부
- `application-dev.properties` / `application-live.properties` 형식 파일: **존재하지 않음**
- 대신 디렉토리 기반:
- `dev/db.properties` (존재, 8라인) + `dev/db.properties.example`
- `live/db.properties.example`만 존재 — **`live/db.properties`는 없음**
- `dev/db.properties` 내용: postgresql driver, url(localhost:5433/bibimbap?currentSchema=dev), username=bibimbap, password=change_me_local_dev, app.upload.game-storage-path, app.webgl.asset-origin(빈값), app.webgl.frame-ancestors=self
- 사실: B4가 "application-dev.properties/application-live.properties 신규/수정 대상"으로 가정한다면, **현재 그런 파일은 없고** profile 설정은 `dev/`·`live/` 디렉토리 + `spring.config.import` 패턴으로 관리됨. → 가정과 현행 구조 불일치(아래 미해결 참조)
- 신뢰도: 확인됨
---
## 종합 판단
### 삭제 안전성 판정 (B2)
| 대상 | 외부 참조 | 삭제 안전성 |
|---|---|---|
| `abstracts/` 패키지 4파일 | 패키지 외부 0건 (상속 구체클래스 0, import 0) | **삭제 안전** (컴파일·런타임 파손 없음) |
| `GameCatalog.java` | GameController.gameDetail()만 참조 | **GameController 동시수정 필요** — fallback 로직(라인 150~172) 제거/404대체 후라야 안전 |
| `header.jspf` | include 0건 | **삭제 안전** (어떤 JSP도 참조 안 함, 전부 /WEB-INF/views/header.jsp 사용) |
### 확정 버전값 (B4)
| 항목 | 확정값 | 신뢰도 |
|---|---|---|
| Spring Boot 3.5.x 최신 GA | **3.5.16** | 확인됨 |
| OWASP dependency-check-maven 최신 GA | **12.2.2** (groupId `org.owasp` 유지) | 확인됨 |
전 항목 1차 출처(spring.io / Maven Central / 프로젝트 코드 직접읽기)로 확인됨 → `source_confidence: high`. 차단 출처 fallback 없음.
## 미해결
- **B4 가정 vs 현행 구조 불일치**: 작업트랙 B4 설명이 "application-dev.properties / application-live.properties 신규/수정 대상"이라 가정하나, 현 프로젝트는 그런 파일이 없고 `dev/db.properties`·`live/db.properties`(+ `spring.config.import`) 패턴을 쓴다. 설정 추가를 어느 파일에 넣을지는 design-advisor가 현행 패턴 기준으로 재결정해야 함. (조사로 사실 확인 완료, 결정은 다운스트림)
- `live/db.properties` 부재 — live profile 활성 시 어디서 DB 설정을 받는지(배포 시 주입 등)는 이번 조사 범위 밖.
- GameCatalog.java의 배열 실제 값(NAMES/CREATORS 등 fallback이 제공하던 게임 목록)은 미조회 — 404 전환 시 사라지는 정적 게임 목록의 내용·개수가 필요하면 별도 조사.
- `nvdDatafeedUrl` 내부 미러 상세 구성은 별도 인프라 필요로 범위 밖.

View File

@ -0,0 +1,83 @@
---
schema_version: "2.3"
sid: "20260630-103214"
started_at: "2026-06-30T10:32:14+0900"
ended_at: "2026-06-30T10:33:00+0900"
user_request: "graphify src/docs 분리 scope를 full 단일 scope로 통합. work-session에 규모 비대화 시 재검토 노트 기록."
---
# Work Session — 20260630-103214
## Summary
graphify `src`·`docs` 분리 scope를 `full` 단일 scope로 통합.
기존 분리 구조에서 src↔docs 교차 엣지 단절 문제를 확인하고, 현재 규모(2038노드)에서는 단일 그래프가 적합하다고 판단.
`graphify merge-graphs`로 두 기존 graph.json을 병합, `docs/graph/index.md` 메타 갱신.
## Decisions
- `src`(1840노드/4403엣지) + `docs`(198노드/282엣지) → `full`(2038노드/4685엣지) 병합
- 판단 근거: 이 프로젝트 규모에서 scope 분리는 재생성 비용 절약보다 cross-link 단절 손실이 더 크다
- 규모 비대화(수만 노드) 시: scope 재분리 또는 `merge-graphs` 파이프라인 유지하되 별도 scope 운용 재검토
## Invocations
[]
## Advisor Invocation Decision Log
- advisor: requirements-advisor
decision: skip
rationale: '요청 명확 — 통합 + 기록 2가지 액션만'
checked_at: "2026-06-30T10:32:14+0900"
- advisor: design-advisor
decision: skip
rationale: '마이크로 편집 수준 (index.md 1파일 + work-session 기록)'
checked_at: "2026-06-30T10:32:14+0900"
- advisor: implementation-advisor
decision: skip
rationale: 'orchestrator 직접 수행 허용 범위'
checked_at: "2026-06-30T10:32:14+0900"
- advisor: verification-advisor
decision: skip
rationale: '코드 변경 0줄 — docs/meta만 변경'
checked_at: "2026-06-30T10:32:14+0900"
- advisor: documentation-advisor
decision: skip
rationale: 'work-session 기록으로 대체'
checked_at: "2026-06-30T10:32:14+0900"
## verified_by_me
코드 변경 없음 — L1/L2 해당 없음.
## needs_user_verification
(없음)
## graph_refresh
`full` scope 신규 생성 완료 (merge-graphs: 2038노드, 4685엣지).
`src`·`docs` scope 디렉토리는 gitignored 본체이므로 삭제 여부는 사용자 판단.
## open_items
- 구 `docs/graph/src/`, `docs/graph/docs/` 로컬 디렉토리 정리 (gitignored, 선택)
- docs partial-stale 미반영분(owasp-dependency-check-guide·multipart-size-risk·changes/2026-06-29-b2-b4-fe-hardening) — 차기 `/graphify full` 재생성 시 반영됨
## user_signals
positive: []
negative: []
## Scale Note
현재 full scope: 2038노드/4685엣지 — 단일 그래프 감당 범위.
**규모 비대화 기준 (수만 노드 도달 시) 재검토 옵션**:
1. `src`·`docs` scope 재분리 + lookup advisor가 양쪽 병렬 쿼리
2. `merge-graphs` 파이프라인 유지 + staleness 독립 추적
3. 기능 모듈별 sub-scope (예: `src-auth`, `src-game`, `docs-schema`)

View File

@ -30,9 +30,9 @@ Java/Maven 웹앱(Spring Boot 3 MVC + MyBatis + PostgreSQL, JSP 뷰) 전면 read
| 1 | INFO(긍정) | **SQL 인젝션 표면 0** — 6개 매퍼 전 쿼리가 파라미터 바인딩 `#{}` 만 사용. `${}` 문자열 보간 단 1건도 없음(java/xml 전수 grep). 검색어도 `ILIKE CONCAT('%', #{query}, '%')` 로 안전. | mapper/*.java 전체 | | 1 | INFO(긍정) | **SQL 인젝션 표면 0** — 6개 매퍼 전 쿼리가 파라미터 바인딩 `#{}` 만 사용. `${}` 문자열 보간 단 1건도 없음(java/xml 전수 grep). 검색어도 `ILIKE CONCAT('%', #{query}, '%')` 로 안전. | mapper/*.java 전체 |
| 2 | INFO(긍정) | **비밀번호 해싱 견고** — PBKDF2WithHmacSHA256, 210,000 iterations, 16B salt, 256bit, `MessageDigest.isEqual`(상수시간 비교). 형식 `pbkdf2_sha256$iter$salt$hash`. | UserController.java:542-576 | | 2 | INFO(긍정) | **비밀번호 해싱 견고** — PBKDF2WithHmacSHA256, 210,000 iterations, 16B salt, 256bit, `MessageDigest.isEqual`(상수시간 비교). 형식 `pbkdf2_sha256$iter$salt$hash`. | UserController.java:542-576 |
| 3 | MED | **POST /login, /signup CSRF 미검증** — logout/profile/game/recruit 모든 mutation 은 `CsrfTokens.isValid()` 검사하나 login·signup 은 누락 → login-CSRF 가능. | UserController.java:118-160(login), 65-116(signup) | | 3 | MED | **POST /login, /signup CSRF 미검증** — logout/profile/game/recruit 모든 mutation 은 `CsrfTokens.isValid()` 검사하나 login·signup 은 누락 → login-CSRF 가능. | UserController.java:118-160(login), 65-116(signup) |
| 4 | MED(부채) | **abstracts 패키지 전체 dead code**`Service`/`Request`/`Result`/`ErrorResult` 어디서도 상속·호출 안 됨. `Service.ChackService` 는 세션키 `"id"`(line 12) 사용, 활성 코드는 `"userId"` 사용 → 재사용 시 인증 우회 함정. | abstracts/Service.java:12 | | 4 | MED(부채) | **abstracts 패키지 전체 dead code**`Service`/`Request`/`Result`/`ErrorResult` 어디서도 상속·호출 안 됨. `Service.ChackService` 는 세션키 `"id"`(line 12) 사용, 활성 코드는 `"userId"` 사용 → 재사용 시 인증 우회 함정. | abstracts/Service.java:12 | **제거됨(2026-06-29)** — abstracts/ 패키지 4파일 전체 삭제. [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md) |
| 5 | MED(부채) | **orphan 깨진 JSP fragment**`WEB-INF/jsp/fragments/header.jspf` 가 Spring Security 태그(`<sec:authorize>`, `${_csrf}`)와 JSTL 사용하나 (a)어디서도 include 안 됨 (b)Spring Security 의존성 자체가 pom 에 없음 (c)라우트(`/games/new`,`/register`)·브랜딩('비빔밥')이 활성앱(`/game/new`,`/signup`,'bibimbap')과 불일치. 이전 프로토타입 잔재. | jsp/fragments/header.jspf:1-25 | | 5 | MED(부채) | **orphan 깨진 JSP fragment**`WEB-INF/jsp/fragments/header.jspf` 가 Spring Security 태그(`<sec:authorize>`, `${_csrf}`)와 JSTL 사용하나 (a)어디서도 include 안 됨 (b)Spring Security 의존성 자체가 pom 에 없음 (c)라우트(`/games/new`,`/register`)·브랜딩('비빔밥')이 활성앱(`/game/new`,`/signup`,'bibimbap')과 불일치. 이전 프로토타입 잔재. | jsp/fragments/header.jspf:1-25 | **제거됨(2026-06-29)** — header.jspf 삭제. [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md) |
| 6 | MED(부채) | **GameCatalog 완전 stub** — 모든 배열이 `{}` 빈 배열, `COUNT=0`. `GameController.gameDetail` 의 폴백 분기(line 111-128)는 영구 도달 불가(`isValidId` 는 항상 false). 죽은 분기 + 죽은 클래스. | game/GameCatalog.java:8-22, controller/api/GameController.java:111-128 | | 6 | MED(부채) | **GameCatalog 완전 stub** — 모든 배열이 `{}` 빈 배열, `COUNT=0`. `GameController.gameDetail` 의 폴백 분기(line 111-128)는 영구 도달 불가(`isValidId` 는 항상 false). 죽은 분기 + 죽은 클래스. | game/GameCatalog.java:8-22, controller/api/GameController.java:111-128 | **제거됨(2026-06-29)** — GameCatalog.java 삭제·fallback 분기 제거. 없는 게임 ID = HTTP 404. [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md) |
| 7 | MED(부채) | **게임 댓글: DB 스키마·매퍼 존재하나 미연결**`game_comments` 테이블, `GameCommentsMapper`(get/add/update), `GameCommentData` 모두 존재하지만 **댓글 작성/조회 컨트롤러 엔드포인트 없음**. 실제 댓글은 JSP 내 localStorage 클라이언트 전용(서버 미저장). `softDeleteGameComments` 만 게임 삭제 시 호출됨. | mapper/GameCommentsMapper.java, views/game-detail.jsp:907-1000 | | 7 | MED(부채) | **게임 댓글: DB 스키마·매퍼 존재하나 미연결**`game_comments` 테이블, `GameCommentsMapper`(get/add/update), `GameCommentData` 모두 존재하지만 **댓글 작성/조회 컨트롤러 엔드포인트 없음**. 실제 댓글은 JSP 내 localStorage 클라이언트 전용(서버 미저장). `softDeleteGameComments` 만 게임 삭제 시 호출됨. | mapper/GameCommentsMapper.java, views/game-detail.jsp:907-1000 |
| 8 | LOW | **WebApplicationFirewall/인증필터 부재 — 세션 기반 임시방편 인증** — Spring Security 미사용. 각 컨트롤러가 수동으로 `session.getAttribute("userId")` 체크. 인가는 리소스 소유권 체크(`userId.equals(game.getUserId())`)로 일관 처리됨(양호)하나 중앙 집중 필터 없음 → 신규 엔드포인트에서 체크 누락 위험. | 전 컨트롤러 | | 8 | LOW | **WebApplicationFirewall/인증필터 부재 — 세션 기반 임시방편 인증** — Spring Security 미사용. 각 컨트롤러가 수동으로 `session.getAttribute("userId")` 체크. 인가는 리소스 소유권 체크(`userId.equals(game.getUserId())`)로 일관 처리됨(양호)하나 중앙 집중 필터 없음 → 신규 엔드포인트에서 체크 누락 위험. | 전 컨트롤러 |
| 9 | LOW | **생성 산출물 git 추적**`src/test/db/dev-to-live-update.sql`(테스트가 생성하는 dev→live 마이그레이션 진단)이 커밋됨. 실제 비밀값은 없고(`-- DATA DIFF ... password_hash` 주석뿐) 비밀 누출 아님. 단 생성물은 비추적이 적절. | src/test/db/dev-to-live-update.sql:54 | | 9 | LOW | **생성 산출물 git 추적**`src/test/db/dev-to-live-update.sql`(테스트가 생성하는 dev→live 마이그레이션 진단)이 커밋됨. 실제 비밀값은 없고(`-- DATA DIFF ... password_hash` 주석뿐) 비밀 누출 아님. 단 생성물은 비추적이 적절. | src/test/db/dev-to-live-update.sql:54 |
@ -47,12 +47,12 @@ Java/Maven 웹앱(Spring Boot 3 MVC + MyBatis + PostgreSQL, JSP 뷰) 전면 read
| 패키지 | 역할 | | 패키지 | 역할 |
|--------|------| |--------|------|
| (root) | `BibimbapApplication`(@SpringBootApplication main), `ServletInitializer`(WAR 배포용 SpringBootServletInitializer) | | (root) | `BibimbapApplication`(@SpringBootApplication main), `ServletInitializer`(WAR 배포용 SpringBootServletInitializer) |
| abstracts | **dead code** — 미사용 제네릭 Service/Request/Result/ErrorResult 베이스 (확인됨: 참조 0) | | abstracts | **dead code** — 미사용 제네릭 Service/Request/Result/ErrorResult 베이스 (확인됨: 참조 0) **패키지 전체 제거됨(2026-06-29)** |
| config | `UploadResourceConfig` — 업로드 경로의 `/profile/**` 정적 리소스 핸들러 등록 | | config | `UploadResourceConfig` — 업로드 경로의 `/profile/**` 정적 리소스 핸들러 등록 |
| controller | 페이지(뷰) 컨트롤러 — `WebMvcController`(공통 페이지+에러), `RecruitController`(모집 페이지+API 혼합) | | controller | 페이지(뷰) 컨트롤러 — `WebMvcController`(공통 페이지+에러), `RecruitController`(모집 페이지+API 혼합) |
| controller/api | API/mutation 컨트롤러 — Game CRUD, GameUpload(zip/썸네일/파일), GameAsset(WebGL 서빙), User(인증/프로필), 예외 핸들러 | | controller/api | API/mutation 컨트롤러 — Game CRUD, GameUpload(zip/썸네일/파일), GameAsset(WebGL 서빙), User(인증/프로필), 예외 핸들러 |
| data | 순수 POJO DTO 6종 (Lombok 미사용 — 수동 getter/setter) | | data | 순수 POJO DTO 6종 (Lombok 미사용 — 수동 getter/setter) |
| game | `GameCatalog`**stub(빈 배열)**, 레거시 정적 게임 카탈로그 폴백(현재 무력) | | game | `GameCatalog`**stub(빈 배열)**, 레거시 정적 게임 카탈로그 폴백(현재 무력) **삭제됨(2026-06-29). 없는 게임 ID = HTTP 404** |
| mapper | MyBatis `@Mapper` 인터페이스 6종 — 어노테이션 기반 SQL(XML 매퍼 0) | | mapper | MyBatis `@Mapper` 인터페이스 6종 — 어노테이션 기반 SQL(XML 매퍼 0) |
| security | `CsrfTokens` — 세션 기반 CSRF 토큰 발급/검증 유틸(static) | | security | `CsrfTokens` — 세션 기반 CSRF 토큰 발급/검증 유틸(static) |

View File

@ -0,0 +1,95 @@
# 2026-06-29 B2·B4·FE 하드닝
**세션**: sid 20260629-175705 (resumed from 20260629-174807)
**브랜치**: feat/v2
**최종 테스트**: `mvn test` 361 PASS
## 요약
3트랙 병렬 작업: B2 프로토타입 dead code 제거, B4 의존성/세션/운영 하드닝, FE 프론트엔드 디자인 개선.
---
## B2. 프로토타입 dead code 제거
### 삭제된 파일
| 파일 | 이유 |
| --- | --- |
| `src/main/java/com/pandoli365/bibimbap/abstracts/Service.java` | 참조 0, 세션키 `"id"` 불일치(활성코드는 `"userId"`) — 인증 우회 함정 |
| `src/main/java/com/pandoli365/bibimbap/abstracts/Request.java` | 참조 0, dead |
| `src/main/java/com/pandoli365/bibimbap/abstracts/Result.java` | 참조 0, dead |
| `src/main/java/com/pandoli365/bibimbap/abstracts/ErrorResult.java` | 참조 0, dead |
| `src/main/webapp/WEB-INF/jsp/fragments/header.jspf` | include 참조 0, Spring Security 의존성 없는 환경에서의 orphan |
| `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java` | 빈 배열 stub, 영구 도달불가 분기 |
### 동작 변경
- **없는 게임 ID 접근**: 기존 `GameCatalog` fallback(redirect:/ 또는 정적뷰) → **HTTP 404** (`ResponseStatusException(NOT_FOUND)`). 사용자 확정(D-B2-404).
- `ApiExceptionControllerAdvice``ResponseStatusException` 핸들러 추가 — 기존 `Exception` catch-all 이 404를 500으로 덮던 결함 수정.
### 테스트
- `GameLikeControllerTest.gameDetailThrowsNotFoundWhenGameMissing` 회귀 가드 추가.
- 런타임 스모크: `GET /game/99999` = 404, `GET /game/3` = 200.
---
## B4. 의존성/세션/운영 하드닝
### pom.xml
| 변경 | 이전 | 이후 |
| --- | --- | --- |
| Spring Boot 버전 | `3.5.14-SNAPSHOT` | `3.5.16` (GA) |
| spring-snapshots repository | 있음 | **제거** |
| spring-snapshots pluginRepository | 있음 | **제거** |
| OWASP Dependency-Check plugin | 없음 | `12.2.2` 추가(빌드 비bind, 수동 실행) |
### application.properties (공통 base)
| 변경 | 이전 | 이후 |
| --- | --- | --- |
| ibatis/mybatis 로그 | `TRACE` | `WARN` |
| 세션 쿠키 HttpOnly | 미설정 | `true` |
| 세션 쿠키 SameSite | 미설정 | `lax` |
### 신규 파일
- `src/main/resources/application-dev.properties` — ibatis `TRACE` 로그 복원(로컬 개발 전용).
- `src/main/resources/application-live.properties``server.servlet.session.cookie.secure=true` (운영 전용).
### 런타임 확인
`Set-Cookie: JSESSIONID=...; Path=/; HttpOnly; SameSite=Lax` 확인.
### 결정 기록
- D-B4-cfgloc: 설정 위치 = Spring 표준 `application-{profile}.properties`. 기존 `db.properties`(DB 전용) 공존.
- D-B4-multipart: 1GB 유지, 리스크 문서화 → [security/multipart-size-risk.md](../security/multipart-size-risk.md).
- D-B4-cve: OWASP Dependency-Check 오프라인 비bind → [security/owasp-dependency-check-guide.md](../security/owasp-dependency-check-guide.md).
---
## FE. 프론트엔드 디자인 개선
### 변경 내용
| 항목 | 변경 |
| --- | --- |
| F1 — index 검색 위계 | `.advanced-search``<details>` 접이식으로 래핑. prefill(쿼리 파라미터) 시 `open` 속성 자동 부여 |
| F2 — 다크모드 input 대비 | 입력 테두리 `border-color: rgba(255,255,255,0.26)` 강화. 적용 뷰: login·signup·index·game-detail |
| F5 — 터치타깃 | 체크박스 font-size `1.15rem`. 적용 뷰: login·signup |
### 확인
브라우저 스모크: 다크모드 토글 후 input 대비 개선, 검색 접이식 동작, 터치타깃 확대 확인.
---
## 참조
- 보안 체크리스트: [security/security-remediation-checklist.md](../security/security-remediation-checklist.md)
- OWASP DC 가이드: [security/owasp-dependency-check-guide.md](../security/owasp-dependency-check-guide.md)
- Multipart 리스크: [security/multipart-size-risk.md](../security/multipart-size-risk.md)
- Local dev 함정: [development/local-dev-setup.md](../development/local-dev-setup.md)

View File

@ -9,3 +9,4 @@
- [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) + 후속(20260629-142115) 배지 표시 배선 완료·보안 하드닝 b1/b2. 태그+검색·메인허브·포스팅보드·업로드보안·배지/평판 구현(5 커밋 35f1dc3~305cc73, 최종 L1 347/347 GREEN). 후속: 게임카드 creator 배지 칩·프로필 myBadges 배선(3d10449, L1 352/352) + SsrfSafeFetcher @PostConstruct DNS ttl=30(b1 best-effort) + 업로드 저장루트 static 밖 이전(b2, 9041bb7, L1 353/353). DB DDL 8종 사용자 직접 적용 완료. L3 스모크 및 b2 자산 수동 이전은 배포 후 과제 이월 — 상세: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md). - [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) + 후속(20260629-142115) 배지 표시 배선 완료·보안 하드닝 b1/b2. 태그+검색·메인허브·포스팅보드·업로드보안·배지/평판 구현(5 커밋 35f1dc3~305cc73, 최종 L1 347/347 GREEN). 후속: 게임카드 creator 배지 칩·프로필 myBadges 배선(3d10449, L1 352/352) + SsrfSafeFetcher @PostConstruct DNS ttl=30(b1 best-effort) + 업로드 저장루트 static 밖 이전(b2, 9041bb7, L1 353/353). DB DDL 8종 사용자 직접 적용 완료. L3 스모크 및 b2 자산 수동 이전은 배포 후 과제 이월 — 상세: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md).
- [2026-06-29-game-like-server-persistence.md](./2026-06-29-game-like-server-persistence.md) — 게임 "좋아요" 서버 영속화. 좋아요가 서버에 안 써지고 localStorage 로만 토글되던 버그 수정. 신규 `POST /game/{id}/like` 토글(@Transactional, CSRF→로그인→존재 게이트) + game_likes row/`games.like_count` 컬럼 단일 트랜잭션 ±1 동기 + 상세 JSP fetch 전환 + explore 무변경(컬럼 자동반영). L1 PASS(GameLikeControllerTest 7/7 + 회귀 219/219), L2 dev DB skip. UNIQUE 마이그레이션(20260629-game-likes-unique.sql) 운영 적용·실환경 스모크는 needs_user_verification — 절차: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md). 좋아요 항목으로 보안 B3 부분 충족. - [2026-06-29-game-like-server-persistence.md](./2026-06-29-game-like-server-persistence.md) — 게임 "좋아요" 서버 영속화. 좋아요가 서버에 안 써지고 localStorage 로만 토글되던 버그 수정. 신규 `POST /game/{id}/like` 토글(@Transactional, CSRF→로그인→존재 게이트) + game_likes row/`games.like_count` 컬럼 단일 트랜잭션 ±1 동기 + 상세 JSP fetch 전환 + explore 무변경(컬럼 자동반영). L1 PASS(GameLikeControllerTest 7/7 + 회귀 219/219), L2 dev DB skip. UNIQUE 마이그레이션(20260629-game-likes-unique.sql) 운영 적용·실환경 스모크는 needs_user_verification — 절차: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md). 좋아요 항목으로 보안 B3 부분 충족.
- [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. - [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.
- [2026-06-29-b2-b4-fe-hardening.md](./2026-06-29-b2-b4-fe-hardening.md) — B2 프로토타입 dead code 제거(abstracts 4파일·header.jspf·GameCatalog 삭제, 없는 게임 ID = HTTP 404) + B4 의존성/세션 하드닝(Spring Boot 3.5.16 GA·snapshot repo 제거·쿠키 HttpOnly/SameSite·로그 WARN·OWASP DC plugin 비bind·profile properties 신규) + FE 다크모드 대비·검색 접이식·터치타깃. mvn test 361 PASS.

View File

@ -35,6 +35,19 @@
- **Java 코드**: `docker compose restart app` (spring-boot:run 재기동 ~1~2s). 또는 `docker compose exec app mvn -o -P dev -DskipTests compile` 후 restart. - **Java 코드**: `docker compose restart app` (spring-boot:run 재기동 ~1~2s). 또는 `docker compose exec app mvn -o -P dev -DskipTests compile` 후 restart.
- 전제: 호스트 `~/.m2` 에 의존성 캐시 존재(offline `-o`). **새 의존성 추가 시**: override 의 `-o` 를 일시 제거해 1회 온라인 받거나, 호스트에서 `./mvnw -P dev dependency:go-offline` 후 다시 offline. - 전제: 호스트 `~/.m2` 에 의존성 캐시 존재(offline `-o`). **새 의존성 추가 시**: override 의 `-o` 를 일시 제거해 1회 온라인 받거나, 호스트에서 `./mvnw -P dev dependency:go-offline` 후 다시 offline.
> **의존성/플러그인 버전 변경 후 online prefetch 필요** (함정, 2026-06-29 확인):
> SNAPSHOT→GA 등 버전 변경 후 offline 모드(`-o`)로 첫 기동 시 `spring-boot-maven-plugin` 신 버전 resolution 실패 → 컨테이너 즉시 종료. 컨테이너가 죽으면 `exec` 진입 불가.
>
> **처치**: 버전 변경 후 첫 기동 전 아래 중 하나로 온라인 prefetch 1회 수행.
> ```bash
> # 방법 A — run --rm 으로 플러그인·의존성 prefetch (컨테이너 내부, 호스트 ~/.m2 마운트됨)
> docker compose run --rm app mvn -B dependency:resolve-plugins dependency:resolve -P dev
>
> # 방법 B — 호스트에서 mvnw 직접 실행 (호스트 JDK 21 필요)
> ./mvnw -B -P dev dependency:resolve-plugins dependency:resolve
> ```
> prefetch 성공 후 `docker compose up -d app` 으로 offline 재기동하면 정상 동작.
- **배포 이미지**: override 를 **제외**하고 base 만 명시한다(자동병합 회피). - **배포 이미지**: override 를 **제외**하고 base 만 명시한다(자동병합 회피).
```bash ```bash
docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기 docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기

View File

@ -24,6 +24,7 @@
| 순수 도메인 로직 (외부 의존 없음) | L1 | | 순수 도메인 로직 (외부 의존 없음) | L1 |
| MyBatis 매퍼 신규/SQL alias·집계 뷰 정의·변경 (DB-방언 계약) | L1 + L2 (dev DB contract) | | MyBatis 매퍼 신규/SQL alias·집계 뷰 정의·변경 (DB-방언 계약) | L1 + L2 (dev DB contract) |
| 인프라 설정 (container/env/compose) | L1 + 수동 스모크 | | 인프라 설정 (container/env/compose) | L1 + 수동 스모크 |
| HTTP 상태코드/예외 매핑 변경 (404/403 등, 전역 `@ExceptionHandler` 경유) | L1 + 런타임 스모크 |
**회귀 테스트 의무**: 버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다. revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다. **회귀 테스트 의무**: 버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다. revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다.
@ -238,3 +239,15 @@ SSRF·zip-slip·경로 boundary·sanitize 같은 보안핵심은 verification-ad
- **verification-advisor 체크리스트**: 보안 거부 단위테스트는 정상경로 성공(present) 테스트와 쌍으로 차별 입증(distinguishing assertion) 확인 조항. 근거: W3-3 vacuous PASS. - **verification-advisor 체크리스트**: 보안 거부 단위테스트는 정상경로 성공(present) 테스트와 쌍으로 차별 입증(distinguishing assertion) 확인 조항. 근거: W3-3 vacuous PASS.
- **orchestrator scope-fence 조항(구조적)**: 단일기능 표시면이 선행기능 컨트롤러에 의존할 때 cross-feature display wiring fence 예외 선언 또는 deferral 분리. 근거: W4 배지 표시 AC 갭. - **orchestrator scope-fence 조항(구조적)**: 단일기능 표시면이 선행기능 컨트롤러에 의존할 때 cross-feature display wiring fence 예외 선언 또는 deferral 분리. 근거: W4 배지 표시 AC 갭.
- **design-advisor 조항**: 트레이드오프 concern 은 1차 권장안 + 조건부 전환 기준까지 선결정으로 좁힘. 근거: W3-3 SSRF IP핀닝 오선택. - **design-advisor 조항**: 트레이드오프 concern 은 1차 권장안 + 조건부 전환 기준까지 선결정으로 좁힘. 근거: W3-3 SSRF IP핀닝 오선택.
### HTTP 상태/예외 매핑 변경은 런타임 스모크 의무 (L1 통과로 충분치 않음)
컨트롤러가 던지는 예외의 HTTP 상태(404/403 등)를 바꾸거나 신규 예외를 도입할 때, 전역 `@RestControllerAdvice`/`@ExceptionHandler(Exception.class)` 가 더 구체적 핸들러보다 먼저 잡아 의도한 상태를 **삼킬 수 있다**(예: `ResponseStatusException(404)` 이 광역 Exception 핸들러에서 500으로 변환). 컴파일·단위(L1)는 "예외를 던진다"까지만 검증하고 실제 HTTP 응답 코드는 미검증 → L1 GREEN 으로 끝내지 말고 **런타임 스모크(`curl -w '%{http_code}'`)** 로 실제 상태코드를 가드한다. 회귀 가드는 (a) 컨트롤러가 올바른 예외를 던지는 단위테스트 + (b) advice 핸들러가 상태를 보존하는 검증, 둘 다 둔다.
> 근거: B2·B4·FE 세션(20260629) — `gameDetail` 404 전환이 mvn test 361 PASS 후에도 런타임 500 반환(전역 Exception 핸들러가 ResponseStatusException 가림). 브라우저 스모크에서 발견 → advice 에 `@ExceptionHandler(ResponseStatusException)` 추가로 해소.
### multi-worker advisor 산출은 orchestrator git diff 교차검증
implementation-advisor 가 여러 code-writer worker 로 파일을 분산 수정한 뒤 반환하는 완료 summary 는 worker 상태 추적 혼선으로 **실제 변경과 불일치**할 수 있다("Worker C만 완료" 보고했으나 실제 4파일 적용 등). orchestrator 는 advisor summary 를 그대로 신뢰하지 말고 `git diff --stat` / 대상 파일 grep 으로 **실변경을 교차검증**한 뒤 다음 단계로 진행한다.
> 근거: B2·B4·FE 세션(20260629) — FE implementation-advisor 가 불완전 summary 반환, git diff 로 4파일 적용 확인 후 진행. profile 미변경은 "입력 컨트롤 없음=정상"으로 판별.

View File

@ -1,10 +1,9 @@
--- ---
kind: graphify-meta kind: graphify-meta
last_generated_at: 2026-06-29T16:12:00+0900 last_generated_at: 2026-06-30T00:00:00+0900
source_commit: fa6a301 source_commit: 2c48184
scopes: scopes:
- src - full
- docs
--- ---
# Graph — graphify 산출물 메타 # Graph — graphify 산출물 메타
@ -32,14 +31,15 @@ docs/graph/
└── audit.md └── audit.md
``` ```
scope 예시: `src`, `src-features`, `docs`, `full` 등. 한 번에 여러 scope 를 운용할 수 있다. scope 예시: `full`, `src`, `docs`, `src-features` 등.
> **scope 전략**: 이 프로젝트는 `full` 단일 scope를 기본으로 한다 (src 1840노드 + docs 198노드 = 2038노드 수준은 단일 그래프로 감당 가능). src↔docs 교차 엣지(코드↔설계문서 관계)가 살아있어야 작업 컨텍스트가 끊기지 않기 때문. 규모가 수만 노드 수준으로 비대해지면 scope 분리 재검토.
## Scopes ## Scopes
| scope | 마지막 생성 | 소스 커밋 | 대상 경로 | 요약 | | scope | 마지막 생성 | 소스 커밋 | 대상 경로 | 요약 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `src` | 2026-06-29 | `fa6a301` | `src/` (Java + AST) | **게임 좋아요 서버 영속화 반영**(9041bb7→fa6a301, AST 1857노드/4419엣지/92커뮤니티). 신규 라우트 `GameController.toggleLike` (`POST /game/{id}/like`) + 신규 주입 엣지 `GameController`→`GameLikesMapper`, mapper 메서드 5 (`GamesMapper.{incrementLikeCount,decrementLikeCount,getLikeCount}` + `GameLikesMapper.{findByGameAndUser,deleteByGameAndUser}`). 직전 상태: **배지 표시면 배선 + 보안 하드닝 반영**(305cc73→9041bb7, AST 1836노드/4330엣지/89커뮤니티). 신규 엣지: `UserBadgesQueryMapper`→{`WebMvcController`,`SearchController`} 주입(게임카드 creator 배지·프로필 myBadges 표시 경로 — GameReviewController authorBadges 정본 답습), `SsrfSafeFetcher.initDnsCachePolicy()` 메서드 노드(networkaddress.cache.ttl 하드닝). W3(잔여)+W4 워크스트림 군집 유지: 태그/검색·메인허브 keyset·게시판/SSRF/유니티피드·업로드보안(ZipSecurity)·배지/평판. 기존 W1 RBAC·W2 게임잼·리뷰·댓글 군집 유지. 정적 이미지/banned-words 제외(AST 기반). | | `full` | 2026-06-30 | `2c48184` | `src/` + `docs/` | **src+docs 통합** (merge-graphs, 2038노드/4685엣지). 기존 `src` scope(AST 1840노드/4403엣지, Java 클래스·메서드·주입엣지) + `docs` scope(semantic 198노드/282엣지, 설계문서·DDL·변경이력)를 단일 그래프로 통합. src↔docs 교차 탐색 가능. 직전 src 상태: B2 dead code 제거 + 없는 게임 404 반영 (31dc2ff). 직전 docs 상태: W3/W4 스키마·변경이력 반영 (305cc73). |
| `docs` | 2026-06-29 | `305cc73` | `docs/` (md + DDL) | **W3/W4 스키마·변경이력 반영**(semantic 198노드/282엣지/14커뮤니티). 신규 DDL 5: tag-ddl·games-hub-ddl·board-ddl·game-upload-ddl·badge-ddl(테이블/뷰 12 + 인덱스) — tags/game_tags/jam_tags/game_views·post_categories/posts/unity_feed_sources/unity_feed_items·game_upload_audit_log·badges/user_badges/reputation_events 스키마 군집. changes 2026-06-29 W3/W4 변경이력 + 구현추적 갱신. 기존 W1/W2 스키마·문서 카테고리·보안·검증·로드맵 군집 유지. graphify 본체(docs/graph/{src,docs}/) 자기참조 제외. (이번 세션 docs/maintenance 추가분은 src 동반 갱신 우선순위 판정으로 차기 일괄 재생성 대상 — graph-refresh-checker partial-stale 권고.) |
## 갱신 시 체크리스트 ## 갱신 시 체크리스트

View File

@ -4,4 +4,6 @@
## 목록 ## 목록
_(아직 문서 없음)_ - [security-remediation-checklist.md](./security-remediation-checklist.md) — 보안 개선 체크리스트 (B1~B4 항목별 근거·체크박스·완료 조건). 기준 분석: 2026-06-16 프로젝트 전면 분석.
- [owasp-dependency-check-guide.md](./owasp-dependency-check-guide.md) — OWASP Dependency-Check maven plugin 운영 가이드. 수동 실행·오프라인 모드·빌드 비bind 이유·예외 기준(CVSS 7+). (B4, 2026-06-29)
- [multipart-size-risk.md](./multipart-size-risk.md) — multipart 업로드 1GB 상한 유지 결정 및 DoS/디스크 고갈 리스크 문서화. 완화 권고(리버스프록시·모니터링) 포함. (B4, 2026-06-29)

View File

@ -0,0 +1,48 @@
# Multipart 업로드 1GB 상한 유지 — 리스크 문서화
**결정**: multipart 상한 1GB 유지 (사용자 확정, D-B4-multipart, sid 20260629-175705)
**관련 체크리스트**: [security-remediation-checklist.md §B4](./security-remediation-checklist.md#b4-의존성세션운영-하드닝)
## 현재 설정
`src/main/resources/application.properties`:
```properties
spring.servlet.multipart.max-file-size=1GB
spring.servlet.multipart.max-request-size=1GB
```
## 리스크
### DoS (Denial of Service)
- 악의적 클라이언트가 1GB 파일 업로드를 동시에 여러 건 요청하면 서버 처리 스레드·메모리·디스크 I/O를 소진할 수 있다.
- Spring MVC 기본 설정에서 multipart 파싱은 요청 완료 전까지 임시 파일(`java.io.tmpdir`)에 버퍼링된다.
- 임시 디스크 고갈 시 다른 기능(로그, 쓰기 연산)도 함께 영향받는다.
### 디스크 고갈
- 업로드 임시 경로(`/tmp` 또는 컨테이너 tmpfs)와 저장 경로(`~/.bibimbap/uploads`)가 모두 대용량 파일로 채워질 수 있다.
- 컨테이너 환경에서 tmpfs 상한이 낮으면 1GB 단일 파일에도 OOM 오류가 발생한다.
## 유지 결정 근거
- bibimbap 은 게임 WebGL 번들 업로드를 지원하며, 실제 게임 빌드 크기가 수백 MB에 이를 수 있다. 상한을 낮추면 합법적 업로드가 차단된다.
- 현재 운영 규모(소규모)에서 대규모 DoS 시도 가능성이 낮다고 사용자가 판단했다.
- 상한 축소·업로드 스트리밍 개선은 이후 운영 부하 확인 후 별도 검토.
## 완화 권고 (추후)
| 권고 항목 | 설명 |
| --- | --- |
| 리버스 프록시 상한 설정 | nginx `client_max_body_size` 또는 AWS ALB 요청 크기 제한으로 애플리케이션 앞단 차단 |
| 디스크 모니터링 | 업로드 경로(`~/.bibimbap/uploads`) 및 tmpdir 사용량 alert 설정 (예: 80% 도달 시 알림) |
| 업로드 인증 게이트 | 현재 게임 업로드는 로그인 필수 — 익명 대용량 업로드 차단. 권한 검사 유지 확인. |
| 동시 업로드 rate limit | Spring MVC thread pool 또는 리버스 프록시 connection limit으로 동시 대용량 요청 제한 |
| tmpdir 분리 | `spring.servlet.multipart.location` 으로 tmpdir를 전용 볼륨으로 분리해 시스템 영향 격리 |
## 관련 문서
- [owasp-dependency-check-guide.md](./owasp-dependency-check-guide.md) — 동일 B4 트랙 CVE 스캔 설정
- [security-remediation-checklist.md](./security-remediation-checklist.md) — 전체 보안 체크리스트
- [../maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md) — 배포 후 디스크 확인 절차

View File

@ -0,0 +1,92 @@
# OWASP Dependency-Check 운영 가이드
**적용 범위**: bibimbap `pom.xml``dependency-check-maven 12.2.2`
**결정 출처**: D-B4-cve (sid 20260629-175705)
**관련 체크리스트**: [security-remediation-checklist.md §B4](./security-remediation-checklist.md#b4-의존성세션운영-하드닝)
## 설정 개요
```xml
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.2</version>
<configuration>
<autoUpdate>false</autoUpdate>
<failBuildOnCVSS>7</failBuildOnCVSS>
<skipProvidedScope>true</skipProvidedScope>
</configuration>
<!-- executions 없음 — 빌드 phase 비bind -->
</plugin>
```
## 수동 실행
### 온라인 모드 (NVD 데이터 갱신 포함)
```bash
mvn dependency-check:check
```
NVD API 키가 없으면 요청 제한에 걸려 매우 느릴 수 있다. 처음 실행 시 수 분 소요 정상.
### 오프라인 모드 (캐시 재사용)
`autoUpdate=false`가 기본이므로 플러그인 설정 그대로 실행하면 로컬 캐시를 사용한다.
```bash
mvn dependency-check:check
```
캐시 위치: `~/.m2/repository/org/owasp/dependency-check-data/` (기본값).
캐시가 없으면 온라인 다운로드가 강제된다.
캐시를 명시적으로 지정하려면:
```bash
mvn dependency-check:check -Ddependency-check.dataDirectory=/path/to/cache
```
### 도커 dev 환경
```bash
docker compose exec app mvn dependency-check:check
```
호스트 `~/.m2` 가 마운트되어 있으면 캐시를 공유한다.
## 예외 기준
| 설정 | 값 | 설명 |
| --- | --- | --- |
| `failBuildOnCVSS` | `7` | CVSS 7.0 이상 high/critical만 빌드 실패 유발 |
| `skipProvidedScope` | `true` | WAS/컨테이너 제공 라이브러리(서블릿 API 등) 제외 |
CVSS 7 미만(low/medium) CVE는 보고서에 기록되지만 빌드를 실패시키지 않는다.
특정 CVE를 false positive로 억제하려면 suppression 파일 사용:
```bash
mvn dependency-check:check -Ddependency-check.suppressionFile=owasp-suppressions.xml
```
## 빌드 phase 비bind 이유
`executions` 블록을 두지 않아 일반 `mvn package`·`mvn test` 에 포함되지 않는다.
**이유**:
1. NVD 데이터 갱신/조회에 수 분이 소요되어 CI 빌드 시간 급증.
2. `autoUpdate=false` 오프라인 모드에서도 스캔 자체 시간이 30초~수 분.
3. CVE 결과는 즉각 hotfix가 아닌 우선순위 검토·예외 결정을 필요로 한다 — 빌드 자동 실패보다 수동 검토가 적합.
4. 로컬 dev 캐시 없는 환경(CI 클린 이미지)에서 온라인 의존성 생김.
**권장 주기**: 배포 전 수동 1회 실행 → 보고서 검토 → CVSS 7+ 항목 처리 후 배포.
## 보고서 위치
실행 후 `target/dependency-check-report.html` 생성.
콘솔에서 HIGH/CRITICAL 요약 출력됨.
## 관련 문서
- [multipart-size-risk.md](./multipart-size-risk.md) — 1GB 유지 리스크 (동일 B4 트랙)
- [security-remediation-checklist.md](./security-remediation-checklist.md) — 전체 보안 체크리스트

View File

@ -66,20 +66,32 @@
- `GameCatalog`는 빈 배열 기반 fallback이다. `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:8`, `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:18` - `GameCatalog`는 빈 배열 기반 fallback이다. `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:8`, `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:18`
- `GameController`는 DB 게임이 없으면 `GameCatalog` fallback을 시도한다. `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:111`, `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:116` - `GameController`는 DB 게임이 없으면 `GameCatalog` fallback을 시도한다. `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:111`, `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:116`
구현 결과 (2026-06-29, sid 20260629-175705):
- `abstracts/` 패키지 4파일(`Service`, `Request`, `Result`, `ErrorResult`) 전체 삭제. 참조 0 확인 후 제거.
- `src/main/webapp/WEB-INF/jsp/fragments/header.jspf` 삭제. include 참조 0 확인 후 제거.
- `src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java` 삭제.
- `GameController.gameDetail()` 의 GameCatalog fallback(redirect:/+정적뷰) 제거 → DB 미존재 게임 ID = **HTTP 404** (`ResponseStatusException(NOT_FOUND)`). 사용자 확정 결정(D-B2-404).
- `ApiExceptionControllerAdvice``ResponseStatusException` 핸들러 추가 — 기존 `Exception` catch-all 이 500으로 덮던 문제 수정, 404 status 보존.
- 회귀 가드 `GameLikeControllerTest.gameDetailThrowsNotFoundWhenGameMissing` 추가.
- `mvn test` 361 PASS. 런타임 스모크: `GET /game/99999` = 404, `GET /game/3` = 200 확인.
체크리스트: 체크리스트:
- [ ] `rg "abstracts|GameCatalog|header.jspf"`로 실제 참조를 재확인한다. - [x] `rg “abstracts|GameCatalog|header.jspf”`로 실제 참조를 재확인한다.
- [ ] `abstracts` 패키지를 삭제해도 컴파일이 깨지지 않는지 확인한다. - [x] `abstracts` 패키지를 삭제해도 컴파일이 깨지지 않는지 확인한다.
- [ ] `header.jspf` 삭제 전 JSP include 경로가 전부 `/WEB-INF/views/header.jsp`인지 확인한다. - [x] `header.jspf` 삭제 전 JSP include 경로가 전부 `/WEB-INF/views/header.jsp`인지 확인한다.
- [ ] `GameCatalog` fallback 제거 시 없는 게임 ID의 기대 동작을 `redirect:/` 또는 404로 결정한다. - [x] `GameCatalog` fallback 제거 시 없는 게임 ID의 기대 동작을 `redirect:/` 또는 404로 결정한다.**HTTP 404 확정**.
- [ ] 삭제 PR에는 기능 변경이 없도록 테스트와 수동 확인 범위를 좁힌다. - [x] 삭제 PR에는 기능 변경이 없도록 테스트와 수동 확인 범위를 좁힌다.
- [ ] 문서에서 제거된 프로토타입 흐름을 최신 구조로 갱신한다. - [x] 문서에서 제거된 프로토타입 흐름을 최신 구조로 갱신한다. → analysis 문서 stale 항목 갱신 완료.
완료 조건: 완료 조건:
- [ ] dead code 파일이 제거되거나 “보존 이유”가 문서화된다. - [x] dead code 파일이 제거되거나 “보존 이유”가 문서화된다. → abstracts 4파일·header.jspf·GameCatalog.java 삭제.
- [ ] `mvn test` 또는 최소 컴파일 검증이 통과한다. - [x] `mvn test` 또는 최소 컴파일 검증이 통과한다. → 361 PASS.
- [ ] 없는 게임 상세 접근의 동작이 명확하다. - [x] 없는 게임 상세 접근의 동작이 명확하다. → **HTTP 404** (ResponseStatusException(NOT_FOUND)).
> 구현 이력: [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md)
## B3. 좋아요/댓글 서버 영속화 연결 ## B3. 좋아요/댓글 서버 영속화 연결
@ -131,24 +143,36 @@
- MyBatis TRACE 로그가 켜져 있다. `src/main/resources/application.properties:25`, `src/main/resources/application.properties:26`, `src/main/resources/application.properties:27` - MyBatis TRACE 로그가 켜져 있다. `src/main/resources/application.properties:25`, `src/main/resources/application.properties:26`, `src/main/resources/application.properties:27`
- multipart 상한이 1GB다. `src/main/resources/application.properties:15`, `src/main/resources/application.properties:16`, `src/main/resources/application.properties:17`, `src/main/resources/application.properties:18` - multipart 상한이 1GB다. `src/main/resources/application.properties:15`, `src/main/resources/application.properties:16`, `src/main/resources/application.properties:17`, `src/main/resources/application.properties:18`
구현 결과 (2026-06-29, sid 20260629-175705):
- **Spring Boot**: `3.5.14-SNAPSHOT`**`3.5.16` (GA)** 고정. `pom.xml` spring-boot.version 갱신.
- **Snapshot repo 제거**: `pom.xml` 의 spring-snapshots repository·pluginRepository 블록 삭제. snapshot 없이 `mvn -B dependency:resolve` PASS 확인.
- **쿠키 하드닝**: `application.properties``server.servlet.session.cookie.http-only=true`, `server.servlet.session.cookie.same-site=lax` 추가. `application-live.properties`(신규) 에 `server.servlet.session.cookie.secure=true`. 런타임 `Set-Cookie` 확인: `JSESSIONID; HttpOnly; SameSite=Lax`.
- **MyBatis 로그**: `application.properties` ibatis TRACE → **WARN**. `application-dev.properties`(신규) 에 TRACE 복원(로컬 개발 전용).
- **Profile properties**: Spring 표준 `application-dev.properties`·`application-live.properties` 신규 생성. 기존 `db.properties`(DB 전용) 공존. 사용자 합의(D-B4-cfgloc).
- **OWASP Dependency-Check**: `pom.xml``dependency-check-maven 12.2.2` plugin 추가. executions 없음(빌드 phase 비bind). 수동 실행: `mvn dependency-check:check`. autoUpdate=false, failBuildOnCVSS=7, skipProvidedScope=true. 상세: [security/owasp-dependency-check-guide.md](./owasp-dependency-check-guide.md).
- **multipart 1GB 유지**: 사용자 확정(D-B4-multipart). 리스크 문서화: [security/multipart-size-risk.md](./multipart-size-risk.md).
체크리스트: 체크리스트:
- [ ] Spring Boot를 안정 release 버전으로 고정한다. - [x] Spring Boot를 안정 release 버전으로 고정한다.**3.5.16 GA**.
- [ ] snapshot repository와 pluginRepository 필요성을 제거하거나 문서화한다. - [x] snapshot repository와 pluginRepository 필요성을 제거하거나 문서화한다. → spring-snapshots 블록 삭제.
- [ ] `server.servlet.session.cookie.http-only=true`를 설정한다. - [x] `server.servlet.session.cookie.http-only=true`를 설정한다.
- [ ] live 프로필에서 `server.servlet.session.cookie.secure=true`를 설정한다. - [x] live 프로필에서 `server.servlet.session.cookie.secure=true`를 설정한다.`application-live.properties`.
- [ ] `server.servlet.session.cookie.same-site=lax` 또는 stricter 정책을 결정한다. - [x] `server.servlet.session.cookie.same-site=lax` 또는 stricter 정책을 결정한다.**lax** 설정.
- [ ] 운영 로그에서 MyBatis TRACE를 낮추고 민감 파라미터 노출 가능성을 확인한다. - [x] 운영 로그에서 MyBatis TRACE를 낮추고 민감 파라미터 노출 가능성을 확인한다. → base WARN, dev 전용 TRACE.
- [ ] multipart 상한을 실제 운영 허용치와 맞춘다. - [x] multipart 상한을 실제 운영 허용치와 맞춘다.**1GB 유지(사용자 확정), 리스크 문서화 완료**.
- [ ] 의존성 CVE 스캔 도구를 정한다. 후보: Dependabot, OWASP Dependency-Check, Maven Versions Plugin. - [x] 의존성 CVE 스캔 도구를 정한다. → **OWASP Dependency-Check maven plugin**.
- [ ] CVE 스캔 결과와 예외 처리 기준을 `docs/security/`에 기록한다. - [x] CVE 스캔 결과와 예외 처리 기준을 `docs/security/`에 기록한다. → [security/owasp-dependency-check-guide.md](./owasp-dependency-check-guide.md).
완료 조건: 완료 조건:
- [ ] 빌드가 snapshot repository 없이 재현 가능하다. - [x] 빌드가 snapshot repository 없이 재현 가능하다. → `mvn -B dependency:resolve` PASS.
- [ ] 운영 쿠키에 HttpOnly/Secure/SameSite 정책이 반영된다. - [x] 운영 쿠키에 HttpOnly/Secure/SameSite 정책이 반영된다. → 런타임 `Set-Cookie` 확인 완료.
- [ ] CVE 스캔 결과가 문서화된다. - [x] CVE 스캔 결과가 문서화된다. → OWASP DC plugin 설정 + guide 문서화.
- [ ] 운영 로그 레벨이 민감 데이터 노출을 최소화한다. - [x] 운영 로그 레벨이 민감 데이터 노출을 최소화한다. → base WARN.
> 구현 이력: [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md)
## 공통 테스트 매트릭스 ## 공통 테스트 매트릭스

View File

@ -0,0 +1,56 @@
# 2026-06-29 B2·B4·FE 작업 세션 핸드오프
**sid**: 20260629-175705 (resumed from 20260629-174807)
**완료 상태**: 3트랙 모두 완료·검증
**브랜치**: feat/v2
**최종 테스트**: `mvn test` 361 PASS
## 완료된 트랙
### B2 — 프로토타입 dead code 제거
- abstracts/ 패키지 4파일 삭제.
- fragments/header.jspf 삭제.
- game/GameCatalog.java 삭제.
- GameController.gameDetail() fallback 제거 → 없는 게임 ID = HTTP 404.
- ApiExceptionControllerAdvice에 ResponseStatusException 핸들러 추가(404 status 보존).
- GameLikeControllerTest.gameDetailThrowsNotFoundWhenGameMissing 추가.
- 런타임 스모크: /game/99999=404, /game/3=200.
### B4 — 의존성/세션/운영 하드닝
- Spring Boot 3.5.14-SNAPSHOT → 3.5.16 GA.
- pom에서 spring-snapshots repository·pluginRepository 제거.
- OWASP dependency-check-maven 12.2.2 비bind 추가.
- application.properties: ibatis TRACE→WARN, HttpOnly·SameSite=Lax 추가.
- application-dev.properties 신규(ibatis TRACE).
- application-live.properties 신규(cookie.secure=true).
- 런타임 Set-Cookie 확인 완료.
### FE — 프론트엔드 디자인 개선
- index 검색 `<details>` 접이식(prefill 시 open).
- 다크모드 input 테두리 대비 강화(login·signup·index·game-detail).
- 체크박스 터치타깃 1.15rem(login·signup).
- 브라우저 스모크 확인.
## 주요 결정
| ID | 결정 |
| --- | --- |
| D-B2-404 | 없는 게임 ID = HTTP 404 (ResponseStatusException(NOT_FOUND)). 사용자 확정. |
| D-B4-cve | CVE 스캔 = OWASP Dependency-Check 비bind. autoUpdate=false, CVSS>=7 실패. 사용자 확정. |
| D-B4-multipart | multipart 1GB 유지. 리스크 문서화: docs/security/multipart-size-risk.md. 사용자 확정. |
| D-B4-cfgloc | application-{profile}.properties 신규. db.properties 공존. |
| D-test-mockfix | BibimbapApplicationTests GameLikesMapper @MockBean 누락(B3 사전결함) 1줄 보강. |
## 이월 없음
이번 세션의 open_items(dev override online prefetch 함정)는 development/local-dev-setup.md에 노트 추가 완료.
## 참조
- 구현 이력: [changes/2026-06-29-b2-b4-fe-hardening.md](../changes/2026-06-29-b2-b4-fe-hardening.md)
- 보안 체크리스트: [security/security-remediation-checklist.md](../security/security-remediation-checklist.md)
- OWASP DC: [security/owasp-dependency-check-guide.md](../security/owasp-dependency-check-guide.md)
- Multipart 리스크: [security/multipart-size-risk.md](../security/multipart-size-risk.md)

View File

@ -8,3 +8,4 @@
- [2026-06-17-w3-feature-skeletons.md](./2026-06-17-w3-feature-skeletons.md) — W3(사이트 플랫폼) 5개 서브기능 골자 카탈로그. 기능당 목적·핵심동작·결합/의존·미결질문·후속진입점(윤곽 깊이, 미결 해소 안 함). 코드 현황 대조(tag/review 테이블 신규·Interceptor 미구현·`/game/**` 핸들러 미등록). 결합표 + 착수순서 제언(W3-2 1순위) + QG-1~3. 당장 구현 아님. `status: confirmed`(20260617-174635 세션서 5기능+착수순서 항목별 합의), source_session: 20260617-172407. ⚠️ **코드 현황 #2·#3 stale (2026-06-23 정정)** — w2-w4-skeletons 참조. - [2026-06-17-w3-feature-skeletons.md](./2026-06-17-w3-feature-skeletons.md) — W3(사이트 플랫폼) 5개 서브기능 골자 카탈로그. 기능당 목적·핵심동작·결합/의존·미결질문·후속진입점(윤곽 깊이, 미결 해소 안 함). 코드 현황 대조(tag/review 테이블 신규·Interceptor 미구현·`/game/**` 핸들러 미등록). 결합표 + 착수순서 제언(W3-2 1순위) + QG-1~3. 당장 구현 아님. `status: confirmed`(20260617-174635 세션서 5기능+착수순서 항목별 합의), source_session: 20260617-172407. ⚠️ **코드 현황 #2·#3 stale (2026-06-23 정정)** — w2-w4-skeletons 참조.
- [2026-06-23-w2-w4-full-design-summary.md](./2026-06-23-w2-w4-full-design-summary.md) — **W2+W3(잔여)+W4 풀설계 통합 요약/설계 인덱스**. 11기능(W2-1~6·W3-1/3-3/3-4/3-5·W4) 풀설계(W1-design 깊이, 오픈질문 0) 진입 인덱스 + 크로스-W 결정 매트릭스(정석 확정) + 교차정합 감사 verdict(HIGH 1 해소·동결 단일권위 유지) + 골자 stale 정정 3건 + 착수순서. 설계 본문은 `.atp/work-session/20260623-104307/implementation/`. 설계 전용(코드 0줄). `status: active`, source_session: 20260623-104307, resumed_from: 20260622-180054. - [2026-06-23-w2-w4-full-design-summary.md](./2026-06-23-w2-w4-full-design-summary.md) — **W2+W3(잔여)+W4 풀설계 통합 요약/설계 인덱스**. 11기능(W2-1~6·W3-1/3-3/3-4/3-5·W4) 풀설계(W1-design 깊이, 오픈질문 0) 진입 인덱스 + 크로스-W 결정 매트릭스(정석 확정) + 교차정합 감사 verdict(HIGH 1 해소·동결 단일권위 유지) + 골자 stale 정정 3건 + 착수순서. 설계 본문은 `.atp/work-session/20260623-104307/implementation/`. 설계 전용(코드 0줄). `status: active`, source_session: 20260623-104307, resumed_from: 20260622-180054.
- [2026-06-23-w2-w4-feature-skeletons.md](./2026-06-23-w2-w4-feature-skeletons.md) — W2(게임잼 6서브: 잼엔티티/심사위원역할/평가동결/심사평가/인기투표/시상집계) + W4(유저 배지/평판) 골자 카탈로그. W3 skeletons 포맷(목적·핵심동작·결합/의존[code-fact vs 해석 태깅]·미결질문[비해소]·후속진입점) + 결합표 + 착수순서 + QG-W2-A~D·W4-A. **W3 skeletons "코드 현황" stale 2건 정정**(RBAC 게이트·리뷰 인프라 실재 — 단 POST_WRITE/GAME_JAM_MANAGE enforcement 미연결, 리뷰는 하이브리드 6축+VIEW). code grounding: `.atp/work-session/20260623-104307/research/W2-W4-grounding.md`. `status: draft`(합의 전), source_session: 20260623-104307. - [2026-06-23-w2-w4-feature-skeletons.md](./2026-06-23-w2-w4-feature-skeletons.md) — W2(게임잼 6서브: 잼엔티티/심사위원역할/평가동결/심사평가/인기투표/시상집계) + W4(유저 배지/평판) 골자 카탈로그. W3 skeletons 포맷(목적·핵심동작·결합/의존[code-fact vs 해석 태깅]·미결질문[비해소]·후속진입점) + 결합표 + 착수순서 + QG-W2-A~D·W4-A. **W3 skeletons "코드 현황" stale 2건 정정**(RBAC 게이트·리뷰 인프라 실재 — 단 POST_WRITE/GAME_JAM_MANAGE enforcement 미연결, 리뷰는 하이브리드 6축+VIEW). code grounding: `.atp/work-session/20260623-104307/research/W2-W4-grounding.md`. `status: draft`(합의 전), source_session: 20260623-104307.
- [2026-06-29-b2-b4-fe-session.md](./2026-06-29-b2-b4-fe-session.md) — B2(dead code 제거)·B4(의존성/세션 하드닝)·FE(프론트 개선) 3트랙 완료 핸드오프. 주요 결정 5건(404/OWASP 비bind/multipart 유지/profile properties/mockfix). mvn test 361 PASS. 이월 없음.

33
pom.xml
View File

@ -26,7 +26,7 @@
<maven.compiler.release>${java.version}</maven.compiler.release> <maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.5.14-SNAPSHOT</spring-boot.version> <spring-boot.version>3.5.16</spring-boot.version>
<mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version> <mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version>
<lombok.version>1.18.44</lombok.version> <lombok.version>1.18.44</lombok.version>
<maven-clean-plugin.version>3.4.1</maven-clean-plugin.version> <maven-clean-plugin.version>3.4.1</maven-clean-plugin.version>
@ -205,28 +205,19 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.2</version>
<configuration>
<autoUpdate>false</autoUpdate>
<failBuildOnCVSS>7</failBuildOnCVSS>
<skipProvidedScope>true</skipProvidedScope>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<profiles> <profiles>
<profile> <profile>

View File

@ -1,7 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public class ErrorResult extends Result{
public ErrorResult(int status) {
super(status);
}
}

View File

@ -1,7 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public abstract class Request {
public boolean IsReceivedAllField() {
return true;
}
}

View File

@ -1,27 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public abstract class Result {
public int status;
public String message;
public Result() {}
public Result(int status) {
this.status = status;
switch (status)
{
case 200:
this.message = "Success"; return;
case 400:
this.message = "Invalid Request"; return;
case 401:
this.message = "세션 만료"; return;
case 1000:
this.message = "NULL USERS"; return;
default:
System.out.println("잘못된 status 케이스");
this.message = "";
return;
}
}
}

View File

@ -1,18 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
import jakarta.servlet.http.HttpSession;
public abstract class Service<Req extends Request, Res extends Result> {
public boolean is_login = false;
public abstract Res StartService(HttpSession session, Req request);
public Res ChackService(HttpSession session, Req request){
if (is_login && session.getAttribute("id") == null)
return (Res) new ErrorResult(401);
if(request != null && !request.IsReceivedAllField())
return (Res) new ErrorResult(401);
return StartService(session, request);
}
}

View File

@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -24,6 +25,16 @@ public class ApiExceptionControllerAdvice {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(body); return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(body);
} }
// ResponseStatusException(: gameDetail 404) 지정 status 보존한다.
// 구체적인 타입이라 아래 Exception 핸들러보다 우선 매칭된다.
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatus(ResponseStatusException exception) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("status", exception.getStatusCode().value());
body.put("message", exception.getReason() != null ? exception.getReason() : "not found");
return ResponseEntity.status(exception.getStatusCode()).body(body);
}
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception exception) { public ResponseEntity<Map<String, Object>> handleException(Exception exception) {
log.error("API request failed", exception); log.error("API request failed", exception);

View File

@ -2,7 +2,6 @@ package com.pandoli365.bibimbap.controller.api;
import com.pandoli365.bibimbap.data.GameData; import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.GameLikeData; import com.pandoli365.bibimbap.data.GameLikeData;
import com.pandoli365.bibimbap.game.GameCatalog;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper; import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GameLikesMapper; import com.pandoli365.bibimbap.mapper.GameLikesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper; import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
@ -23,9 +22,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -147,29 +146,7 @@ public class GameController {
return "game-detail"; return "game-detail";
} }
if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE || !GameCatalog.isValidId((int) id)) { throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return "redirect:/";
}
int intId = (int) id;
int idx = GameCatalog.toIndex(intId);
model.addAttribute("gameId", intId);
model.addAttribute("gameName", GameCatalog.NAMES[idx]);
model.addAttribute("creator", GameCatalog.CREATORS[idx]);
model.addAttribute("likeCount", GameCatalog.LIKE_COUNTS[idx]);
model.addAttribute("likeCountFormatted", String.format("%,d", GameCatalog.LIKE_COUNTS[idx]));
model.addAttribute("creatorNote", GameCatalog.CREATOR_NOTES[idx]);
model.addAttribute("gitUrl", safeExternalUrl(GameCatalog.GIT_URLS[idx]));
model.addAttribute("webglUrl", webglUrlForGame(intId));
model.addAttribute("webglFrameSrc", webglFrameSrc(webglUrlForGame(intId)));
model.addAttribute("webglDeployPath", webglUrlForGame(intId));
model.addAttribute("owner", false);
model.addAttribute("comments", List.of());
model.addAttribute("reviews", List.of());
model.addAttribute("currentUserId", sessionUserId(session));
model.addAttribute("userRole", (String) session.getAttribute("role"));
model.addAttribute("liked", false);
return "game-detail";
} }
@GetMapping("/game/{id}/edit") @GetMapping("/game/{id}/edit")

View File

@ -1,27 +0,0 @@
package com.pandoli365.bibimbap.game;
public final class GameCatalog {
private GameCatalog() {
}
public static final String[] NAMES = {};
public static final String[] CREATORS = {};
public static final int[] LIKE_COUNTS = {};
public static final String[] CREATOR_NOTES = {};
public static final String[] GIT_URLS = {};
public static final int COUNT = NAMES.length;
public static boolean isValidId(int id) {
return id >= 1 && id <= COUNT;
}
public static int toIndex(int id) {
return id - 1;
}
}

View File

@ -0,0 +1,3 @@
# dev 전용 오버라이드 (spring.profiles.active=dev 시 자동 로드)
# MyBatis SQL/파라미터 디버깅 — dev 한정
logging.level.org.apache.ibatis=TRACE

View File

@ -0,0 +1,3 @@
# live(운영) 전용 오버라이드 (spring.profiles.active=live 시 자동 로드)
# HTTPS 운영 — 세션 쿠키 Secure 강제
server.servlet.session.cookie.secure=true

View File

@ -25,6 +25,10 @@ app.webgl.frame-ancestors=self
# upload storage (static 트리 밖 — 웹서버 직접 서빙 차단, 컨트롤러 권한게이트 경유) # upload storage (static 트리 밖 — 웹서버 직접 서빙 차단, 컨트롤러 권한게이트 경유)
app.upload.game-storage-path=${user.home}/.bibimbap/uploads app.upload.game-storage-path=${user.home}/.bibimbap/uploads
# session cookie hardening
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=lax
# log # log
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
logging.level.org.apache.ibatis=TRACE logging.level.org.apache.ibatis=WARN

View File

@ -1,24 +0,0 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<header class="site-header">
<div class="site-header__inner">
<a class="logo" href="${pageContext.request.contextPath}/">비빔밥</a>
<form class="search" method="get" action="${pageContext.request.contextPath}/">
<input type="search" name="q" value="<c:out value='${q}'/>" placeholder="게임 이름 또는 제작자 검색" aria-label="검색"/>
<button type="submit">검색</button>
</form>
<nav class="auth">
<sec:authorize access="isAuthenticated()">
<a href="${pageContext.request.contextPath}/games/new">게임 올리기</a>
<form action="${pageContext.request.contextPath}/logout" method="post" class="logout-form">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit" class="link-btn">로그아웃</button>
</form>
</sec:authorize>
<sec:authorize access="!isAuthenticated()">
<a href="${pageContext.request.contextPath}/login">로그인</a>
<a class="btn-primary" href="${pageContext.request.contextPath}/register">회원가입</a>
</sec:authorize>
</nav>
</div>
</header>

View File

@ -952,6 +952,10 @@
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.18); box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.18);
} }
html[data-theme="dark"] .game-comments__form textarea,
html[data-theme="dark"] .game-comments__edit-form textarea {
border-color: rgba(255, 255, 255, 0.26);
}
/* ── 6축 평가 입력 그리드 ─────────────────────────────── */ /* ── 6축 평가 입력 그리드 ─────────────────────────────── */
.game-reviews__axes { .game-reviews__axes {

View File

@ -265,6 +265,11 @@
html[data-theme="dark"] .search-form__input { html[data-theme="dark"] .search-form__input {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none' stroke='%23aaa' stroke-width='2'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M12 12l5 5'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none' stroke='%23aaa' stroke-width='2'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M12 12l5 5'/%3E%3C/svg%3E");
} }
html[data-theme="dark"] .search-form__input,
html[data-theme="dark"] .advanced-search__input,
html[data-theme="dark"] .advanced-search__select {
border-color: rgba(255, 255, 255, 0.26);
}
.search-form__input::placeholder { .search-form__input::placeholder {
color: var(--text-muted); color: var(--text-muted);
} }
@ -394,6 +399,37 @@
background: var(--card-bg); background: var(--card-bg);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
} }
.advanced-search__summary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.25rem;
min-height: 44px;
box-sizing: border-box;
list-style: none;
cursor: pointer;
font-size: 0.9375rem;
font-weight: 700;
color: var(--text-muted);
-webkit-tap-highlight-color: transparent;
}
.advanced-search__summary::-webkit-details-marker { display: none; }
.advanced-search__summary::before {
content: "▶";
font-size: 0.65rem;
color: var(--accent);
transition: transform 0.2s ease;
}
details[open] .advanced-search__summary::before {
transform: rotate(90deg);
}
.advanced-search__summary:hover {
color: var(--text);
}
/* details 열렸을 때 form 상단 마진 */
details[open] > form.advanced-search-form {
margin-top: 0.5rem;
}
.advanced-search__row { .advanced-search__row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -475,6 +511,7 @@
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.625rem; gap: 0.625rem;
justify-content: center;
} }
@media (min-width: 480px) { @media (min-width: 480px) {
.card-grid { .card-grid {
@ -747,7 +784,12 @@
</a> </a>
<% } %> <% } %>
</div> </div>
<form class="advanced-search" role="search" action="<%= ctx %>/games/search" method="get"> <%
boolean advOpen = !searchKeyword.isBlank() || !searchCreator.isBlank() || !"latest".equals(searchSort) || !selectedTagSlugs.isEmpty();
%>
<details class="advanced-search"<%= advOpen ? " open" : "" %>>
<summary class="advanced-search__summary">상세 검색</summary>
<form class="advanced-search-form" role="search" action="<%= ctx %>/games/search" method="get">
<div class="advanced-search__row"> <div class="advanced-search__row">
<label class="search-form__label" for="adv-keyword">게임 이름·소개 검색</label> <label class="search-form__label" for="adv-keyword">게임 이름·소개 검색</label>
<input class="advanced-search__input" type="search" id="adv-keyword" name="keyword" value="<%= escapedSearchKeyword %>" placeholder="게임 이름·소개 검색" autocomplete="off" /> <input class="advanced-search__input" type="search" id="adv-keyword" name="keyword" value="<%= escapedSearchKeyword %>" placeholder="게임 이름·소개 검색" autocomplete="off" />
@ -796,7 +838,8 @@
</div> </div>
</div> </div>
<% } %> <% } %>
</form> </form>
</details>
<div class="search-history" id="search-history" hidden> <div class="search-history" id="search-history" hidden>
<div class="search-history__head"> <div class="search-history__head">
<span class="search-history__label">최근 검색</span> <span class="search-history__label">최근 검색</span>

View File

@ -120,6 +120,9 @@
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.25); box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.25);
} }
html[data-theme="dark"] .auth-field__input {
border-color: rgba(255, 255, 255, 0.26);
}
.auth-row { .auth-row {
display: flex; display: flex;
align-items: center; align-items: center;
@ -135,8 +138,8 @@
color: var(--text-muted); color: var(--text-muted);
} }
.auth-check input { .auth-check input {
width: 1rem; width: 1.15rem;
height: 1rem; height: 1.15rem;
accent-color: var(--accent); accent-color: var(--accent);
} }
.auth-button { .auth-button {

View File

@ -120,6 +120,9 @@
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.25); box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.25);
} }
html[data-theme="dark"] .auth-field__input {
border-color: rgba(255, 255, 255, 0.26);
}
.auth-grid { .auth-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@ -135,8 +138,8 @@
} }
.auth-check input { .auth-check input {
flex-shrink: 0; flex-shrink: 0;
width: 1rem; width: 1.15rem;
height: 1rem; height: 1.15rem;
margin-top: 0.15rem; margin-top: 0.15rem;
accent-color: var(--accent); accent-color: var(--accent);
} }

View File

@ -4,6 +4,7 @@ import com.pandoli365.bibimbap.badge.BadgeService;
import com.pandoli365.bibimbap.badge.ReputationService; import com.pandoli365.bibimbap.badge.ReputationService;
import com.pandoli365.bibimbap.mapper.BadgesMapper; import com.pandoli365.bibimbap.mapper.BadgesMapper;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper; import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewAxesMapper; import com.pandoli365.bibimbap.mapper.GameReviewAxesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewStatsMapper; import com.pandoli365.bibimbap.mapper.GameReviewStatsMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper; import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
@ -60,6 +61,10 @@ class BibimbapApplicationTests {
@MockBean @MockBean
private GamesMapper gamesMapper; private GamesMapper gamesMapper;
// B3 좋아요 영속화: GameController GameLikesMapper 주입 contextLoads 안정화(직전 세션 누락 보강)
@MockBean
private GameLikesMapper gameLikesMapper;
@MockBean @MockBean
private GameCommentsMapper gameCommentsMapper; private GameCommentsMapper gameCommentsMapper;

View File

@ -19,10 +19,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockHttpSession;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.web.server.ResponseStatusException;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowableOfType;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
@ -191,6 +194,21 @@ class GameLikeControllerTest {
verifyNoInteractions(gameLikesMapper); verifyNoInteractions(gameLikesMapper);
} }
// ---- B2 회귀 가드: 없는 게임 ID 상세 접근 404 (GameCatalog fallback 제거 ) ----
@Test
void gameDetailThrowsNotFoundWhenGameMissing() {
GameController controller = controller();
when(gamesMapper.getGame(99999L)).thenReturn(null);
ResponseStatusException ex = catchThrowableOfType(
() -> controller.gameDetail(99999L, new ExtendedModelMap(), new MockHttpSession()),
ResponseStatusException.class);
assertThat(ex).isNotNull();
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ---- helpers ---- // ---- helpers ----
private GameController controller() { private GameController controller() {