---
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 토큰 처리(라이트 고정) — 디자인 의도 확인 필요(조사 범위 밖, 설계 결정).
