---
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의 `<head>` 최상단에 `<link rel="stylesheet" href="${pageContext.request.contextPath}/css/global.css">` 를 삽입한다. 이 link는 `theme-init.jsp` `<jsp:include>` 직후에 위치시켜 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개에만 `<link>` 태그 추가 + 인라인 토큰 블록 제거
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` 에 `<link>` 추가
2. 각 파일의 인라인 `html {}`, `html[data-theme="dark"] {}`, `body {}`, `.admin-btn*` 블록 제거
3. admin 기능(등록/수정/삭제) 동작 검증

**Phase 3 — 나머지 19개 JSP 일괄 적용**
1. 남은 파일에 `<link>` 추가 + 인라인 토큰 제거
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`가 `<head>` 최상단에서 `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에서는 `</body>` 직전에 `<script src="${pageContext.request.contextPath}/js/bibimbap-utils.js"></script>` 를 삽입하고, 기존 인라인 함수 선언을 제거한다.

### 파일 경로 및 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<Response>}
   */
  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` 에 `<input type="hidden" name="_csrf" value="...">` 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` 에 `<input type="hidden" name="_csrf" value="...">` 추가 (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` 는 `<script src=".../js/bibimbap-date.js"></script>` 를 추가하고 기존 인라인 선언을 제거한다. 다른 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) { ... }

  /**
   * <time> 요소를 생성한다. 수정된 항목이면 "(수정됨)" 뱃지를 DocumentFragment로 감싸 반환한다.
   *
   * @param {string}  iso    - ISO 8601 날짜 문자열
   * @param {boolean} edited - true이면 "(수정됨)" 뱃지 추가
   * @returns {HTMLElement|DocumentFragment}
   */
  function buildTimeEl(iso, edited) { ... }

  return { fmtAbsolute: fmtAbsolute, fmtRelative: fmtRelative, buildTimeEl: buildTimeEl };
})();
```

### 파일 영향 맵

| 변경 유형 | 경로 | 역할 |
|---|---|---|
| 신규 | src/main/webapp/js/bibimbap-date.js | 날짜 포맷 유틸리티 |
| 수정 | src/main/webapp/WEB-INF/views/game-detail.jsp | script 태그 추가 + 인라인 3개 함수 제거 + `BibimbapDate.` 프리픽스 추가 |

### 마이그레이션 경로

1. `bibimbap-date.js` 작성 (기존 game-detail.jsp 인라인 코드 그대로 이식)
2. `game-detail.jsp` 에 script 태그 추가
3. 호출부 3곳(`buildTimeEl` 2건, `fmtAbsolute/fmtRelative` 직접 사용 1건)을 `BibimbapDate.buildTimeEl(...)` 등으로 수정
4. 인라인 함수 선언 3개 제거
5. game-detail 댓글/리뷰 날짜 표시 수동 검증

### 위험 평가

| 위험 | 가능성 | 대응 |
|---|---|---|
| game-detail.jsp가 매우 큰 파일(2200줄+)이므로 인라인 선언 제거 시 위치 파악 실수 | 중간 | 라인 번호 명시: fmtAbsolute 1516, fmtRelative 1521, buildTimeEl 1538 |
| 다른 파일에서 동일 함수명을 전역 선언하는 경우 충돌 | 없음 (조사 결과 game-detail.jsp 단독 선언 확인됨) | 추가 확인 불필요 |

---

## 전체 파일 영향 맵

| 변경 유형 | 경로 | 역할 |
|---|---|---|
| 신규 | src/main/webapp/css/global.css | 디자인 토큰 + body + admin-btn |
| 신규 | src/main/webapp/js/bibimbap-utils.js | post / handleResult / makeErrorHandler |
| 신규 | src/main/webapp/js/bibimbap-date.js | 날짜 포맷 3개 함수 |
| 수정 (인라인 CSS 제거 + link 추가) | src/main/webapp/WEB-INF/views/*.jsp (23개) | global.css 연동 |
| 수정 (script 추가 + 인라인 JS 제거) | admin-console.jsp, admin-jam-list.jsp, admin-post-categories.jsp, admin-unity-feeds.jsp | bibimbap-utils.js 연동 |
| 수정 (script 추가 + 인라인 제거) | game-detail.jsp | bibimbap-date.js 연동 |
| 수정 (openModal 제거) | login.jsp, signup.jsp, game-register.jsp, profile.jsp | 모달 패턴 표준화 |
| 수정 (window.confirm 교체) | admin-post-categories.jsp:323, admin-unity-feeds.jsp:401 | BibimbapModal.confirm 전환 |
| 수정 (CSRF 갭 — 보안) | recruit-form.jsp | hidden input 추가 + 폴백 헤더 수정 |

---

## 대안 비교

### CSS 중앙화 방안

| 안 | 장점 | 단점 | 채택? |
|---|---|---|---|
| A: `src/main/webapp/css/global.css` | 빌드 도구 불필요, Tomcat 즉시 서빙 | 캐시 버스팅 수동 관리 필요 | **채택** |
| B: `theme-init.jsp`에 `<style>` 블록 추가 | include 메커니즘 재사용 | JSP 응답마다 CSS가 HTML에 인라인 삽입됨 — 캐싱 불가, 오히려 더 많은 바이트 전송 | 미채택 |
| C: `src/main/resources/static/css/global.css` | Spring Boot 자동 서빙 | 해당 경로가 현재 미존재이고 WAR 배포 시 classpath static과 webapp static 혼용이 복잡해짐 | 미채택 |

### JS 유틸리티 배포 방안

| 안 | 장점 | 단점 | 채택? |
|---|---|---|---|
| A: `src/main/webapp/js/` 정적 파일 | 빌드 불필요, 직접 서빙 | 캐시 버스팅 수동 관리 필요 | **채택** |
| B: JSP include 파일 (`<jsp:include>`) | 기존 include 패턴 일관성 | JS를 JSP로 서빙하면 컨텐츠 타입이 `text/html`로 설정될 위험, 브라우저 모듈 캐싱 불가 | 미채택 |

---

## 구현 순서 권고

```
1. [P0-보안] recruit-form.jsp CSRF 갭 수정         — 단독 작업, 위험 낮음
2. [P0] global.css 작성 + login.jsp 1개 검증        — Phase 1
3. [P0] admin 4개 파일 global.css 적용              — Phase 2
4. [P0] 나머지 19개 JSP global.css 적용             — Phase 3
5. [P1] bibimbap-utils.js 작성                       — admin 파일 수정 전제
6. [P1] admin 4개 파일 bibimbap-utils.js 연동        — 5 완료 후
7. [P1] window.confirm 2곳 BibimbapModal.confirm 전환 — 6과 동시 가능
8. [P1] openModal 패턴 4개 파일 표준화               — 7 완료 후
9. [P2] bibimbap-date.js 추출 + game-detail.jsp 연동 — 독립 작업
```

---

## 롤아웃 / 마이그레이션

**역호환**: 이 권고안의 모든 변경은 순수 정적 파일 추가 + JSP 인라인 제거이므로 서버사이드 Java 코드, MyBatis, API 계약에 영향이 없다. 롤백은 추가한 `<link>`, `<script>` 태그를 되돌리고 인라인 블록을 복원하면 된다.

**캐시 버스팅**: 빌드 시스템이 없으므로 쿼리 스트링 버전 파라미터(`?v=20260630`)를 link/script 태그에 수동으로 붙인다. 초기 배포 시 한 번 붙이고, CSS/JS 변경 시마다 날짜를 업데이트하는 것으로 충분하다.

**배포 단위**: 정적 파일 추가는 WAR 재배포가 필요하다. 단계별 적용이지만 배포 자체는 1회로 묶어 진행해도 된다.

**롤백 경로**: 
1. `global.css` 추가 후 이슈 발생 시 → `<link>` 태그 제거 + git revert
2. `bibimbap-utils.js` 이후 이슈 발생 시 → script 태그 제거 + 인라인 함수 복원

---

## 검증 포인트

아래 AC는 verification-advisor가 점검한다.

**AC-1 (P0)**: `src/main/webapp/css/global.css` 파일 존재 확인.

**AC-2 (P0)**: global.css가 라이트/다크 테마 HTML을 열었을 때 각각 올바른 배경색(`--surface: #faf8f5` / `#121212`)이 적용되는지 브라우저 확인.

**AC-3 (P0)**: JSP 23개 파일에 `<link rel="stylesheet" ... /css/global.css>` 포함 전수 확인.
```
grep -rl 'global\.css' src/main/webapp/WEB-INF/views/ | wc -l == 23
```
*시점 안정성 주의*: 이 카운트는 구현이 완료된 직후 측정해야 한다. 구현 중간에 측정하면 미완료 파일이 포함돼 FAIL로 나올 수 있다. verification은 구현 완료 후 1회 측정한다.

**AC-4 (P0-보안)**: `recruit-form.jsp` 에 `<input type="hidden" name="_csrf"` 가 존재하고, JS 폴백 브랜치에 `'X-CSRF-Token': csrfToken` 이 포함되어 있음 확인.
```
grep -c 'X-CSRF-Token' src/main/webapp/WEB-INF/views/recruit-form.jsp >= 1
```

**AC-5 (P1)**: `src/main/webapp/js/bibimbap-utils.js` 파일 존재 + `window.BibimbapUtils` 노출 확인.

**AC-6 (P1)**: admin 4개 파일에 인라인 `function post(` 선언이 제거되었음 확인.
```
grep -rl 'function post(' src/main/webapp/WEB-INF/views/admin-*.jsp | wc -l == 0
```

**AC-7 (P1)**: `admin-post-categories.jsp:323` 및 `admin-unity-feeds.jsp:401` 위치에 `window.confirm(` 이 더 이상 없음 확인.
```
grep -c 'window\.confirm' src/main/webapp/WEB-INF/views/admin-post-categories.jsp == 0
grep -c 'window\.confirm' src/main/webapp/WEB-INF/views/admin-unity-feeds.jsp == 0
```

**AC-8 (P1)**: login/signup/game-register/profile 4개 파일에 `function openModal(` 선언이 제거되었음 확인 (전수 4건 → 0건).
```
grep -rl 'function openModal(' src/main/webapp/WEB-INF/views/ | wc -l == 0
```

**AC-9 (P2)**: `src/main/webapp/js/bibimbap-date.js` 파일 존재 + `window.BibimbapDate` 노출 확인.

**AC-10 (P2)**: `game-detail.jsp` 인라인에 `function fmtAbsolute(`, `function fmtRelative(`, `function buildTimeEl(` 3개 선언이 모두 제거되었음 확인 (전수 3건 → 0건).
```
grep -c 'function fmtAbsolute\|function fmtRelative\|function buildTimeEl' src/main/webapp/WEB-INF/views/game-detail.jsp == 0
```

**AC-11 (전체)**: 신규 정적 파일 전수 3개 존재 확인.
```
ls src/main/webapp/css/global.css src/main/webapp/js/bibimbap-utils.js src/main/webapp/js/bibimbap-date.js
```
