Compare commits
No commits in common. "fa9a888c5b25a202afbae390f972a97d7c2c96cd" and "4c16d16dd9322288f60bf23891596e875c68a6d7" have entirely different histories.
fa9a888c5b
...
4c16d16dd9
|
|
@ -1,40 +0,0 @@
|
|||
/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. 파괴적 조작은 사용자 확인 게이트.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# 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 우선.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
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 완료조건에 런타임 확인 기록 권고.
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
---
|
||||
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건 끊김.)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
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 | - |
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
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 의존성 다운로드 완료. |
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
---
|
||||
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: []
|
||||
```
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
---
|
||||
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` 내부 미러 상세 구성은 별도 인프라 필요로 범위 밖.
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
---
|
||||
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`)
|
||||
|
|
@ -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 전체 |
|
||||
| 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) |
|
||||
| 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 | **제거됨(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 | **제거됨(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) |
|
||||
| 4 | MED(부채) | **abstracts 패키지 전체 dead code** — `Service`/`Request`/`Result`/`ErrorResult` 어디서도 상속·호출 안 됨. `Service.ChackService` 는 세션키 `"id"`(line 12) 사용, 활성 코드는 `"userId"` 사용 → 재사용 시 인증 우회 함정. | abstracts/Service.java:12 |
|
||||
| 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 |
|
||||
| 6 | MED(부채) | **GameCatalog 완전 stub** — 모든 배열이 `{}` 빈 배열, `COUNT=0`. `GameController.gameDetail` 의 폴백 분기(line 111-128)는 영구 도달 불가(`isValidId` 는 항상 false). 죽은 분기 + 죽은 클래스. | game/GameCatalog.java:8-22, controller/api/GameController.java:111-128 |
|
||||
| 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())`)로 일관 처리됨(양호)하나 중앙 집중 필터 없음 → 신규 엔드포인트에서 체크 누락 위험. | 전 컨트롤러 |
|
||||
| 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) |
|
||||
| abstracts | **dead code** — 미사용 제네릭 Service/Request/Result/ErrorResult 베이스 (확인됨: 참조 0) → **패키지 전체 제거됨(2026-06-29)** |
|
||||
| abstracts | **dead code** — 미사용 제네릭 Service/Request/Result/ErrorResult 베이스 (확인됨: 참조 0) |
|
||||
| config | `UploadResourceConfig` — 업로드 경로의 `/profile/**` 정적 리소스 핸들러 등록 |
|
||||
| controller | 페이지(뷰) 컨트롤러 — `WebMvcController`(공통 페이지+에러), `RecruitController`(모집 페이지+API 혼합) |
|
||||
| controller/api | API/mutation 컨트롤러 — Game CRUD, GameUpload(zip/썸네일/파일), GameAsset(WebGL 서빙), User(인증/프로필), 예외 핸들러 |
|
||||
| data | 순수 POJO DTO 6종 (Lombok 미사용 — 수동 getter/setter) |
|
||||
| game | `GameCatalog` — **stub(빈 배열)**, 레거시 정적 게임 카탈로그 폴백(현재 무력) → **삭제됨(2026-06-29). 없는 게임 ID = HTTP 404** |
|
||||
| game | `GameCatalog` — **stub(빈 배열)**, 레거시 정적 게임 카탈로그 폴백(현재 무력) |
|
||||
| mapper | MyBatis `@Mapper` 인터페이스 6종 — 어노테이션 기반 SQL(XML 매퍼 0) |
|
||||
| security | `CsrfTokens` — 세션 기반 CSRF 토큰 발급/검증 유틸(static) |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -9,4 +9,3 @@
|
|||
- [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-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.
|
||||
|
|
|
|||
|
|
@ -35,19 +35,6 @@
|
|||
- **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.
|
||||
|
||||
> **의존성/플러그인 버전 변경 후 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 만 명시한다(자동병합 회피).
|
||||
```bash
|
||||
docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
| 순수 도메인 로직 (외부 의존 없음) | L1 |
|
||||
| MyBatis 매퍼 신규/SQL alias·집계 뷰 정의·변경 (DB-방언 계약) | L1 + L2 (dev DB contract) |
|
||||
| 인프라 설정 (container/env/compose) | L1 + 수동 스모크 |
|
||||
| HTTP 상태코드/예외 매핑 변경 (404/403 등, 전역 `@ExceptionHandler` 경유) | L1 + 런타임 스모크 |
|
||||
|
||||
**회귀 테스트 의무**: 버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다. revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다.
|
||||
|
||||
|
|
@ -239,15 +238,3 @@ SSRF·zip-slip·경로 boundary·sanitize 같은 보안핵심은 verification-ad
|
|||
- **verification-advisor 체크리스트**: 보안 거부 단위테스트는 정상경로 성공(present) 테스트와 쌍으로 차별 입증(distinguishing assertion) 확인 조항. 근거: W3-3 vacuous PASS.
|
||||
- **orchestrator scope-fence 조항(구조적)**: 단일기능 표시면이 선행기능 컨트롤러에 의존할 때 cross-feature display wiring fence 예외 선언 또는 deferral 분리. 근거: W4 배지 표시 AC 갭.
|
||||
- **design-advisor 조항**: 트레이드오프 concern 은 1차 권장안 + 조건부 전환 기준까지 선결정으로 좁힘. 근거: W3-3 SSRF IP핀닝 오선택.
|
||||
|
||||
### 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 미변경은 "입력 컨트롤 없음=정상"으로 판별.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
---
|
||||
kind: graphify-meta
|
||||
last_generated_at: 2026-06-30T00:00:00+0900
|
||||
source_commit: 2c48184
|
||||
last_generated_at: 2026-06-29T16:12:00+0900
|
||||
source_commit: fa6a301
|
||||
scopes:
|
||||
- full
|
||||
- src
|
||||
- docs
|
||||
---
|
||||
|
||||
# Graph — graphify 산출물 메타
|
||||
|
|
@ -31,15 +32,14 @@ docs/graph/
|
|||
└── audit.md
|
||||
```
|
||||
|
||||
scope 예시: `full`, `src`, `docs`, `src-features` 등.
|
||||
|
||||
> **scope 전략**: 이 프로젝트는 `full` 단일 scope를 기본으로 한다 (src 1840노드 + docs 198노드 = 2038노드 수준은 단일 그래프로 감당 가능). src↔docs 교차 엣지(코드↔설계문서 관계)가 살아있어야 작업 컨텍스트가 끊기지 않기 때문. 규모가 수만 노드 수준으로 비대해지면 scope 분리 재검토.
|
||||
scope 예시: `src`, `src-features`, `docs`, `full` 등. 한 번에 여러 scope 를 운용할 수 있다.
|
||||
|
||||
## Scopes
|
||||
|
||||
| scope | 마지막 생성 | 소스 커밋 | 대상 경로 | 요약 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `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). |
|
||||
| `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 기반). |
|
||||
| `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 권고.) |
|
||||
|
||||
## 갱신 시 체크리스트
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,4 @@
|
|||
|
||||
## 목록
|
||||
|
||||
- [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)
|
||||
_(아직 문서 없음)_
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
# 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) — 배포 후 디스크 확인 절차
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# 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) — 전체 보안 체크리스트
|
||||
|
|
@ -66,32 +66,20 @@
|
|||
- `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`
|
||||
|
||||
구현 결과 (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 확인.
|
||||
|
||||
체크리스트:
|
||||
|
||||
- [x] `rg “abstracts|GameCatalog|header.jspf”`로 실제 참조를 재확인한다.
|
||||
- [x] `abstracts` 패키지를 삭제해도 컴파일이 깨지지 않는지 확인한다.
|
||||
- [x] `header.jspf` 삭제 전 JSP include 경로가 전부 `/WEB-INF/views/header.jsp`인지 확인한다.
|
||||
- [x] `GameCatalog` fallback 제거 시 없는 게임 ID의 기대 동작을 `redirect:/` 또는 404로 결정한다. → **HTTP 404 확정**.
|
||||
- [x] 삭제 PR에는 기능 변경이 없도록 테스트와 수동 확인 범위를 좁힌다.
|
||||
- [x] 문서에서 제거된 프로토타입 흐름을 최신 구조로 갱신한다. → analysis 문서 stale 항목 갱신 완료.
|
||||
- [ ] `rg "abstracts|GameCatalog|header.jspf"`로 실제 참조를 재확인한다.
|
||||
- [ ] `abstracts` 패키지를 삭제해도 컴파일이 깨지지 않는지 확인한다.
|
||||
- [ ] `header.jspf` 삭제 전 JSP include 경로가 전부 `/WEB-INF/views/header.jsp`인지 확인한다.
|
||||
- [ ] `GameCatalog` fallback 제거 시 없는 게임 ID의 기대 동작을 `redirect:/` 또는 404로 결정한다.
|
||||
- [ ] 삭제 PR에는 기능 변경이 없도록 테스트와 수동 확인 범위를 좁힌다.
|
||||
- [ ] 문서에서 제거된 프로토타입 흐름을 최신 구조로 갱신한다.
|
||||
|
||||
완료 조건:
|
||||
|
||||
- [x] dead code 파일이 제거되거나 “보존 이유”가 문서화된다. → abstracts 4파일·header.jspf·GameCatalog.java 삭제.
|
||||
- [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)
|
||||
- [ ] dead code 파일이 제거되거나 “보존 이유”가 문서화된다.
|
||||
- [ ] `mvn test` 또는 최소 컴파일 검증이 통과한다.
|
||||
- [ ] 없는 게임 상세 접근의 동작이 명확하다.
|
||||
|
||||
## B3. 좋아요/댓글 서버 영속화 연결
|
||||
|
||||
|
|
@ -143,36 +131,24 @@
|
|||
- 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`
|
||||
|
||||
구현 결과 (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).
|
||||
|
||||
체크리스트:
|
||||
|
||||
- [x] Spring Boot를 안정 release 버전으로 고정한다. → **3.5.16 GA**.
|
||||
- [x] snapshot repository와 pluginRepository 필요성을 제거하거나 문서화한다. → spring-snapshots 블록 삭제.
|
||||
- [x] `server.servlet.session.cookie.http-only=true`를 설정한다.
|
||||
- [x] live 프로필에서 `server.servlet.session.cookie.secure=true`를 설정한다. → `application-live.properties`.
|
||||
- [x] `server.servlet.session.cookie.same-site=lax` 또는 stricter 정책을 결정한다. → **lax** 설정.
|
||||
- [x] 운영 로그에서 MyBatis TRACE를 낮추고 민감 파라미터 노출 가능성을 확인한다. → base WARN, dev 전용 TRACE.
|
||||
- [x] multipart 상한을 실제 운영 허용치와 맞춘다. → **1GB 유지(사용자 확정), 리스크 문서화 완료**.
|
||||
- [x] 의존성 CVE 스캔 도구를 정한다. → **OWASP Dependency-Check maven plugin**.
|
||||
- [x] CVE 스캔 결과와 예외 처리 기준을 `docs/security/`에 기록한다. → [security/owasp-dependency-check-guide.md](./owasp-dependency-check-guide.md).
|
||||
- [ ] Spring Boot를 안정 release 버전으로 고정한다.
|
||||
- [ ] snapshot repository와 pluginRepository 필요성을 제거하거나 문서화한다.
|
||||
- [ ] `server.servlet.session.cookie.http-only=true`를 설정한다.
|
||||
- [ ] live 프로필에서 `server.servlet.session.cookie.secure=true`를 설정한다.
|
||||
- [ ] `server.servlet.session.cookie.same-site=lax` 또는 stricter 정책을 결정한다.
|
||||
- [ ] 운영 로그에서 MyBatis TRACE를 낮추고 민감 파라미터 노출 가능성을 확인한다.
|
||||
- [ ] multipart 상한을 실제 운영 허용치와 맞춘다.
|
||||
- [ ] 의존성 CVE 스캔 도구를 정한다. 후보: Dependabot, OWASP Dependency-Check, Maven Versions Plugin.
|
||||
- [ ] CVE 스캔 결과와 예외 처리 기준을 `docs/security/`에 기록한다.
|
||||
|
||||
완료 조건:
|
||||
|
||||
- [x] 빌드가 snapshot repository 없이 재현 가능하다. → `mvn -B dependency:resolve` PASS.
|
||||
- [x] 운영 쿠키에 HttpOnly/Secure/SameSite 정책이 반영된다. → 런타임 `Set-Cookie` 확인 완료.
|
||||
- [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)
|
||||
- [ ] 빌드가 snapshot repository 없이 재현 가능하다.
|
||||
- [ ] 운영 쿠키에 HttpOnly/Secure/SameSite 정책이 반영된다.
|
||||
- [ ] CVE 스캔 결과가 문서화된다.
|
||||
- [ ] 운영 로그 레벨이 민감 데이터 노출을 최소화한다.
|
||||
|
||||
## 공통 테스트 매트릭스
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -8,4 +8,3 @@
|
|||
- [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-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
33
pom.xml
|
|
@ -26,7 +26,7 @@
|
|||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<spring-boot.version>3.5.16</spring-boot.version>
|
||||
<spring-boot.version>3.5.14-SNAPSHOT</spring-boot.version>
|
||||
<mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version>
|
||||
<lombok.version>1.18.44</lombok.version>
|
||||
<maven-clean-plugin.version>3.4.1</maven-clean-plugin.version>
|
||||
|
|
@ -205,19 +205,28 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</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>
|
||||
</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>
|
||||
<profile>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.pandoli365.bibimbap.abstracts;
|
||||
|
||||
public class ErrorResult extends Result{
|
||||
public ErrorResult(int status) {
|
||||
super(status);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.pandoli365.bibimbap.abstracts;
|
||||
|
||||
public abstract class Request {
|
||||
public boolean IsReceivedAllField() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -25,16 +24,6 @@ public class ApiExceptionControllerAdvice {
|
|||
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)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception exception) {
|
||||
log.error("API request failed", exception);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.pandoli365.bibimbap.controller.api;
|
|||
|
||||
import com.pandoli365.bibimbap.data.GameData;
|
||||
import com.pandoli365.bibimbap.data.GameLikeData;
|
||||
import com.pandoli365.bibimbap.game.GameCatalog;
|
||||
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
|
||||
|
|
@ -22,9 +23,9 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -146,7 +147,29 @@ public class GameController {
|
|||
return "game-detail";
|
||||
}
|
||||
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE || !GameCatalog.isValidId((int) id)) {
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# dev 전용 오버라이드 (spring.profiles.active=dev 시 자동 로드)
|
||||
# MyBatis SQL/파라미터 디버깅 — dev 한정
|
||||
logging.level.org.apache.ibatis=TRACE
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# live(운영) 전용 오버라이드 (spring.profiles.active=live 시 자동 로드)
|
||||
# HTTPS 운영 — 세션 쿠키 Secure 강제
|
||||
server.servlet.session.cookie.secure=true
|
||||
|
|
@ -25,10 +25,6 @@ app.webgl.frame-ancestors=self
|
|||
# upload storage (static 트리 밖 — 웹서버 직접 서빙 차단, 컨트롤러 권한게이트 경유)
|
||||
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
|
||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logging.level.org.apache.ibatis=WARN
|
||||
logging.level.org.apache.ibatis=TRACE
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<%@ 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>
|
||||
|
|
@ -952,10 +952,6 @@
|
|||
border-color: var(--accent);
|
||||
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축 평가 입력 그리드 ─────────────────────────────── */
|
||||
.game-reviews__axes {
|
||||
|
|
|
|||
|
|
@ -265,11 +265,6 @@
|
|||
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");
|
||||
}
|
||||
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 {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
@ -399,37 +394,6 @@
|
|||
background: var(--card-bg);
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -511,7 +475,6 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.625rem;
|
||||
justify-content: center;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
.card-grid {
|
||||
|
|
@ -784,12 +747,7 @@
|
|||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<%
|
||||
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">
|
||||
<form class="advanced-search" role="search" action="<%= ctx %>/games/search" method="get">
|
||||
<div class="advanced-search__row">
|
||||
<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" />
|
||||
|
|
@ -838,8 +796,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</details>
|
||||
</form>
|
||||
<div class="search-history" id="search-history" hidden>
|
||||
<div class="search-history__head">
|
||||
<span class="search-history__label">최근 검색</span>
|
||||
|
|
|
|||
|
|
@ -120,9 +120,6 @@
|
|||
border-color: var(--accent);
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -138,8 +135,8 @@
|
|||
color: var(--text-muted);
|
||||
}
|
||||
.auth-check input {
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.auth-button {
|
||||
|
|
|
|||
|
|
@ -120,9 +120,6 @@
|
|||
border-color: var(--accent);
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
@ -138,8 +135,8 @@
|
|||
}
|
||||
.auth-check input {
|
||||
flex-shrink: 0;
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: 0.15rem;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.pandoli365.bibimbap.badge.BadgeService;
|
|||
import com.pandoli365.bibimbap.badge.ReputationService;
|
||||
import com.pandoli365.bibimbap.mapper.BadgesMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameReviewAxesMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameReviewStatsMapper;
|
||||
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
|
||||
|
|
@ -61,10 +60,6 @@ class BibimbapApplicationTests {
|
|||
@MockBean
|
||||
private GamesMapper gamesMapper;
|
||||
|
||||
// B3 좋아요 영속화: GameController 가 GameLikesMapper 주입 → contextLoads 안정화(직전 세션 누락 보강)
|
||||
@MockBean
|
||||
private GameLikesMapper gameLikesMapper;
|
||||
|
||||
@MockBean
|
||||
private GameCommentsMapper gameCommentsMapper;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,13 +19,10 @@ import org.springframework.http.HttpStatus;
|
|||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
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.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
|
@ -194,21 +191,6 @@ class GameLikeControllerTest {
|
|||
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 ----
|
||||
|
||||
private GameController controller() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue