chore(graph): 클린 풀 재생성 메타 갱신 + 타세션 work-session 리포트 추적

- docs/graph/index.md: source_commit 360078a→20789a2, 2089노드/4719엣지/128커뮤니티,
  고스트 중복(452 exact+244 fuzzy) 제거. "재생성 요청 중" 해소.
- ADR-0010: 직전 세션들(103443/105459/143405/160000/170024) 미커밋 work-session
  리포트 추적 편입(housekeeping, 코드 변경 없음).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BaZnrbiYgWc9JJMZ9Czb
This commit is contained in:
이정수 2026-06-30 19:24:58 +09:00
parent 20789a2922
commit 661e570069
12 changed files with 1017 additions and 3 deletions

View File

@ -0,0 +1,56 @@
# ATP Session Report
schema_version: "2"
sid: 20260630-103443
user_request: |
dependency-check:update-only 실행 시 NVD retry 경고 반복, 설치가 안 된다고 판단
started_at: 2026-06-30T10:34:43+09:00
## Advisor Invocation Decision Log
- advisor: requirements-advisor
decision: skip
rationale: 요청 명확 — NVD download 왜 느린지 + 설정 방법
checked_at: 2026-06-30T10:34:43+09:00
- advisor: research-advisor
decision: skip
rationale: NVD API key 설정법은 공개 문서 기준 이미 알려진 사항
checked_at: 2026-06-30T10:34:43+09:00
- advisor: design-advisor
decision: skip
rationale: 코드 변경 없음, 설정 옵션 2가지 제시
checked_at: 2026-06-30T10:34:43+09:00
- advisor: implementation-advisor
decision: skip
rationale: pom.xml 수정 여부 사용자 선택 후 마이크로 편집
checked_at: 2026-06-30T10:34:43+09:00
- advisor: verification-advisor
decision: call
rationale: pom.xml 수정 시 빌드 검증 필요 (현재 코드 변경 없어 skip)
checked_at: 2026-06-30T10:34:43+09:00
## Diagnosis
- 프로세스 PID 89764 현재 실행 중 (9:20AM 시작, 1시간 10분째)
- DB 위치: ~/.m2/repository/org/owasp/dependency-check-data/11.0/odc.mv.db (80MB)
- 현재 다운로드 중: ~6% (20,000 / 361,769 records)
- 원인: NVD API 키 없음 → rate limit 5 req/30s → retry 반복
- 결론: 실패가 아니라 극도로 느린 정상 동작
## Summary
NVD API Key 미설정으로 rate limiting 발생. 키 없이 완료까지 예상 1~3시간.
키 획득 후 30배 빠른 다운로드 가능.
## Invocations: []
## verified_by_me
- (코드 변경 없음 — 검증 skip)
## needs_user_verification
- NVD API Key 발급 (사용자 직접)
- update-only 재실행 확인
## graph_refresh
skip: no code change
## open_items: []
ended_at: TBD

View File

@ -0,0 +1,71 @@
---
phase: verification
agent: verification-advisor
agent_version: 1
generated_at: 2026-06-30T02:18:20Z
concerns: []
concerns_checked: true
---
# 검증 결과
## Acceptance Criteria (입력 받은 그대로 인용)
1. `GET /posts` 응답 200 반환
2. 앱 로그에 `PSQLException: could not determine data type of parameter` 에러 없음
3. PostsMapper.java `listPublishedKeyset` SQL에 `::bigint`, `::timestamptz` 캐스트 존재
4. PostsMapper.java `update()` SQL에 `category_id = #{categoryId}` 포함
## 실행된 전략
변경 scope: `src/main/java/com/pandoli365/bibimbap/mapper/PostsMapper.java` (MyBatis 매퍼 SQL 변경)
전략 규칙 매핑: MyBatis 매퍼 신규/SQL alias·집계 뷰 정의·변경 → L1 + L2 (dev DB contract) + 런타임 스모크
| id | cmd | exit | severity | 결과 |
|---|---|---|---|---|
| L1-unit | `./mvnw -o test` | 0 | blocker | pass |
| L2-runtime-smoke | `curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/posts` | 0 | blocker | pass |
| L2-log-scan | `docker logs bibimbap-app --since=5m \| grep -i "PSQLException\|could not determine"` | 1(no match) | blocker | pass |
| L1-code-grep | `grep -n "::bigint\|::timestamptz\|category_id = " PostsMapper.java` | 0 | blocker | pass |
### L1 단계 분해
| 단계 | 결과 |
|---|---|
| L1 컴파일 (javac 121 소스) | pass |
| L1 unit+regression (361 tests) | pass — Tests run: 361, Failures: 0, Errors: 0, Skipped: 0 |
| L2 contract (dev DB, GET /posts) | pass — HTTP 200 |
| L2 로그 스캔 (PSQLException) | clean — 0건 |
## 실패 상세 (해당 시)
없음.
## Acceptance 매칭
| criterion | 매칭 전략 | 판정 |
|---|---|---|
| AC1: GET /posts 응답 200 | L2 런타임 스모크 (`curl -w "%{http_code}"` → `200`) | pass |
| AC2: 로그에 PSQLException 없음 | L2 로그 스캔 (`docker logs \| grep` → 0건) | pass |
| AC3: listPublishedKeyset에 `::bigint`, `::timestamptz` 캐스트 존재 | L1 코드 grep (line 44: `#{categoryId}::bigint`, line 46: `#{cursorCreatedAt}::timestamptz`, line 47: `#{cursorId}::bigint`) | pass |
| AC4: update() SQL에 `category_id = #{categoryId}` 포함 | L1 코드 grep (line 122: `SET category_id = #{categoryId},`) | pass |
## 코드 grep 상세 (AC3/AC4)
```
44: AND (#{categoryId}::bigint IS NULL OR p.category_id = #{categoryId}::bigint)
46: #{cursorCreatedAt}::timestamptz IS NULL
47: OR (p.created_at, p.id) < (#{cursorCreatedAt}::timestamptz, #{cursorId}::bigint)
122: SET category_id = #{categoryId},
```
- AC3 `::bigint`: line 44, 47 확인
- AC3 `::timestamptz`: line 46, 47 확인
- AC4 `category_id = #{categoryId}`: line 122 (`SET category_id = #{categoryId},`) 확인
## 종합 판정
```
overall: pass
rollback_signal: none
```

View File

@ -0,0 +1,175 @@
---
schema_version: "2"
sid: "20260630-105459"
started_at: "2026-06-30T10:54:59"
ended_at: "2026-06-30T11:32:00"
user_request: "포스팅 메뉴 진입시 500 Error 발생해"
Summary: |
PostsMapper.listPublishedKeyset SQL에서 null 파라미터를 타입 없이 PostgreSQL에 전달해
PSQLException: could not determine data type of parameter $1 → 500 발생.
::bigint / ::timestamptz 명시 캐스트 추가로 수정.
update() SQL category_id 누락 버그 함께 수정. GET /posts 200 확인.
Invocations:
- agent: research-advisor
result: "포스팅 Controller/Mapper 구조 파악 + 500 원인 후보 목록 작성"
- agent: verification-advisor
result: "AC 4항목 전체 pass (HTTP 200, PSQLException 0건, SQL 캐스트 확인, category_id 확인)"
Decisions:
- "PSQLException $1 타입 미결정 → ::bigint / ::timestamptz 캐스트로 수정 (테이블 미존재 가설은 DDL 멱등 적용 후 NOTICE already-exists로 기각)"
- "update SQL category_id 누락 → 동일 파일 수정에 포함"
verified_by_me:
- "L1: compile pass (spring-boot:run 재시작 컴파일)"
- "L2: GET /posts → HTTP 200"
- "log scan: PSQLException 0건 (clean)"
needs_user_verification: "(없음)"
graph_refresh: "skip: no-graphify"
user_signals:
positive: []
negative: []
open_items:
- "회귀 테스트 미추가 (버그 수정 규약 미준수): PostControllerTest에 'null cursor 파라미터로 GET /posts 200' 시나리오 추가 필요. 후속 세션에서 처리."
commit: "168671b"
---
# Advisor Invocation Decision Log
# 각 advisor 호출/스킵 판단 즉시 1줄 append
- advisor: requirements-advisor
decision: skip
rationale: '요청 명확 — 포스팅 메뉴 진입 시 500 에러. 추가 요구사항 분해 불필요.'
checked_at: "2026-06-30T10:54:59"
- advisor: graphify-lookup-advisor
decision: skip
rationale: 'graphify add-on 없음. research-advisor 직접 호출.'
checked_at: "2026-06-30T10:54:59"
- advisor: design-advisor
decision: skip
rationale: '원인 확정 후 수정 경로 단일 — 파라미터 캐스트 + SQL 컬럼 추가. 설계 분기 없음.'
checked_at: "2026-06-30T11:20:00"
- advisor: implementation-advisor
decision: skip
rationale: '단일 파일 2-hunk 수정. orchestrator 직접 수행.'
checked_at: "2026-06-30T11:20:00"
---
Retrospective:
signals:
positive: []
negative: []
what_went_well:
- "초기 가설(테이블 미존재) DDL 멱등 적용 → NOTICE already-exists 로 빠르게 기각하고,
앱 로그 확인으로 실제 원인(PSQLException $1 타입 미결정)을 2단계 이내에 특정했다.
'가설 → DDL 검증 → 로그 확인' 순서가 500 버그 디버깅 효율 측면에서 유효했음."
- "단일 파일 2-hunk 수정(캐스트 추가 + category_id 누락)에 implementation-advisor를 거치지 않고
orchestrator가 직접 수행 — 불필요한 위임 없이 세션 시간을 절약."
- "두 번째 버그(update SQL category_id 누락)를 동일 파일 수정에 포함해 커밋 경계를 깔끔하게 유지."
what_to_improve:
- "PostsMapper.listPublishedKeyset 는 '#{param}::cast IS NULL' 패턴으로 null을 처리하는 반면,
JamsMapper·GamesMapper의 keyset 쿼리는 '<if test=\"... != null\">' 동적 XML 분기로 null을 회피한다.
두 패턴이 혼재하며, '::cast IS NULL' 패턴은 PostgreSQL이 prepared statement $1의 타입을 추론할 때
null 리터럴을 타입 미지정으로 전달하면 PSQLException이 발생한다는 함정이 있다.
이 패턴을 사용하는 매퍼는 반드시 명시 캐스트(::bigint / ::timestamptz 등)가 있어야 하며,
docs에 규약으로 기록하지 않으면 신규 매퍼 작성 시 동일 패턴 재발이 높다."
- "버그 재현 테스트(회귀 테스트) 없이 커밋됐다. verification-strategies.md 에는
'버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다'고 명시돼 있으나 이번 세션은 적용하지 않았다."
- "docker-compose.override.yml 존재 시 '도커 이미지 재빌드가 필요한 것 아닌가' 라는 혼선이 발생했다
(회고 포인트 3). override가 base image를 교체하기 때문에 docker compose up --build 는 override 환경에서
이미지 rebuild 후에도 override가 그 이미지를 다시 무시한다. local-dev-setup.md에 이 함정을
명시적 경고로 추가하지 않으면 재발 가능."
memory_candidates:
- name: mybatis-postgres-null-param-explicit-cast
type: feedback
description: "MyBatis + PostgreSQL에서 nullable 파라미터를 '#{p}::type IS NULL' 패턴으로 쓸 때 명시 캐스트 필수 — 미지정 시 PSQLException $N 타입 미결정"
body_draft: |
## Why
PostgreSQL은 prepared statement에서 null 리터럴의 타입을 추론할 수 없다.
MyBatis가 null 파라미터를 바인딩하면 $N의 타입이 결정되지 않아
`PSQLException: could not determine data type of parameter $1` 이 발생한다.
## 패턴 및 규약
nullable Long/OffsetDateTime 파라미터를 '#{p} IS NULL' 조건으로 쓸 때:
- **필수**: `#{p}::bigint`, `#{p}::timestamptz` 등 명시 캐스트 추가
- **대안**: `<if test="p != null">` 동적 XML 분기로 null 케이스 분리
현재 bibimbap 매퍼에는 두 패턴이 혼재함:
- PostsMapper.listPublishedKeyset → '::cast IS NULL' 패턴 (캐스트 추가로 수정됨 commit 168671b)
- JamsMapper.listVisibleKeyset, GamesMapper.listVisibleKeyset/searchVisibleKeyset → '<if test>' 분기 패턴
신규 keyset 페이징 매퍼 작성 시 '<if test>' 분기를 우선 권장.
'::cast IS NULL' 패턴을 쓴다면 명시 캐스트 누락 여부를 code review에서 확인한다.
## How to apply
1. nullable 파라미터를 IS NULL 조건으로 쓰는 MyBatis SQL을 rg로 전수 확인:
`rg '#{[^}]+}\s+IS\s+NULL' src/main/java --type java`
2. 각 라인에 `::type` 캐스트가 없으면 추가.
3. 또는 해당 조건 블록을 `<if test="p != null">` 으로 재작성.
rationale_for_saving: "동일 패턴 재발 가능성 높음 — 신규 페이징 매퍼 추가 시마다 잠재적으로 발생. 코드에서 유도 불가(컴파일 시점에 탐지 안 됨, L1 단위테스트도 @MockBean으로 회피)."
signal_source: observation
docs_sync_target: "/Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md"
memory_optional: true
- name: docker-compose-override-masks-build
type: feedback
description: "docker-compose.override.yml 존재 시 'docker compose up --build' 로 Dockerfile 이미지를 새로 구워도 override가 base image를 교체하므로 빌드가 로컬 dev에 반영되지 않는다"
body_draft: |
## Why
`docker compose up --build app` 은 base `docker-compose.yml``build:` 지시를 실행해
Dockerfile로 WAR를 굽는다. 그러나 `docker-compose.override.yml`이 존재하면
override가 `app` 서비스의 image/command/volumes를 재정의하므로
방금 구운 이미지는 override에 의해 무시된다.
## 규약
- 로컬 dev: `docker compose up -d app` (--build 불필요/무효)
코드 변경 반영: `docker compose restart app`
- 배포 이미지 빌드: override를 제외하고 base만 명시
`docker compose -f docker-compose.yml up -d --build app`
## How to apply
`--build` 를 쓰기 전에 `docker-compose.override.yml` 존재 여부 확인:
```bash
ls docker-compose.override.yml
```
존재하면 로컬 dev 경로(`restart`)를 사용하고,
Dockerfile 이미지가 필요한 경우 `-f docker-compose.yml` 로 override를 명시적으로 제외한다.
rationale_for_saving: "override 존재 사실 자체는 local-dev-setup.md에 있으나 '--build 무효' 함정이 명시되지 않아 재발 가능. 코드에서 유도 불가."
signal_source: observation
docs_sync_target: "/Users/wemadeplay/workspace/stz/bibimbap/docs/development/local-dev-setup.md"
memory_optional: true
- name: bug-fix-regression-test-required
type: feedback
description: "버그 수정 커밋에 재현 테스트 포함 의무(verification-strategies.md 기존 규약) — 이번 세션에서 미적용"
body_draft: |
## Why
verification-strategies.md §회귀 테스트 의무:
"버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다.
revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다."
이번 세션(20260630-105459)에서 PSQLException 재현 테스트를 추가하지 않았다.
MyBatis + PostgreSQL 통합 계층이라 L1 단위테스트만으로는 재현이 어렵지만,
최소한 PostControllerTest에 "null cursor 파라미터로 GET /posts 200" 시나리오를 추가해야 함.
## How to apply
버그 수정 세션 종료 전 체크:
1. 버그 재현 조건(null cursor)을 트리거하는 테스트가 존재하는가?
2. 없으면 unit 레벨(mock) 테스트라도 추가 — 매퍼 호출 시 null 파라미터 경로 포함.
3. open_items에 "회귀 테스트 추가" 를 명시하고 후속 세션에서 처리.
rationale_for_saving: "기존 규약이 있음에도 이번 세션에서 미준수. open_items에도 없음 — 다음 세션이 이를 모르면 영구 누락 가능."
signal_source: observation
docs_sync_target: null
memory_optional: false
protocol_feedback:
- "버그 수정 세션에서 verification-advisor가 회귀 테스트 존재 여부를 AC 항목으로 체크하지 않았다.
verification-advisor 체크리스트에 '버그 수정 커밋 시 재현 테스트 존재 여부 확인' 항목을 추가 권고.
(verification-strategies.md §회귀 테스트 의무 조항이 verification-advisor의 실행 체크리스트에 연결되지 않음 — 구조적 갭.)"
- "연구 후보 가설을 DDL로 검증하는 과정(already-exists NOTICE로 기각)은 효율적이었지만,
프로토콜상 '초기 가설 검증 우선순위: 앱 로그 → DDL 검증' 순서를 명시하는 규약이 없다.
버그 디버깅 세션 체크리스트에 '먼저 앱 로그/에러 스택 확인 후 외부 가설(DB 스키마 등) 검증' 순서를 추가하면
DDL 적용 같은 부작용이 있는 검증 단계를 줄일 수 있다."
applied_changes: []

View File

@ -0,0 +1,190 @@
---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-30T11:10:00+09:00
concerns:
- "PostsMapper.update() SQL에 category_id 누락 — 카테고리 변경이 DB에 반영되지 않음 (버그, 500 직접 원인은 아님)"
- "PostController의 editForm/update/delete 핸들러가 getPublished()를 사용해 DRAFT 포스트 조회 불가 — DRAFT 저장 후 재편집 시 null → redirect:/posts 또는 404 (500 간접 원인 후보)"
- "posts-detail.jsp에서 post attribute null 체크 없이 post.getTitle() 등 직접 접근 — post가 null이면 NullPointerException → 500"
- "PostAdminController에 인증/권한 체크 부재 — Spring Security 미사용 환경에서 /admin/** RbacInterceptor 보호 범위 확인 필요"
source_confidence: high
workers_spawned: 4
---
# 조사 결과
## 주제
bibimbap 프로젝트 포스팅 메뉴 진입 시 500 Error 원인 조사
---
## 포인트별 발견
### 포인트 1: 포스팅 Controller URL 매핑
- 경로: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/java/com/pandoli365/bibimbap/controller/PostController.java`
- 신뢰도: 확인됨
| HTTP | 경로 | 핸들러 메서드 | 반환 |
|------|------|-------------|------|
| GET | `/posts` | `list` | View: `posts-list` |
| GET | `/posts/{id}` | `detail` | View: `posts-detail` (null이면 redirect:/posts) |
| GET | `/posts/new` | `createForm` | View: `posts-form` (미인증 시 redirect:/login) |
| POST | `/posts` | `create` | JSON ResponseEntity |
| GET | `/posts/{id}/edit` | `editForm` | View: `posts-form` (미인증 시 redirect:/login) |
| POST | `/posts/{id}` | `update` | JSON ResponseEntity |
| POST | `/posts/{id}/delete` | `delete` | JSON ResponseEntity |
- 경로: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/java/com/pandoli365/bibimbap/controller/PostAdminController.java`
- 신뢰도: 확인됨
| HTTP | 경로 | 핸들러 메서드 |
|------|------|-------------|
| GET | `/admin/post-categories` | `postCategoriesPage` → View: `admin-post-categories` |
| POST | `/admin/post-categories` | `createCategory` |
| POST | `/admin/post-categories/{id}` | `updateCategory` |
| POST | `/admin/post-categories/{id}/delete` | `deleteCategory` |
---
### 포인트 2: Service / Mapper 체인
- 신뢰도: 확인됨
**PostController 의존 Bean:**
- `PostsMapper` (annotation 기반 MyBatis) — `listPublishedKeyset`, `getPublished`, `insert`, `update`, `softDelete`
- `PostCategoriesMapper``listActive`, `getActive`, `getById`, `insert`, `update`, `delete`, `countPostsByCategory`
- `PostMarkdownService` — CommonMark 파싱 + Jsoup basicWithImages safelist sanitize
- `OgPreviewService` — SSRF-safe HTTP fetch + OG 메타 파싱, 실패 시 graceful empty
**별도 PostService/PostServiceImpl 없음.** 비즈니스 로직이 Controller에 직접 집약. XML Mapper 없음, 전부 `@Select`/`@Insert`/`@Update` annotation.
---
### 포인트 3: 500 에러 원인 후보 (가능성 순)
#### [1순위 — 가장 유력] posts-detail.jsp NPE (확인됨)
- 파일: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-detail.jsp`
- `PostData post = (PostData) request.getAttribute("post");` 이후 null 체크 없이 `post.getTitle()`, `post.getId()` 등 직접 접근
- **`post` attribute가 null인 상태로 JSP가 렌더링되면 NullPointerException → 500**
- 발생 경로: `PostController.detail()``postsMapper.getPublished(id)`를 호출해 null을 받으면 `redirect:/posts`로 분기하므로 Controller 레벨에서는 방어됨. 그러나 다른 진입 경로(forward, include, 테스트 호출)에서 post attribute 누락 시 노출 가능.
#### [2순위] DRAFT 포스트 조회 시 null 처리 흐름 (확인됨)
- 파일: `PostController.java` line 167, 206, 252
- `editForm`, `update`, `delete` 핸들러 모두 `postsMapper.getPublished(id)` 사용
- `getPublished` SQL: `WHERE p.status = 'PUBLISHED' AND p.is_delete = false`
- **DRAFT 상태로 저장된 포스트 ID로 editForm 접근 시 → null → `if (post == null) return "redirect:/posts"`** — 500이 아닌 redirect지만, JS fetch로 update/delete 호출 시 null → NPE 가능성:
- `PostController.update()` line 206: `existing.setCategoryId(categoryId)``existing`이 null이면 NPE → 500
- `PostController.delete()` line 252: `postsMapper.softDelete(id)` 호출 전 null 체크 여부 추가 확인 필요
#### [3순위] PostsMapper.update() — category_id 컬럼 누락 (확인됨)
- 파일: `PostsMapper.java` line 120-135
- UPDATE SQL 대상 컬럼: `title, body_markdown, body_sanitized_html, link_url, og_*, status, updated_at`
- **`category_id = #{categoryId}` 누락** → 카테고리 변경이 DB에 반영되지 않음
- 500 직접 원인은 아니나, 카테고리 변경 요청 시 데이터 정합성 손상
#### [4순위] PostsMapper.insert() 직후 ID null 체크 (확인됨)
- `PostController.create()` line 149: `if (post.getId() == null)` 체크 후 500 응답
- DB INSERT 실패(constraint violation, DB 연결 오류 등) 시 MyBatis 예외 → Spring이 500으로 변환
- 포스트 생성 시 `category_id` FK 참조 실패(비활성 카테고리 등) 가능
#### [5순위] Bean 주입 / 빌드 이슈 (확인됨 — 현재 정상)
- Spring Boot 3.5.16, MyBatis Spring Boot 3.0.5: 호환 정상
- 모든 Bean 생성자 주입, `@Mapper` 어노테이션 정상
- JSTL 미사용(scriptlet 방식), tomcat-embed-jasper compile scope: JSP 렌더링 정상
- ViewResolver: prefix=`/WEB-INF/views/`, suffix=`.jsp` 정상
- **기동 실패 유발 구조적 결함 없음**
#### [6순위] PostAdminController 권한 체크 부재 (추정)
- `PostController``gate.has(session, "POST_WRITE")` 명시 검사
- `PostAdminController`에는 `PermissionGate` 주입 자체 없음
- `InterceptorConfig`에서 `/admin/**``RbacInterceptor` 등록 확인됨 — 필터 레벨 보호 존재
- `/admin/jams/**`는 제외(addExcludePatterns). `/admin/post-categories`는 포함 여부 추가 확인 필요
- 500 직접 원인 아님; 인증 우회 시 NPE 가능성
---
### 포인트 4: 관련 JSP 파일 경로
- 신뢰도: 확인됨
| JSP | 절대 경로 |
|-----|----------|
| 포스트 목록 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-list.jsp` |
| 포스트 상세 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-detail.jsp` |
| 포스트 작성/수정 폼 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-form.jsp` |
| 관리자 카테고리 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/admin-post-categories.jsp` |
**admin-post-categories.jsp 주의사항:**
- `var BASE = '/admin/post-categories';` — context path 미포함 하드코딩
- 다른 JSP들은 `<%= ctx %>` 변수(contextPath 동적 주입)를 사용하는 것과 비교해 inconsistent
- context root가 `/` 이외로 배포 시 404 발생
---
### 포인트 5: 최근 git 변경 사항 (포스팅 관련)
- 신뢰도: 확인됨
| 커밋 해시 | 메시지 |
|---------|--------|
| `6047a39` | `feat(board): W3-3 포스팅 보드 — 공지/블로그 + OG 미리보기(SSRF 방어) + 유니티 피드 감시` |
- 포스팅 기능이 이 단일 커밋(32파일, 5327줄 추가)으로 초기 도입됨 (2026-06-29)
- **후속 수정 커밋 없음** — 초기 도입 이후 한 번도 수정되지 않은 상태
- 이전 세션 로그에서 발견된 PKIX 빌드 오류(`spring-boot-dependencies:3.5.14-SNAPSHOT`)는 `e51ab01` 커밋으로 해소됨 (현재 정식 버전 사용)
---
## 종합 판단
**"포스팅 메뉴 진입 시 500"의 가장 유력한 원인은 `posts-detail.jsp`의 null 체크 부재(NPE)다.**
구체적 시나리오:
1. `GET /posts/{id}` 요청 → `PostController.detail()``postsMapper.getPublished(id)` 호출
2. `id`가 DRAFT 상태이거나 삭제된 포스트이거나 존재하지 않으면 → `null` 반환 → `redirect:/posts`로 처리됨 (Controller 레벨 방어 있음)
3. 그러나 **"포스팅 메뉴 진입"이 `GET /posts` (목록)라면**: `posts-list.jsp`로 렌더링되며 `postCategoriesMapper.listActive()`가 빈 결과를 반환할 경우 JSP 루프는 정상 처리. 별도 NPE 없음.
4. **만약 카테고리가 하나도 없는 상태에서 `createForm`(`GET /posts/new`) 진입 시**: `postCategoriesMapper.listActive()`가 빈 리스트를 반환하더라도 JSTL/scriptlet 루프이므로 NPE 없음.
따라서 **정확한 500 재현 경로는 다음 중 하나**:
- `GET /posts/{존재하지 않거나 DRAFT인 id}` → Controller null 체크 → redirect (500 아님)
- `POST /posts/{DRAFT id}` (JS fetch update) → `existing = postsMapper.getPublished(id)` → null → `existing.setCategoryId()`**NPE → 500** ← **가장 유력**
- `posts-detail.jsp` 직접 forward/include 시 post attribute 누락 → NPE → 500
**권위 격상 전 검증 필요 항목:**
- `PostController.update()` line 206에서 `existing`이 null일 때의 실제 분기 코드 (null 체크가 있는지 여부) — worker 탐색에서 null 체크 코드가 보이지 않아 NPE 유력하나, 해당 라인 전후 코드 직접 확인 권장
---
## 영향 파일 목록
| 파일 | 역할 | 이슈 |
|------|------|------|
| `src/main/java/.../controller/PostController.java` | 메인 Controller | DRAFT 조회 불가, update NPE 후보 |
| `src/main/java/.../controller/PostAdminController.java` | 관리자 Controller | 권한 체크 부재 |
| `src/main/java/.../mapper/PostsMapper.java` | MyBatis Mapper | update SQL category_id 누락 |
| `src/main/java/.../mapper/PostCategoriesMapper.java` | 카테고리 Mapper | 정상 |
| `src/main/java/.../service/PostMarkdownService.java` | 마크다운 렌더링 | 정상 |
| `src/main/java/.../service/OgPreviewService.java` | OG 미리보기 | 정상 |
| `src/main/webapp/WEB-INF/views/posts-detail.jsp` | 상세 JSP | NPE 위험 (null 체크 없는 post 접근) |
| `src/main/webapp/WEB-INF/views/posts-list.jsp` | 목록 JSP | 정상 |
| `src/main/webapp/WEB-INF/views/posts-form.jsp` | 작성/수정 JSP | 정상 |
| `src/main/webapp/WEB-INF/views/admin-post-categories.jsp` | 관리자 JSP | context path 하드코딩 주의 |
---
## 미해결 (open_questions)
1. **`PostController.update()` line 206 전후 null 체크 존재 여부**: `existing`이 null일 때 NPE로 이어지는지, 아니면 별도 분기가 있는지 해당 라인 직접 확인 필요. (현재 탐색에서는 null 체크 코드가 보이지 않아 NPE 유력 추정)
2. **정확한 "포스팅 메뉴 진입" URL**: `/posts` (목록), `/posts/{id}` (상세), `/posts/new` (작성폼), `/admin/post-categories` 중 어느 URL에서 500이 발생하는지 재현 경로 미확인
3. **DB 스키마 상태**: `post_categories` 테이블에 활성(is_active=true) 카테고리가 존재하는지 — 카테고리 없는 상태에서 create/edit 폼의 카테고리 드롭다운이 빈 상태로 표시되고 categoryId=null 제출 → validate() line 305 `categoryId == null` 조건으로 BadRequest 반환 (500 아님)
4. **RbacInterceptor `/admin/post-categories` 보호 여부**: `addExcludePatterns`에서 `/admin/jams/**` 외 추가 제외 경로 상세 확인 필요
5. **애플리케이션 로그 스택트레이스**: 실제 500 발생 시의 예외 클래스와 라인 넘버 — 현재 로그 파일에서 포스팅 관련 스택트레이스 미발견

View File

@ -0,0 +1,62 @@
# ATP Session Report
schema_version: v2
sid: 20260630-143405
started_at: 2026-06-30T14:34:05
ended_at: ~
user_request: "테스트용 계정 하나 더 파고 기록하자. 기존 계정은 일반 사용자용, 하나는 관리자 테스트용"
## Summary
관리자 테스트 계정(admin@bibimbap.local / test1234! / role=ADMIN)을 seed-dev.sql에 추가.
screenshot-guide.md 계정 표를 용도·role 포함 2행으로 확장. commit: 925eb73
## Advisor Invocation Decision Log
```yaml
- advisor: requirements-advisor
decision: skip
rationale: 요청 명확 — 관리자 계정 생성 + 기록, 스펙 불명 없음
checked_at: 2026-06-30T14:34:05
- advisor: research-advisor
decision: skip
rationale: seed-dev.sql + rbac-ddl.sql + screenshot-guide 직접 확인으로 충분
checked_at: 2026-06-30T14:36:00
- advisor: design-advisor
decision: skip
rationale: 파일 영향 맵 확정(2파일), 신규 로직 없음, 마이크로 편집
checked_at: 2026-06-30T14:36:00
- advisor: implementation-advisor
decision: skip
rationale: orchestrator 직접 수행(2파일 이내 마이크로 편집)
checked_at: 2026-06-30T14:36:00
- advisor: verification-advisor
decision: skip
rationale: SQL 변경 없음(DDL 아닌 seed), 코드 변경 0줄 — 문서+데이터 파일만
checked_at: 2026-06-30T14:40:00
- advisor: documentation-advisor
decision: skip
rationale: screenshot-guide.md 직접 수정으로 기록 완료
checked_at: 2026-06-30T14:40:00
```
## Invocations: []
## Decisions: []
## verified_by_me
- L1: skip (src/ 코드 변경 없음 — seed SQL + 문서 파일만)
- 로그 스캔: clean
## needs_user_verification
seed 적용 후 실제 DB에서 로그인 확인:
```
docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev.sql
# 이후 http://localhost:8080/ 에서 admin@bibimbap.local / test1234! 로 로그인
```
## graph_refresh: skip (no-graphify)
## open_items: []
## user_signals:
positive: []
negative: []
ended_at: 2026-06-30T14:41:00

View File

@ -0,0 +1,29 @@
---
phase: documentation
agent: documentation-advisor
agent_version: 1
generated_at: 2026-06-30T17:00:00+09:00
concerns: []
concerns_checked: true
---
# 문서화 보고
## 작성/수정된 문서
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|---|---|---|---|---|
| `docs/changes/2026-06-30-visual-redesign-bibimbap-css.md` | changes | 신규 | `docs/changes/index.md` | `docs/changes/2026-06-29-b2-b4-fe-hardening.md` (선행 FE 작업) |
| `docs/changes/index.md` | changes | 수정(링크 추가) | — | — |
## 의사결정 기록 위치
- 카테고리 판별: 런타임 동작 변경(CSS 신설·JSP 마크업 이식) → `docs/changes/`
- ADR 미생성: 비주얼 레이어 이식은 되돌리기 어려운 기술·아키텍처 결정 수준이 아님 (CSS + 마크업 클래스 추가 수준)
- 세션 보고서: `.atp/work-session/20260630-160000/report.md` (decisions D1~D4, needs_user_verification 6건 포함)
## 추후 문서화가 필요한 항목
- needs_user_verification 결과 확인 후 이상 없으면 별도 문서 불필요. 결함 발견 시 `docs/analysis/` 또는 새 changes 항목으로 기록.
- 다크모드 토큰 전략(bibimbap.css 구조)이 아키텍처 결정으로 고착된다면 `docs/adr/` 추가 검토.
- `NumberFormatException "write"` 선재 버그는 범위 밖 — 별도 세션에서 `docs/analysis/` 기록 권장.

View File

@ -0,0 +1,12 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-30T16:30:00+09:00
---
# 파일 소유권 맵
| 파일 | 담당 worker | worker id | 변경 유형 | 의존 |
|---|---|---|---|---|
| src/main/webapp/WEB-INF/views/profile.jsp | implementation-advisor (직접) | - | modify | - |

View File

@ -0,0 +1,36 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 1
generated_at: 2026-06-30T16:30:00+09:00
concerns: []
concerns_checked: true
workers_spawned: 0
planned_workers: 1
actual_workers: 0
---
# 구현 보고 — 단위 6: 프로필 비주얼 이식
## 변경 목록
| 파일 | worker | 결과 요약 |
|---|---|---|
| src/main/webapp/WEB-INF/views/profile.jsp | advisor 직접 | bibimbap.css 토큰 클래스 7곳 추가, AJAX/scriptlet/id 전부 보존 |
## Bash 단계 (advisor 직접)
- grep 검증 6종 → 전항목 통과
## 설계와의 차이
### planned_workers > actual_workers 전환 사유
파일 1개, 변경 유형 class 추가(기계적 6-8개 edit) → 파일 수 1 + 예상 변경 줄수 < 20 advisor 직접 실행 선택. worker spawn 오버헤드 불필요.
### 디자인 08-profile.jsp 대비 이식/비이식 처리
- **이식**: `.section-eyebrow`(heading eyebrow), `.section-title`(heading title), `.profile-head`(summary wrapper 병행), `.avatar`(avatar div 병행), `.game-row`(game 행 병행), `.meta`(game body 병행), `.card`(games 패널 병행), `.btn .btn-primary`/`.btn .btn-ghost`(버튼/수정 링크 병행)
- **비이식(비노출)**: 공개/비공개 토글 `<form action=".../visibility">` — 엔드포인트 `/games/{id}/visibility` 미존재 확인(grep 무수확), `/profile/edit` — 라우트 미존재 확인, `${user.*}` EL — scriptlet 변수 보존
- **보존 확인**: `submitNickname()`/`uploadAvatar()` 함수, `window.BibimbapCsrf.headers()` 두 곳, `id="profile-avatar-img"`/`id="profile-avatar-initial"`, `getName()`/`getThumbnailUrl()` accessor, `rawThumbUrl.startsWith("/") ? ctx+rawThumbUrl : rawThumbUrl` 경로 처리
## Verification 을 위한 힌트
- acceptance criteria: 보존 grep 6종 전항목 통과(advisor 직접 확인 완료)
- 영향받는 테스트: 프로필 페이지 E2E (profile 렌더/닉네임변경/아바타업로드)
- 비주얼 확인: bibimbap.css 토큰 클래스가 기존 자체 CSS와 충돌 없이 병행 적용되는지 브라우저 확인 필요

View File

