diff --git a/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md b/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md
new file mode 100644
index 0000000..c25f881
--- /dev/null
+++ b/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md
@@ -0,0 +1,542 @@
+---
+phase: design
+agent: design-advisor
+agent_version: 1
+generated_at: 2026-06-30T03:15:00Z
+concerns:
+ - "admin-console.jsp, admin-jam-list.jsp JS 내부의 post() 함수 구현 세부가 admin-post-categories / admin-unity-feeds와 정확히 동일한지 코드 레벨 비교 필요. 통합 시 파라미터 처리 분기가 예상치 않게 동작 변경될 수 있음. 구현 advisor가 통합 전 3-way diff 수행 권고."
+ - "header.jsp 내 .site-header CSS 변수 블록(--header-bg 등)은 global.css 의 토큰 범위와 겹치지 않는 컴포넌트 전용 변수이므로 중앙화 대상에서 제외. 구현 시 헷갈리지 않도록 명칭 분리 유지 확인 요망."
+concerns_checked: true
+references:
+ requirements: "(인라인 입력 — 이 문서의 ## 맥락 섹션)"
+ research: ".atp/work-session/20260630-113258/research/frontend-analysis.md"
+ adrs: []
+---
+
+# 프론트엔드 컴포넌트 개선 권고안
+
+## 목표 / 비목표
+
+### 목표
+- FR-1: CSS 디자인 토큰 중복 제거 → 23개 파일 대신 단일 파일 변경으로 전역 반영
+- FR-2: JS 공용 유틸리티(notify/openModal/post/handleResult/handleError) 중복 선언 제거
+- FR-3: BibimbapModal 호출 패턴을 단일 방식으로 표준화
+- FR-4: recruit-form의 CSRF 토큰 폴백 갭 수정 (보안)
+- FR-5: 날짜 포맷 함수를 공용 파일로 추출하여 복사 중복 방지
+
+### 비목표
+- Webpack/Vite 등 빌드 도구 도입 (별도 의사결정 사항)
+- CSS-in-JS, PostCSS 파이프라인 도입
+- JSP 템플릿 엔진 교체 또는 React/Vue 마이그레이션
+- 기존 HTML/JSP 구조 및 클래스명 리팩터링 (CSS 토큰만 외부화)
+- header.jsp·footer.jsp·modal.jsp 기존 구현 변경
+- 서버사이드 날짜 포매팅 도입
+
+---
+
+## 요약 (우선순위 매트릭스)
+
+| 이슈 | 영향도 | 구현 비용 | 우선순위 |
+|---|---|---|---|
+| CSS 토큰 23개 파일 중복 | 높음 (유지보수 23배 비용) | 낮음 (파일 추가 + include 삽입) | P0 |
+| 폼 제출 CSRF 갭 (recruit-form) | 높음 (보안) | 낮음 (10줄 이내 수정) | P0 — 보안 |
+| JS 공용 유틸리티 분리 | 중간 (동작 불일치 방지) | 중간 (파일 작성 + 기존 인라인 제거) | P1 |
+| 모달 패턴 표준화 | 중간 (신규 페이지 기준 제공) | 중간 (패턴 A/B 대체 + admin 2곳 수정) | P1 |
+| 날짜 포맷 유틸리티 모듈 | 낮음 (예방적 분리) | 낮음 (파일 추출) | P2 |
+
+---
+
+## 권고 1 — CSS 토큰 중앙화 (P0)
+
+### 접근
+
+`src/main/webapp/css/global.css` 정적 파일을 신규 생성한다. 이 파일에 라이트/다크 테마 CSS 변수 블록, `body` 기본 스타일, `safe-area` padding 믹스인 클래스, `.admin-btn` 컴포넌트를 선언한다.
+
+각 JSP의 `
` 최상단에 `` 를 삽입한다. 이 link는 `theme-init.jsp` `` 직후에 위치시켜 FOUC(Flash of Unstyled Content)를 방지한다.
+
+Spring Boot WAR에서 `src/main/webapp/` 하위 정적 파일은 Servlet 컨테이너(Tomcat)가 직접 서빙한다. `UploadResourceConfig`의 `addResourceHandlers`는 `/profile/**` 전용이므로 `/css/**` 는 별도 등록 없이 Tomcat 기본 DefaultServlet이 처리한다. `src/main/resources/static/` 경로는 필요하지 않으며 신규 생성도 불필요하다.
+
+### 파일 경로
+
+| 역할 | 경로 |
+|---|---|
+| 신규 — 전역 CSS | `src/main/webapp/css/global.css` |
+| 수정 대상 — 인라인 토큰 제거 | `src/main/webapp/WEB-INF/views/*.jsp` (23개) |
+
+### global.css 선언 내용 (구체 명세)
+
+```
+/* 1. 라이트 테마 토큰 */
+html {
+ color-scheme: light;
+ --surface: #faf8f5;
+ --card-bg: #fff;
+ --text: #1a1a1a;
+ --text-muted: #5c5c5c;
+ --accent: #e8a54b;
+ --accent-soft: rgba(232, 165, 75, 0.16);
+ --border: rgba(0, 0, 0, 0.08);
+ --shadow: rgba(0, 0, 0, 0.06);
+ --field-bg: #fff;
+ --button-text: #1a1a1a;
+}
+
+/* 2. 다크 테마 토큰 */
+html[data-theme="dark"] {
+ color-scheme: dark;
+ --surface: #121212;
+ --card-bg: #1e1e1e;
+ --text: #ece8e1;
+ --text-muted: #a39e96;
+ --border: rgba(255, 255, 255, 0.1);
+ --shadow: rgba(0, 0, 0, 0.35);
+ --field-bg: #181818;
+ --button-text: #1a1a1a;
+}
+
+/* 3. body 기본 스타일 */
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
+ background: var(--surface);
+ color: var(--text);
+}
+
+/* 4. admin-btn 컴포넌트 (admin 4개 파일 중복 제거) */
+.admin-btn {
+ min-height: 2.25rem;
+ padding: 0 0.85rem;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--card-bg);
+ color: var(--text);
+ font: inherit;
+ font-size: 0.8125rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+.admin-btn:hover { border-color: rgba(232, 165, 75, 0.45); }
+.admin-btn--primary { border-color: transparent; background: var(--accent); color: var(--button-text); }
+.admin-btn--danger { border-color: rgba(200, 60, 60, 0.45); color: #c83c3c; }
+```
+
+**포함하지 않는 것**: 각 JSP 고유 레이아웃 클래스(`.admin-page`, `.form-page`, `.auth-main` 등), `max-width` 값(파일마다 48/72/78rem으로 다름), 페이지 전용 컴포넌트 CSS. 이들은 계속 인라인으로 유지한다.
+
+**`--danger` 토큰**: `admin-unity-feeds.jsp`에만 있는 `--danger: #c83c3c / #ef7878` 변수는 global.css에 추가하지 않는다. 해당 파일이 유일한 사용처이므로 인라인 유지가 맞다.
+
+**`--accent-soft` 미선언 파일 처리**: `login.jsp`와 `profile.jsp`는 `--accent-soft`를 선언하지 않는다. global.css에 전역 선언 후 이 두 파일의 인라인 블록에서 해당 변수가 없어도 문제 없다 (사용 위치가 없거나 다른 변수로 대체됨). 구현 시 각 파일의 실제 사용처를 확인하고 미사용이면 그냥 삭제, 사용이면 global.css 선언이 적용되므로 정상.
+
+### 마이그레이션 경로 (단계별)
+
+**Phase 1 — 파일 생성 + 1개 JSP 검증 (위험 최소)**
+1. `src/main/webapp/css/global.css` 작성
+2. `login.jsp` 1개에만 `` 태그 추가 + 인라인 토큰 블록 제거
+3. 브라우저에서 라이트/다크 테마 전환 육안 검증
+4. 문제 없으면 Phase 2 진행
+
+**Phase 2 — admin 4개 파일 적용 (`.admin-btn` 중복 제거 포함)**
+1. `admin-post-categories.jsp`, `admin-unity-feeds.jsp`, `admin-console.jsp`, `admin-jam-list.jsp` 에 `` 추가
+2. 각 파일의 인라인 `html {}`, `html[data-theme="dark"] {}`, `body {}`, `.admin-btn*` 블록 제거
+3. admin 기능(등록/수정/삭제) 동작 검증
+
+**Phase 3 — 나머지 19개 JSP 일괄 적용**
+1. 남은 파일에 `` 추가 + 인라인 토큰 제거
+2. 각 페이지 라이트/다크 테마 시각 검증
+
+### 위험 평가
+
+| 위험 | 가능성 | 대응 |
+|---|---|---|
+| 일부 JSP가 인라인으로 토큰 값을 오버라이드하고 있다면 global.css 도입 후 이전과 동일 동작 | 낮음 (토큰 값 동일 확인됨) | Phase 1 검증 후 진행 |
+| 특정 파일의 인라인 `html {}` 에 global.css에 없는 추가 변수 존재 (예: `--radius`, `--webgl-bg`) | 있음 (game-detail.jsp 확인) | 파일 전용 변수는 인라인 유지 — 중앙화 대상은 공통 8+4개 토큰만 |
+| Tomcat DefaultServlet이 `/css/` 경로를 차단하는 보안 필터 존재 | 매우 낮음 | `/images/logo.png` 가 이미 동일 방식으로 서빙 중 — 검증됨 |
+| FOUC: CSS 로드 전 테마 변수 미적용 깜빡임 | 낮음 | `theme-init.jsp`가 `` 최상단에서 `data-theme` 속성을 먼저 설정하고 있으므로 CSS 변수 로드 타이밍이 맞음 |
+
+---
+
+## 권고 2 — JS 공용 유틸리티 분리 (P1)
+
+### 접근
+
+`src/main/webapp/js/bibimbap-utils.js` 를 신규 생성하고 admin 4개 파일의 공통 함수(`post`, `handleResult`, `handleError`)를 이 파일로 추출한다. 이 파일은 `window.BibimbapUtils` 네임스페이스에 노출한다.
+
+`notify()`와 `openModal()` 은 title이 파일마다 달라 단순 통합이 불가능하므로 별도 전략을 쓴다(권고 3에서 다룸).
+
+각 JSP에서는 `` 직전에 `` 를 삽입하고, 기존 인라인 함수 선언을 제거한다.
+
+### 파일 경로 및 API 계약 (함수 시그니처)
+
+**신규 파일**: `src/main/webapp/js/bibimbap-utils.js`
+
+```javascript
+window.BibimbapUtils = (function () {
+
+ /**
+ * CSRF 토큰을 포함한 application/x-www-form-urlencoded POST 요청을 전송한다.
+ *
+ * @param {string} url - 요청 대상 URL
+ * @param {URLSearchParams|null} params - 요청 바디 파라미터 (null 이면 빈 URLSearchParams 사용)
+ * @param {string} csrfToken - X-CSRF-Token 헤더에 설정할 토큰 값
+ * @returns {Promise}
+ */
+ function post(url, params, csrfToken) {
+ // ...
+ }
+
+ /**
+ * fetch Response가 ok이면 location.reload(), 아니면 오류 메시지를 콜백으로 전달한다.
+ *
+ * @param {Response} res - fetch가 반환한 Response 객체
+ * @param {function(string)} onError - 오류 메시지 문자열을 받는 콜백
+ */
+ function handleResult(res, onError) {
+ // ...
+ }
+
+ /**
+ * 네트워크/예외 오류 발생 시 오류 메시지를 콜백으로 전달한다.
+ *
+ * @param {function(string)} onError - 오류 메시지 문자열을 받는 콜백
+ * @returns {function} catch 핸들러로 사용 가능한 함수
+ */
+ function makeErrorHandler(onError) {
+ // ...
+ }
+
+ return { post: post, handleResult: handleResult, makeErrorHandler: makeErrorHandler };
+})();
+```
+
+**시그니처 결정 이유**:
+
+- `post(url, params, csrfToken)`: 기존 admin 4개 파일의 `post()` 는 클로저로 `CSRF_TOKEN` 변수를 캡처하는 방식이었다. 전역 파일로 추출하면 클로저 캡처가 불가능하므로 `csrfToken`을 명시 파라미터로 받는다. 호출부가 항상 토큰을 직접 넘기므로 의도가 코드에 드러나고 타입 검사도 명확해진다.
+
+- `handleResult(res, onError)`: 기존 구현은 `notify()`를 직접 호출했다. `notify()`는 파일마다 title이 달라 전역화 불가능하므로, 대신 오류 메시지 문자열을 콜백으로 전달하는 방식으로 변경한다. 호출부가 메시지를 받아 직접 표시하므로 유연성과 분리가 동시에 달성된다.
+
+- `makeErrorHandler(onError)`: 기존 `handleError()`는 매직 문자열 `'요청 중 오류가 발생했습니다.'` 를 하드코딩했다. 유틸리티 레벨에서 특정 언어 문자열을 결정하지 않도록 콜백으로 위임한다.
+
+**inflate 경고**: `post()` 의 세 번째 파라미터 `csrfToken`이 항상 사용되는지 구현 시 확인 필요. 만약 호출부 중 하나라도 빈 문자열을 넘기는 패턴이 발견되면 `BibimbapCsrf.token()` 통합을 고려한다 (concerns 참조).
+
+### 마이그레이션 경로
+
+1. `bibimbap-utils.js` 작성 (3개 함수 포함)
+2. `admin-post-categories.jsp` 에 script 태그 추가 + 인라인 `post`, `handleResult`, `handleError` 함수 제거. `BibimbapUtils.post(...)`, `BibimbapUtils.handleResult(...)`, `BibimbapUtils.makeErrorHandler(...)` 로 호출부 수정
+3. 기능 검증 후 나머지 admin 3개 파일에 동일 적용
+
+### 위험 평가
+
+| 위험 | 가능성 | 대응 |
+|---|---|---|
+| admin 파일마다 `post()` 구현이 미묘하게 달라 통합 시 동작 변경 | 중간 | 통합 전 4개 파일 `post()` 구현 3-way diff 수행 (concerns 등록) |
+| `handleResult` 시그니처 변경으로 기존 호출부 수정 필요 | 확실 | 마이그레이션 범위를 admin 4개로 한정, 변경이 넓지 않음 |
+
+---
+
+## 권고 3 — 모달 사용 패턴 표준화 (P1)
+
+### 접근
+
+**표준 패턴 채택**: `BibimbapModal` API (`window.BibimbapModal.alert / .confirm / .prompt`) 를 직접 호출하는 인라인 패턴을 표준으로 정한다. 기존 헬퍼 래퍼(`openModal()`, `notify()`)는 더 이상 신규 작성하지 않는다.
+
+**패턴 A (openModal 래퍼) 처리**: login/signup/game-register/profile 4개 파일의 `openModal()` 로컬 함수를 제거하고 호출부를 `window.BibimbapModal.alert({...})` 직접 호출로 대체한다. `BibimbapModal` 미존재 폴백은 `else { alert(message); }` 로 단순화한다.
+
+**패턴 B (notify 래퍼) 처리**: admin 4개 파일의 `notify()` 를 제거한다. 권고 2의 `BibimbapUtils.handleResult(res, onError)` 에서 `onError` 콜백이 `BibimbapModal.alert` 를 직접 호출하도록 호출부를 작성한다. 이렇게 하면 `notify()` 래퍼 없이도 동일 효과를 얻는다.
+
+**패턴 C (인라인 직접 체크) 처리**: posts-form/recruit-form/game-detail 은 이미 인라인이므로 현재 형태 유지. 다만 `game-detail.jsp` 내 `notifyError()` / `confirmAction()` 헬퍼(라인 1553-1561)는 파일 전용이므로 제거하지 않는다. 단일 파일에서의 로컬 헬퍼는 허용.
+
+**window.confirm 직접 사용 수정 (보안/일관성)**:
+- `admin-post-categories.jsp:323` 의 `window.confirm()` → `BibimbapModal.confirm({...})` 으로 변경
+- `admin-unity-feeds.jsp:401` 의 `window.confirm()` → `BibimbapModal.confirm({...})` 으로 변경
+
+이 두 곳은 삭제 확인 흐름이므로 `onConfirm` 콜백에 실제 삭제 로직을 이동한다.
+
+### 표준 패턴 예시 (참고용)
+
+```javascript
+// 알림 (확인 버튼만)
+if (window.BibimbapModal) {
+ window.BibimbapModal.alert({ title: '제목', message: '내용', confirmText: '확인' });
+} else {
+ alert('내용');
+}
+
+// 확인/취소 대화
+if (window.BibimbapModal) {
+ window.BibimbapModal.confirm({
+ title: '삭제 확인',
+ message: '삭제하시겠습니까?',
+ onConfirm: function () { /* 삭제 로직 */ }
+ });
+} else {
+ if (window.confirm('삭제하시겠습니까?')) { /* 삭제 로직 */ }
+}
+```
+
+### 파일 영향 맵
+
+| 변경 유형 | 경로 | 역할 |
+|---|---|---|
+| 수정 — openModal 제거 | login.jsp, signup.jsp, game-register.jsp, profile.jsp | 패턴 A → 직접 호출 |
+| 수정 — notify 제거 | admin-console.jsp, admin-jam-list.jsp, admin-post-categories.jsp, admin-unity-feeds.jsp | 패턴 B → 콜백 방식 |
+| 수정 — window.confirm 교체 | admin-post-categories.jsp:323, admin-unity-feeds.jsp:401 | BibimbapModal.confirm 사용 |
+| 유지 — 변경 없음 | game-detail.jsp (notifyError/confirmAction), posts-form.jsp, recruit-form.jsp | 파일 전용 헬퍼 허용 |
+
+### 마이그레이션 경로
+
+1. `admin-post-categories.jsp` + `admin-unity-feeds.jsp` 의 `window.confirm` 2곳 먼저 수정 (보안 갭과 연결)
+2. login/signup openModal 제거 + 직접 호출 전환
+3. game-register/profile 동일 처리
+4. admin 4개 파일 notify 제거 (권고 2와 동시 진행)
+
+### 위험 평가
+
+| 위험 | 가능성 | 대응 |
+|---|---|---|
+| BibimbapModal이 modal.jsp 로드 전에 JS가 실행되는 경우 | 낮음 (header.jsp가 modal.jsp를 항상 include) | 변경 없이 현 구조 유지 |
+| window.confirm 삭제 시 콜백 이동 누락으로 삭제 로직 미실행 | 중간 | 각 파일 수정 후 삭제 기능 수동 검증 |
+
+---
+
+## 권고 4 — 폼 제출 유틸리티 + CSRF 갭 수정 (P0 — 보안)
+
+### 접근
+
+**즉시 수정 (CSRF 갭)**: `recruit-form.jsp` 의 fetch 호출부에서 `BibimbapCsrf` 미존재 폴백(라인 390-394)이 CSRF 토큰을 전혀 포함하지 않는 문제를 수정한다. 두 가지 방법 중 **방법 A를 채택**한다.
+
+**방법 A (채택)**: 폴백 브랜치에 `hidden input`에서 추출한 토큰을 직접 삽입한다.
+
+```javascript
+// recruit-form.jsp 수정안
+var csrfToken = (document.querySelector('input[name="_csrf"]') || {}).value || '';
+fetch(form.action, {
+ method: 'POST',
+ headers: window.BibimbapCsrf ? window.BibimbapCsrf.headers({
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ 'Accept': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ }) : {
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ 'Accept': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': csrfToken // 갭 수정
+ },
+ body: body
+})
+```
+
+**전제**: `recruit-form.jsp` 에 `` hidden input을 추가해야 한다. 현재 `recruit-form.jsp` 에는 이 hidden input이 없다. 추가하면 `new FormData(form)` 이 자동으로 `_csrf` 파라미터를 포함하므로 서버사이드 form 파라미터 검증도 함께 강화된다.
+
+**방법 B (미채택)**: `theme-init.jsp` 의 `BibimbapCsrf` 를 항상 신뢰하여 폴백 분기 자체를 제거. 단, `BibimbapCsrf` 미존재 케이스를 완전히 제거하면 `theme-init.jsp` 가 로드 실패 시 CSRF 토큰이 아예 없어지는 더 큰 갭이 생긴다. 따라서 채택하지 않는다.
+
+**401 리다이렉트 불일치 수정**: `recruit-form.jsp` 의 401 처리를 `posts-form.jsp` 와 동일하게 `redirectLogin` 함수로 분리한다.
+
+**성공 시 폴백 불일치 수정**: `recruit-form.jsp:419-421` 의 `alert(...)` 호출을 `posts-form.jsp:260-261` 패턴에 맞게 `alert` 없이 `go()` 만 호출하도록 변경한다.
+
+### 파일 영향 맵
+
+| 변경 유형 | 경로 | 역할 |
+|---|---|---|
+| 수정 (보안) | recruit-form.jsp | hidden _csrf input 추가 + 폴백 브랜치 토큰 삽입 |
+| 수정 (일관성) | recruit-form.jsp | 401 처리 redirectLogin 함수 분리 |
+| 수정 (일관성) | recruit-form.jsp | 성공 폴백 alert 제거 |
+| 변경 없음 | posts-form.jsp | 현행 유지 (기준 파일) |
+
+### 마이그레이션 경로
+
+1. `recruit-form.jsp` 에 `` 추가 (JSP EL 또는 request attribute 사용)
+2. JS 폴백 브랜치에 `X-CSRF-Token` 헤더 추가
+3. 401 처리 함수 분리
+4. 성공 폴백 통일
+5. 실제 폼 제출(등록) + 401 시나리오(로그아웃 후 제출) 수동 검증
+
+### 위험 평가
+
+| 위험 | 가능성 | 대응 |
+|---|---|---|
+| hidden _csrf input 추가 시 서버 컨트롤러가 기대하는 파라미터명 불일치 | 낮음 | posts-form.jsp와 동일 파라미터명 `_csrf` 사용 |
+| BibimbapCsrf가 항상 존재한다고 가정하고 폴백을 제거하고 싶은 유혹 | 중간 | 방법 B 미채택 이유 참조 — 폴백 브랜치 유지 |
+
+---
+
+## 권고 5 — 날짜 포맷 유틸리티 모듈 (P2)
+
+### 접근
+
+`game-detail.jsp` 인라인의 `fmtAbsolute`, `fmtRelative`, `buildTimeEl` 세 함수를 `src/main/webapp/js/bibimbap-date.js` 로 추출하고 `window.BibimbapDate` 네임스페이스에 노출한다.
+
+`game-detail.jsp` 는 `` 를 추가하고 기존 인라인 선언을 제거한다. 다른 detail 페이지(posts-detail, recruit-detail, jam-detail)에서 날짜 포맷이 필요해질 때 이 파일을 include하면 된다 — 현재는 필요하지 않으므로 강제 적용하지 않는다.
+
+### 파일 경로 및 API 계약 (함수 시그니처)
+
+**신규 파일**: `src/main/webapp/js/bibimbap-date.js`
+
+```javascript
+window.BibimbapDate = (function () {
+
+ /**
+ * ISO 8601 문자열을 ko-KR 로케일 절대 날짜/시각 문자열로 변환한다.
+ *
+ * @param {string} iso - ISO 8601 날짜 문자열
+ * @returns {string} - "2026. 6. 30. 오전 10:00:00" 형식, 파싱 실패 시 빈 문자열
+ */
+ function fmtAbsolute(iso) { ... }
+
+ /**
+ * ISO 8601 문자열을 상대 시각 문자열로 변환한다 (7일 이내: "n분/시간/일 전", 초과: 절대).
+ *
+ * @param {string} iso - ISO 8601 날짜 문자열
+ * @returns {string} - "3시간 전" 또는 절대 날짜, 파싱 실패 시 빈 문자열
+ */
+ function fmtRelative(iso) { ... }
+
+ /**
+ *