@ -0,0 +1,136 @@
---
schema_version: 2
sid: 20260630-160000
resumed_from: null
started_at: 2026-06-30T16:00:00+09:00
ended_at: 2026-06-30T17:20:00+09:00
branch: feat/v2
user_request: |
클로드 디자인 리디자인 산출물(zip)을 비주얼 레이어 방식으로 반영.
공유 src/main/webapp/css/bibimbap.css 신설 + 디자인 클래스/구조 개선을
기존 WEB-INF/views JSP에 입힘. 파일 통째 교체 금지.
하드 제약 6종(CSRF/contextPath/header include/CDN0/game-detail 보존/실모델명).
순서 9단계, 각 단위 커밋 + 빌드/스모크 검증.
---
# Summary
클로드 디자인 리디자인 산출물을 "비주얼 레이어" 방식으로 9단위 반영 완료.
공유 bibimbap.css 신설(다크 오버라이드 포함) + theme-init 단일 link로 전 페이지
적용. 빈상태 파편 2종, 홈/로그인/회원가입/약관/프로필/모집작성에 디자인 클래스·
마크업만 이식하고 모델명·CSRF·AJAX·include·자체 head는 전부 기존값 보존.
game-detail은 이미 동일 토큰 구현 상태라 no-op. errer는 외형만 이식(scriptlet 유지).
하드 제약 6종 전부 준수(디자인 form/EL/common·/ctx·미존재 라우트 미채택).
커밋 8개(단위별) + L2 스모크가 errer JSTL taglib 결함 1건 포착·수정.
# Advisor Invocation Decision Log
- advisor: requirements-advisor
decision: skip
rationale: '핸드오프 문서가 요구·제약·순서를 확정. 오픈질문 없음.'
checked_at: 2026-06-30T16:00:30+09:00
- advisor: graphify-lookup-advisor
decision: skip
rationale: '실제 JSP 파일 직접 읽기가 graph 인덱스보다 정확(모델명/CSRF 근거 필요). research 직행.'
checked_at: 2026-06-30T16:00:30+09:00
- advisor: research-advisor
decision: call
rationale: '9개 페이지 각각 실모델명/CSRF/head구조 + 디자인 산출물 클래스 매핑 추출 필요. parallel-explorer 분산.'
checked_at: 2026-06-30T16:00:30+09:00
# Invocations
- research-advisor (sonnet): 디자인↔실제 페이지별 매핑 spec. 5 parallel-explorer. source_confidence mixed.
- implementation-advisor ×6 (sonnet): 단위1 css / 단위2+3 빈상태+홈 / 단위4 인증 / 단위5+8 약관+errer / 단위6 프로필 / 단위9 모집 / 단위7 game-detail(no-op). 파일소유권 충돌 0.
- verification-advisor (sonnet): L1 WAR 빌드 PASS, css WAR 포함 PASS, JSP 사전컴파일 미설정→런타임 검증 필요 안내.
- graph-refresh-checker (sonnet): partial-stale 판정.
- graphify-update-advisor (sonnet): full scope 메타 갱신.
# Decisions
- D1 (auto): errer.jsp 기존 scriptlet statusCode 읽기 유지. 디자인 errorData EL 미채택 — 외형(SVG/empty-state)만 이식. 근거: 기존 동작 검증됨, errorData 노출 미확인(concern).
- D2 (auto): 미존재 라우트(/games/{id}/visibility, /profile/edit, /password/reset) 요소 비노출. 근거: 백엔드 부재, 비주얼 레이어 범위 밖, 핸드오프 제약.
- D3 (auto): 모든 디자인 form 태그 통째 이식 금지. body 내부 마크업/클래스만 추출, 기존 head/include/AJAX/CSRF 보존.
- D4 (사용자 확인): bibimbap.css 다크모드 토큰 처리.
# user_signals
positive:
- '다크모드/재기동 결정 질문에 한 번에 추천안 수락 — 계획 가시화·결정 게이트가 마찰 없이 통과.'
negative: []
# verified_by_me
- 'L1: WAR 빌드 ./mvnw -q -DskipTests package exit 0 (PASS)'
- 'L1: bibimbap.css WAR 포함 target/.../css/bibimbap.css (PASS)'
- 'L2: 로컬 dev 컨테이너(spring-boot:run) 재기동 후 공개 페이지 JSP 컴파일/렌더 스모크 — /, /login, /signup, /terms, /posts, /recruit, /error 전부 200'
- 'L2: 마커 검증 — home css link+hero, login pw-toggle, signup pw-meter, terms toc, error empty-state 전부 서빙 확인'
- 'L2 결함 포착·수정: errer.jsp stray javax JSTL taglib → /error 500 → taglib 제거 → 200 (fix 커밋 360078a). 회귀: 동일 advisor 산출 sibling(terms)는 200 정상.'
- '로그 스캔: 재기동 후 신규 JasperException 0건 (07:35:52 잔존분은 재기동 전)'
# browser_review (후속 — 사용자 요청 "브라우저 실제 적용본 검토")
- 검토 페이지(8): 홈(다크+라이트 토글 양쪽 정합), /login(pw-toggle·비번찾기 미노출 확인), /signup(강도막대·약관체크), /terms(sticky TOC+조항본문), /posts·/recruit(빈상태), /error(empty-state+버튼), /game/3.
- game-detail 실동작 확인: WebGL 16:9 플레이어 로드, 6축 레이더 SVG(몰입성/창의성/조작성/완성도/사운드/비주얼), 리뷰 AJAX 목록·평점 3.8(6)·로그인 게이트·정렬칩 — WebGL/6축/리뷰 보존 확정(unit7 no-op 정당).
- **결함 2건째 포착·수정**: posts-empty/recruit-empty.jspf 정적 include 인코딩 미상속 → 빈상태 한글 mojibake. 각 파편 pageEncoding=UTF-8 추가로 해소(fix a4d163d), 브라우저 재확인 정상. 교훈 docs 반영(d379a1c).
- L1 재검: docker 컨테이너(temurin-21) 내 mvn package PASS(호스트 Java 미설치라 컨테이너 빌드).
# needs_user_verification
- '로그인 화면 인터랙티브 제출: 실제 email/password 로그인 AJAX(_csrf + BibimbapCsrf.headers 이중방어) 성공 이동, 회원가입 제출→modal→/login. (마크업 렌더·제출코드 무손실 grep 확인했으나 브라우저 세션 제출 1회 권장. 테스트계정 admin@bibimbap.local)'
- '프로필: 닉네임/아바타 변경 AJAX 동작(로그인 세션 필요).'
- '게임상세: WebGL 플레이·6축 리뷰 작성/수정/삭제·덧글 CRUD (변경 없음=no-op이나 회귀 확인 권장).'
- '모집작성: /recruit/new 제출 성공 + 라이브 미리보기(로그인 세션 필요).'
- '다크모드 토글 시 body 컴포넌트 ↔ header/footer 색 정합 육안 확인.'
- '참고(범위 밖·선재): 로그 NumberFormatException "write" — 숫자 @RequestParam에 비숫자 유입. 본 작업(비주얼·컨트롤러 무수정)과 무관, 별도 추적 권장.'
# graph_refresh
partial-stale → graphify-update-advisor가 full scope 메타 갱신(docs/graph/index.md, source_commit 360078a). 신규 노드 6건(뷰 fragment 2 + css + docs 3) + include 엣지 2건 반영. 커밋 8c86e8c. (코어 의존성 구조는 무변경.)
# Retrospective
```yaml
Retrospective:
signals:
positive:
- quote_or_paraphrase: "다크모드/재기동 결정 질문에 한 번에 추천안 수락"
about: 결정 게이트(D4 다크모드 토큰 처리)를 계획 가시화 후 질문 → 1라운드 수락. 마찰 0.
negative: []
what_went_well:
- "research 단계에서 핸드오프 매핑 spec(디자인↔실제 모델명/CSRF/라우트)을 선행 추출해 구현 6단위 전반의 불일치 차단 — 하드 제약 6종 전부 준수."
- "L2 스모크(spring-boot:run 재기동 후 공개 페이지 전수)가 WAR 빌드 PASS에도 숨은 errer.jsp JSTL taglib 결함을 포착·fix 커밋까지 완결."
- "단위별 커밋 8개 — 롤백 경계 명확, 결함 fix 커밋(360078a)이 구분 가능."
what_to_improve:
- "JSP 사전컴파일 미설정 → WAR 빌드 PASS가 JSP EL/taglib 런타임 오류를 은닉. 비주얼 레이어 작업에서 디자인 산출물의 JSTL taglib이 stray로 유입되는 패턴이 실제 발생했으므로, 'JSP 변경은 빌드만으로 불충분 — 런타임 스모크 의무' 규약이 없다."
memory_candidates:
- name: jsp-runtime-smoke-gate
type: feedback
description: "JSP 변경(비주얼 레이어 포함)은 WAR 빌드 PASS만으로 충분하지 않음 — 런타임 스모크를 의무 검증 레벨로 명시해야 함."
body_draft: |
### JSP 변경은 빌드 PASS만으로 불충분 — 런타임 스모크 의무
이 프로젝트는 JSP 사전컴파일이 미설정(Tomcat이 런타임에 컴파일)되어 있다.
결과적으로 `./mvnw package`가 exit 0이어도 JSP EL 표현식·taglib 선언 오류는
런타임에만 발현한다. 비주얼 레이어 작업에서 디자인 산출물의 stray JSTL taglib이
유입되는 패턴이 실증됐다(errer.jsp → /error 500, fix: 360078a).
**Why**: WAR 빌드는 JSP를 파일로만 패키징하고 컴파일하지 않는다. L1 빌드 GREEN은
"JSP 구문이 유효하다"는 보증이 아니다.
**How to apply**:
- JSP 파일이 변경된 모든 세션에서 verification 단계에 "런타임 스모크" 항목을
명시(spring-boot:run 재기동 후 변경된 JSP 경로 200 확인).
- 비주얼 레이어 작업은 디자인 산출물에서 stray taglib(`<%@ taglib ...%>`)이
유입되었는지 grep 전수 확인을 implementation 체크리스트에 포함한다:
`rg '<%@\s*taglib' src/main/webapp/WEB-INF/views/`
- 사전컴파일 도입(maven-jasper-plugin)은 별도 open item으로 관리한다.
> 근거: 세션 20260630-160000 — errer.jsp javax.servlet.jsp.jstl taglib stray 유입
> → /error 500 런타임 발현. L2 스모크에서 포착·수정.
rationale_for_saving: "재현성 있음 — JSP 파일이 변경되는 모든 세션에서 동일 위험. verification-strategies.md에 동일 항목 없음."
signal_source: observation
docs_sync_target: "/Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md"
memory_optional: true
protocol_feedback: []
applied_changes:
- "jsp-runtime-smoke-gate 교훈을 verification-strategies.md에 신규 §추가 (커밋 8d52b10). docs-first 단독 마감 — memory_optional이므로 memory 강제 안 함."
```
# open_items
- '.atp/work-session/* 다수 미추적(이번 20260630-160000 포함, 선재 세션들도). 소스 변경은 전부 커밋됨. 정책상 work-session 추적 기본이나 본 세션 산출 외 선재분은 범위 밖 — 추적 여부 사용자 판단.'

View File

@ -0,0 +1,180 @@
---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-30T16:00:00+09:00
concerns:
- "low source confidence — errer.jsp의 error-page 매핑(web.xml/ErrorConfig) 및 pageContext.errorData 노출 가능 여부 미확인. error/error.jsp의 ${pageContext.errorData.statusCode} 바인딩이 현 Tomcat/Spring 구성에서 동작하는지 검증 전 권위 데이터(시드·계약)로 승격 금지."
- "디자인 산출물 다수가 include 경로 '/WEB-INF/views/common/header.jsp'를 참조하나 현 프로젝트는 'common/' 서브디렉터리가 없음(header.jsp는 /WEB-INF/views/ 직하). 이식 시 경로 전부 교정 필요 — 디자인 마크업을 '있는 그대로' 복사 금지."
concerns_checked: true
source_confidence: mixed
workers_spawned: 5
---
# 조사 결과 — 비주얼 레이어 이식 mapping-spec
## 주제
디자인 산출물(외부 클로드 디자인 툴 결과물)을 기존 JSP에 "CSS 클래스/마크업 구조만" 이식하기 위한 페이지별 (a)모델 속성명 (b)CSRF 패턴 (c)head/include 구조 (d)제출 방식 (e)안전 이식 가능 블록 추출.
근거: 각 항목 file:line 인용은 parallel-explorer 5개(G1~G5)가 전문 읽기로 확인. 라우트/필드 바인딩은 advisor가 controller 직접 grep으로 교차검증.
---
## 공유 인프라 (G1) — 확인됨
### theme-init.jsp (32줄)
- 독립 `<head>` 조각. `<link>`/`<style>` 없음. 포함: `<meta name="csrf-token">`(line 7) + `<script>`(테마 FOUC 방지 + `window.BibimbapCsrf` 전역 등록, line 19-30).
- **`window.BibimbapCsrf`는 여기서 정의됨** — 모든 AJAX CSRF 헤더의 출처. (확인됨)
- views 디렉터리 27개 JSP 중 공유조각 4개를 제외한 **23개 전원이 theme-init.jsp를 include**, 미포함 0건. (확인됨, grep)
### header.jsp (250줄)
- 자체 `<style>`(14-194). header 전용 CSS 변수는 `.site-header` 스코프에서만 선언. BEM 클래스(`.site-header__*`) 일관. (확인됨)
- contextPath = `${pageContext.request.contextPath}` (line 197). **`${ctx}` 별칭 정의 없음.** (확인됨)
- nav = `<nav class="site-header__nav">` + 플랫 `<a class="site-header__nav-link">`, ul 없음. (line 201-203)
- `window.BibimbapCsrf` 정의 없음(theme-init 소유). modal.jsp include(line 231).
### footer.jsp (96줄)
- 자체 `<style>`(2-77), script 없음. `.site-footer__*` 클래스. 링크색 `#e8a54b` 하드코딩(line 43) — bibimbap.css `--color-accent: #D4A853`와 불일치(토큰 정합성 관찰). (확인됨)
### bibimbap.css (352줄) — 디자인 산출물
- 상단 주석(1-5): "외부 라이브러리 없음·순수 CSS". **@import / @font-face / CDN url() 없음.** 폰트는 로컬 `'Pretendard Variable'` 의존. (확인됨)
- `:root` 변수 24개(8-41): `--color-*`, `--radius-*`, `--font-sans/mono`. **다크 모드 오버라이드 블록 없음(라이트 고정 토큰).** (확인됨)
- 레이아웃 클래스: `.page .btn .btn-primary .field .input .textarea .card .empty-state .game-grid .game-card .profile-head .avatar .terms .terms-toc .terms-body .form-grid .form-actions` 등.
- **`.site-header*`/`.site-footer*`를 정의하지 않음 → header/footer와 직접 클래스 충돌 없음.** (확인됨)
- 주석 line 4 예시는 `${ctx}/css/bibimbap.css` 사용 — 현 코드베이스 패턴(`${pageContext.request.contextPath}`)과 다름. link 작성 시 교정 필요.
---
## G2 홈 + 빈상태
### index.jsp (기존)
- 게임 모델: `GameData` — accessor `getName()`/`getCreator()`/`getThumbnailUrl()` (확인됨, GameData.java:40/48/80). **→ EL은 `${game.name}` `${game.creator}` (디자인의 `${game.title}`/`g.author`는 불일치).**
- 자체 head 보유 + theme-init/header/footer include.
- 게임 등록 라우트 = `/game/new` **단수** (확인됨, WebMvcController.java:62 / GameController.java:67). 디자인의 `/games/new` 복수는 불일치.
### recruit-list.jsp / posts-list.jsp (기존)
- `/recruit/new` (RecruitController.java:41/49) · `/posts/new` (PostController.java:99) **둘 다 실재 확인됨.** 디자인 CTA href는 이 값으로 교정.
- 빈 상태 분기 위치에 디자인 fragments(recruit-empty.jspf, posts-empty.jspf) include 가능 — 단, fragments는 디자인 클래스(`.empty-state`)를 쓰므로 bibimbap.css 도입이 선행돼야 시각 정상.
### 01-home.jsp + fragments 3종 (디자인)
- **이식 가능**: 히어로 섹션, 정렬칩, `.game-card`/`.game-grid` 외형 클래스, `.empty-state` fragment 마크업.
- **이식 금지/교정**: `${game.title}`→`${game.name}`, `${game.author}`→`${game.creator}`, `/games/new`→`/game/new`. 모델 바인딩 이름을 디자인 그대로 쓰면 빈값 렌더.
---
## G3 인증 (login / signup)
### login.jsp (기존) — 확인됨
- 제출: form action="…/login" 선언되나 submit 인터셉트 후 **AJAX fetch POST /login** (line 272-273), 성공 시 `location.href=ctx+'/'`.
- CSRF **이중 방어**: hidden `<input name="_csrf">`(line 224) + `window.BibimbapCsrf.headers()`(line 286-294).
- 필드 name: `email`(227), `password`(231), `remember`(235).
- 에러 표시: 인라인 DOM 슬롯 없음 → `window.BibimbapModal.alert()` 호출.
- 자체 head 보유(4-6) + theme-init(9)/header(208).
### signup.jsp (기존) — 확인됨
- 제출: **AJAX fetch POST /signup**, 성공 시 BibimbapModal.alert 후 /login 이동.
- CSRF 이중 방어 동일(hidden `_csrf` line 222 + headers line 300-306).
- 필드 name: `displayName`(227), `email`(231), `password`(236), `passwordConfirm`(240), `termsAccepted`(244), `provider`(hidden 223), `providerUserId`(hidden 224, submit 직전 email 복사 line 293).
- **클라이언트 비밀번호 일치 검증 JS 없음**(checkValidity만, line 285-293).
### 02-login.jsp / 03-signup.jsp (디자인)
- 자체 head **없음**(fragment). include가 `common/header.jsp`(불일치 경로).
- **CSRF hidden input 전무 + submit 인터셉트 JS 전무** → native POST. **form 태그 통째 이식 금지** (CSRF 누락·AJAX 로직 상실).
- 약관 체크박스 name=`agree`(03-signup line 55) — 서버 기대값 `termsAccepted`와 불일치 → 그대로 이식 시 약관 동의 유실.
- `providerUserId`/`provider` hidden 없음 → 기존 signup 필수 hidden 누락.
- **이식 가능(비주얼 전용)**: `.pw-field`+`.pw-toggle` 마크업·인라인 onclick(02-login 29-33), `.pw-meter` data-level 강도막대+oninput(03-signup 34-36), `#pw-ok` 일치표시+oninput(43-45, id를 기존 필드 id에 맞춰 교정), `.form-error`/`.form-ok`/`.help` 슬롯.
- **이식 금지**: form 태그/action/method, common/ include, `name=agree`, native POST.
- `/password/reset` 링크(02-login line 27): 기존 login.jsp에 없음. **라우트 미존재(grep 무수확) → 링크 비노출 권고.** (확인됨: 컨트롤러에 매핑 없음)
---
## G4 약관 + 프로필
### terms.jsp (기존) — 확인됨
- 조항 본문 **전량 하드코딩**(모델 바인딩 없음). `<article class="policy-doc"> > <section class="policy-section">` + `<h2>`+`<ol><li>` (99-191).
- 자체 head 보유(4-94, 인라인 style 포함) + theme-init(7)/header(97)/footer(193).
### 07-terms.jsp (디자인)
- sticky 목차 `<nav class="terms-toc">`(17-29, 신규 블록), 조항 `<div class="terms-body"> <section id="aN"> <h3><p><br>`(32-94). 자체 head 없음, common/ include.
- **이식 가능**: `.terms`/`.terms-toc`/`.terms-body` 래퍼·TOC(신규 추가). **마크업 변환 필요**: `<section class="policy-section">+<ol><li>``<section id="aN">+<p>`, `<h2>`→`<h3>`.
- **이식 금지/교정**: 자체 head 미보유분은 기존 head 유지, common/ 경로 교정.
### profile.jsp (기존) — 확인됨
- 모델: session attr `displayName`/`email`/`avatarUrl`(17-19), request attr `myGames`(27-28)/`myBadges`(31-36). **scriptlet 방식 — 디자인의 EL `${user}` 객체와 다름.**
- CSRF: hidden input 없음, **`window.BibimbapCsrf.headers()` 헤더 주입만**(634-645, 699-708).
- AJAX 함수: `submitNickname()`(619, POST /profile/nickname), `uploadAvatar()`(692, POST /profile/avatar).
- 썸네일 경로: `rawThumbUrl.startsWith("/") ? ctx+rawThumbUrl : rawThumbUrl`(491-493).
- **공개/비공개 토글 UI·API가 기존엔 전무.** (확인됨)
### 08-profile.jsp (디자인)
- `.profile-head` 헤더(17-24), `.game-row`+`.thumb`+`.meta`(39-58), 공개토글 `<form action="${ctx}/games/${g.id}/visibility"> <label class="switch"><input type=checkbox onchange="this.form.submit()">`(46-53).
- **공개토글 엔드포인트 `/games/{id}/visibility` 현 컨트롤러에 미존재**(grep 무수확; 유사한 `/admin/jams/{jamId}/visibility`만 존재 — 동명 경로이나 별개 엔티티/권한, 혼동 금지). 이 토글은 **백엔드 신설 없이는 비주얼만 이식 불가** → 비주얼 레이어 범위 밖. (확인됨)
- `/profile/edit` 링크(08-profile line 23): 기존 인라인 편집(닉네임/아바타)만 있고 라우트 미존재(grep 무수확). 비노출 또는 백엔드 결정 필요.
- **이식 가능**: `.profile-head`·`.game-row` 외형(단 EL `${user.*}`→scriptlet 변수 교정, `${myGames}` 게임 필드는 `getName()`/`getThumbnailUrl()` 사용).
- **이식 금지(보존 필수)**: `submitNickname()`/`uploadAvatar()` fetch + `window.BibimbapCsrf` 헤더, `id="profile-avatar-img"`/`profile-avatar-initial` (JS 참조), `/games/.../visibility` 토글(백엔드 부재).
---
## G5 게임상세 + 모집 + 에러
### game-detail.jsp (기존, 2385줄) — 블록 매핑 (확인됨)
| 블록 | line | 보존필수 |
|---|---|---|
| scriptlet 모델바인딩(XSS escape) | 1-22 | 필수 |
| head + 인라인 CSS(토큰 40여개) | 23-1203 | CSS 교체가능 |
| body+header include | 1204-1205 | 이식가능 |
| topbar(뒤로/owner액션) | 1206-1218 | 이식가능(클래스만) |
| 메타카드(제목/번호/제작자/좋아요) | 1220-1256 | 이식가능(id 유지) |
| **WebGL iframe** | 1258-1276 | **필수(sandbox/allow/scriptlet src 불변)** |
| 제작자 한마디 패널 | 1279-1296 | 이식가능 |
| **리뷰 패널 마크업** | 1298-1347 | **필수(id 20여개 JS참조)** |
| **덧글 패널 마크업** | 1349-1381 | **필수(id JS참조)** |
| **IIFE 스크립트(CRUD/CSRF/6축레이더/별점)** | 1387-2382 | **필수 전체보존** |
- CSRF: `baseHeaders()`/`formHeaders()` 공통 헬퍼가 `window.BibimbapCsrf.headers()` 중앙화(1562-1569).
- 리뷰 CRUD: `reviewsUrl = ctx+'/game/'+gid+'/reviews'`(2021), GET/POST/PUT/DELETE(2306/2375/2236). 덧글: `commentsUrl=…/comments`(1800), POST/PUT/DELETE(1977/1845/1901).
- 6축: `AXIS_KEYS=[immersion,creativity,controls,completeness,sound,visual]`(1508), `buildHexRadar()`(1607-1702) SVG 동적생성, `buildStarRadioGroup()`(1731-1786).
- 보존필수 id 목록: `game-review-composer/form/axes/stars/list`, `game-reviews-summary-panel/radar/empty/toolbar/more`, `game-comment-form/input/list/composer/login-gate`, `game-comments-toolbar/more`, `game-like-btn/count`.
### 06-game-detail.jsp (디자인)
- 자체 head 없음, EL `${game.*}`, WebGL iframe **sandbox 속성 없음**(보안 후퇴 — 절대 이 형태로 교체 금지), 6축은 `<div class="help">6축 평가 포함</div>` 플레이스홀더(빌더 없음), 리뷰는 `<c:forEach>` 서버렌더(기존 AJAX 아님), 좋아요는 native form POST.
- **결론: game-detail은 통째 교체 절대 금지. 표층 CSS 클래스 토큰 교체와 topbar/메타카드/제작자패널 외형만 선별 이식. 디자인의 WebGL·6축·리뷰 구현은 모두 기능 후퇴이므로 버린다.**
### recruit-form.jsp (기존, 450줄) — 확인됨
- 제출: `action="…/recruit/new"` novalidate + **AJAX fetch POST**(376-395), `window.BibimbapCsrf.headers()`.
- 필드 name(controller @RequestParam로 교차확인): `projectName`(260) `genre`(264) `summary`(268) `role`(272) `status`(280) `type`(289) `period`(298) `team`(302) `contact`(306) `description`(310). (RecruitController.java:52-61에서 동일 name 수신 확인됨)
- 글자수 카운터 JS 없음(maxlength만). 미리보기 `render()` IIFE(338-446).
### 10-recruit-form.jsp (디자인)
- native form POST `action="${ctx}/recruit"`(line 18, **CSRF 없음·엔드포인트 불일치 — 기존은 /recruit/new**), 필수표시 `<span class="req">*</span>`, 글자수카운터 summary만 인라인 oninput(33-35), 필드 `name="title"`(22)는 서버 기대 `projectName`**불일치**(확인됨).
- **이식 가능**: `.req` 필수표시, `.count`/`#sc` 글자수 마크업+인라인 oninput(독립적), `.form-page`/`.field` 외형.
- **이식 금지**: form action/method(엔드포인트 /recruit ≠ /recruit/new, CSRF 누락), `name="title"`(→projectName 유지), AJAX submit·미리보기 render() 보존.
### errer.jsp (기존, 232줄) — 확인됨 / error/error.jsp (디자인) — 미확인 부분 존재
- errer.jsp: `isErrorPage=true`, **`jakarta.servlet.error.status_code` scriptlet**(190)으로 코드 읽음, `${statusCode}`/`${status}` EL 미사용. statusCode!=null 단일 체크(203-208), 코드별 분기 없음. requestUri/message 상세 블록 보유.
- error/error.jsp(디자인): `${pageContext.errorData.statusCode}`→`<c:set var=code>`(19) + `<c:choose>` 403/otherwise(404) 분기(27/44). 상세정보 블록 없음.
- **미확인**: web.xml/ErrorConfig의 error-page 매핑 위치(grep 무수확), `pageContext.errorData`가 현 구성에서 노출되는지. → 디자인 EL 바인딩 채택 전 검증 필요. concern 등록함.
- **이식 가능**: SVG 아이콘/버튼/`<c:choose>` 분기 외형. **주의**: 코드 바인딩 방식 변경(scriptlet→errorData EL)은 동작 검증 후. 상세정보(requestUri/message) 노출은 정책 결정 사항.
---
## 종합 판단
### bibimbap.css 도입 지점 결론 (단일 지점 추천)
**theme-init.jsp에 `<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bibimbap.css">` 1줄 추가가 유일한 단일 지점이다.** 23개 전 페이지가 예외 없이 theme-init을 include하므로 전역 적용된다(확인됨). header/footer는 include 체인 중간이라 단일 지점 불가. CSS 자산은 `src/main/webapp/css/`에 두면 Tomcat DefaultServlet이 서빙(CLAUDE.md 정책 부합). 단 디자인 주석의 `${ctx}` 별칭은 현 코드에 없으므로 `${pageContext.request.contextPath}`로 작성.
### CSS 클래스 네임스페이스 충돌 요약
- bibimbap.css는 `.site-header*`/`.site-footer*`를 정의하지 않음 → **header/footer 인라인 스타일과 클래스 직접 충돌 없음.**
- 잠재 충돌: **다크 모드 이중 체계.** header/footer는 `html[data-theme="dark"] .site-*` 자체 변수, bibimbap.css `:root`는 다크 오버라이드 없는 라이트 고정 토큰. 도입 후 body 컴포넌트(라이트 고정) vs header/footer(다크 대응)가 공존 → 다크 모드에서 body 영역이 라이트로 남는 시각 불일치 가능. (확인됨, 디자인 의도 확인 필요)
- 색상 토큰 불일치: footer `#e8a54b` vs bibimbap.css `--color-accent #D4A853`.
### 횡단 패턴 (확인됨)
1. **모든 디자인 산출물이 자체 head 미보유 + `common/` include 경로 사용** → 기존 자체-head 구조에 "그대로" 붙일 수 없음. body 내부 마크업만 추출해 기존 head/include 유지한 페이지에 이식.
2. **모든 디자인 form이 CSRF hidden·AJAX 로직 미보유(native POST)** → 어떤 form 태그도 통째 이식 금지. 제출 로직(AJAX+BibimbapCsrf)은 전 페이지 보존 필수.
3. **모델 바인딩 이름 불일치 다발**: 디자인 EL(`game.title`/`author`/`user`/`/games/new`/`/recruit`/`name=title`/`name=agree`) vs 기존(`name`/`creator`/scriptlet session/`/game/new`/`/recruit/new`/`projectName`/`termsAccepted`). 이름 그대로 이식 시 빈 렌더·필드 유실.
## 미해결
- errer.jsp error-page 매핑 위치 및 `pageContext.errorData` 노출 가능 여부 (web.xml/Java config 미확인 — concern 등록). 디자인 error EL 채택 전 검증 필요.
- `/games/{id}/visibility`(프로필 공개토글), `/profile/edit`, `/password/reset` 라우트 부재(grep 확인) → 디자인이 제안하나 백엔드 신설 없이는 비주얼 이식 무의미. 비주얼 레이어 범위 밖 — 노출 정책은 design/orchestrator 결정 사항.
- bibimbap.css 도입 후 다크 모드 body 토큰 처리(라이트 고정) — 디자인 의도 확인 필요(조사 범위 밖, 설계 결정).

View File

@ -0,0 +1,67 @@
---
schema_version: 1
sid: 20260630-170024
user_request: "/frontend-design 디자인 변경 및 추가했는데 검토해줘 — feat(redesign) 커밋 시리즈 검토 (read-only)"
started_at: 2026-06-30T17:00:24
mode: review-only (코드 변경 없음, orchestrator 직접 수행)
---
# Advisor Invocation Decision Log
- advisor: requirements-advisor
decision: skip
rationale: '요청 명확(=리디자인 커밋 검토). 빌드할 신규 기능 없음.'
checked_at: 2026-06-30T17:01
- advisor: design/implementation-advisor
decision: skip
rationale: 'review-only. 코드 작성 0. 빌드 체인 불필요.'
checked_at: 2026-06-30T17:01
- advisor: verification/graphify/retrospective
decision: skip
rationale: '코드 변경 0줄 → graph-refresh-checker 트리거 미충족. 검증 대상 산출물 없음.'
checked_at: 2026-06-30T17:30
# Summary
feat(redesign) 시리즈(0390da6~a4d163d) 검토. 13개 src 파일 +807/-223.
공유 bibimbap.css(384L) 신설 + JSP 9단위 비주얼 레이어 이식.
보안/인코딩/CSRF/CDN 하드제약은 준수. 핵심 문제는 **디자인 일관성** — 공유 토큰 시스템과 기존 페이지-로컬 토큰 시스템 2종이 병존.
# Findings (severity desc)
- HIGH(coherence): bibimbap.css `--color-accent:#D4A853` ≠ 앱 전역 브랜드 골드 `#e8a54b`(header/footer/modal/26파일). 모든 페이지에 골드 2종 공존. 다크 override는 `--color-accent-strong:#e8a54b`만 일치시켜 모순 노출. unit-1 "공유 css 전 페이지 공통" 목표와 상충.
- MED: 클래스 합집합 override 모호 — profile.jsp `class="profile-button btn btn-primary"` 등. bibimbap `.btn-primary`(외부 link) vs 페이지-로컬 `.profile-button--primary`(`<style>`) 동일 specificity, source order(페이지 style 후행)로 로컬 승 → 추가한 btn 클래스 일부 inert/예측불가 혼합.
- MED: recruit-form.jsp 가 공유 시스템 미채택 — 전체 인라인 `<style>` 병렬 토큰(`--accent:#e8a54b` 등) + bibimbap 클래스명 shadow(.field/.form-grid/.req/.count/.preview-card/.form-actions). 가장 이질적.
- LOW: index.jsp bibimbap.css 이중 로드(직접 `<link>` + theme-init include).
- LOW: 인라인 핸들러(login onclick / signup·summary oninput) — CSP 비친화 + recruit-form addEventListener 패턴과 혼재.
- LOW: 전역 `:focus-visible` 링 없음(링크/chip/버튼). input 은 focus ring 보유 ✓.
- LOW: Pretendard/JetBrains Mono 폰트스택만 — @font-face 없음. 미설치 환경 silent fallback → 디자인 의도 display 개성 소실(단 CDN-0 제약상 트레이드오프).
# Positives
- XSS 안전: recruit-form 미리보기 `textContent` ✓, index `HtmlUtils.htmlEscape`(thumbUrl/jamTitle/searchQuery) ✓, 신규 raw user-data EL 출력 0.
- CSRF 보존 ✓ (`BibimbapCsrf.headers` + meta csrf-token).
- CDN 0 ✓. 빈상태 fragment 깔끔 + pageEncoding 수정 완료 ✓.
- 다크 토큰 header.jsp 출처 명시 ✓. 반응형 breakpoint 존재 ✓.
- 커스텀 empty-state SVG = 가장 distinctive 한 시그니처 요소.
# verified_by_me
- 검토 단계: 13파일 diff + bibimbap.css + 2 fragment + theme-init 전수 read.
- 구현 후 L1: `./mvnw -DskipTests package` exit 0 PASS. WAR 내 css/bibimbap.css 포함 확인.
- 구현 후 L2: dev 컨테이너 소스 bind-mount(`.:/build`)로 변경 라이브. 스모크
`/` `/login` `/signup` `/terms` `/recruit` `/posts` `/error` HTTP 200 + JSP 컴파일 에러 0.
서빙 `/css/bibimbap.css` accent `#e8a54b` 확인.
- 로그 스캔: JSP 예외 마커 0건 clean.
# needs_user_verification
- `/recruit/new`(302, 로그인 필요): 인증 세션에서 rf- rename 후 폼 레이아웃·라이브 미리보기 정상 렌더.
- `/profile`(302, 로그인 필요): 인증 세션에서 버튼/카드 외형(중복 클래스 제거 후) + 골드 정합.
- 다크모드 토글 시 전 페이지 골드 단일(#e8a54b) 육안 정합.
# open_items
- `.atp/work-session/20260630-170024/report.md`(본 파일) 미커밋 — 기존 work-session 디렉토리들도 untracked 상태와 일관.
# graph_refresh
- partial-stale (구조 시그널 0 — css 토큰 스냅샷만 약 stale). graph-refresh-checker 권고대로
단독 재생성 생략(토폴로지 무변경, 비용 부당), 다음 구조변경 사이클에 흡수.
# commit
- cf1e7ce fix(redesign): 골드 토큰 통일·클래스 충돌 정리 (4 src + 2 docs)
ended_at: 2026-06-30T17:45

View File

@ -1,7 +1,7 @@
---
kind: graphify-meta
last_generated_at: 2026-06-30T17:00:00+0900
source_commit: 360078a
last_generated_at: 2026-06-30T18:20:00+0900
source_commit: 20789a2
scopes:
- full
---
@ -39,7 +39,7 @@ scope 예시: `full`, `src`, `docs`, `src-features` 등.
| scope | 마지막 생성 | 소스 커밋 | 대상 경로 | 요약 |
| --- | --- | --- | --- | --- |
| `full` | 2026-06-30 | `360078a` | `src/` + `docs/` | **src+docs 통합** (재생성 요청 중). 신규 노드 6건(posts-empty.jspf, recruit-empty.jspf, bibimbap.css, screenshot-guide.md, screenshot-guide.py, 2026-06-30-screenshot-guide-auth.md) + include 엣지 2건(posts-list.jsp→posts-empty.jspf, recruit-list.jsp→recruit-empty.jspf) 반영. Java/컨트롤러/라우트/모델/스키마 무변경. |
| `full` | 2026-06-30 | `20789a2` | `src/` + `docs/` | **src+docs 통합** — 클린 풀 재생성(2089 노드 / 4719 엣지 / 128 커뮤니티). 직전 미반영 graphify 의 고스트 중복(452 exact + 244 fuzzy) 제거 + 누적 docs/changes 노드(리디자인·스크린샷·UI 다축평가 사례) + db/*-ddl SQL 스키마 노드 반영. AST 1853(코드) + 시맨틱 236(docs/SQL). JSP/CSS 정적자산은 스캔 범위 외(§285). |
## 갱신 시 체크리스트