bibimbap/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendat...

27 KiB

phase agent agent_version generated_at concerns concerns_checked references
design design-advisor 1 2026-06-30T03:15:00Z
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 의 토큰 범위와 겹치지 않는 컴포넌트 전용 변수이므로 중앙화 대상에서 제외. 구현 시 헷갈리지 않도록 명칭 분리 유지 확인 요망.
true
requirements research adrs
(인라인 입력 — 이 문서의 ## 맥락 섹션) .atp/work-session/20260630-113258/research/frontend-analysis.md

프론트엔드 컴포넌트 개선 권고안

목표 / 비목표

목표

  • 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)가 직접 서빙한다. UploadResourceConfigaddResourceHandlers/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.jspprofile.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

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.jspnotifyError() / confirmAction() 헬퍼(라인 1553-1561)는 파일 전용이므로 제거하지 않는다. 단일 파일에서의 로컬 헬퍼는 허용.

window.confirm 직접 사용 수정 (보안/일관성):

  • admin-post-categories.jsp:323window.confirm()BibimbapModal.confirm({...}) 으로 변경
  • admin-unity-feeds.jsp:401window.confirm()BibimbapModal.confirm({...}) 으로 변경

이 두 곳은 삭제 확인 흐름이므로 onConfirm 콜백에 실제 삭제 로직을 이동한다.

표준 패턴 예시 (참고용)

// 알림 (확인 버튼만)
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.jspwindow.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에서 추출한 토큰을 직접 삽입한다.

// 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.jspBibimbapCsrf 를 항상 신뢰하여 폴백 분기 자체를 제거. 단, BibimbapCsrf 미존재 케이스를 완전히 제거하면 theme-init.jsp 가 로드 실패 시 CSRF 토큰이 아예 없어지는 더 큰 갭이 생긴다. 따라서 채택하지 않는다.

401 리다이렉트 불일치 수정: recruit-form.jsp 의 401 처리를 posts-form.jsp 와 동일하게 redirectLogin 함수로 분리한다.

성공 시 폴백 불일치 수정: recruit-form.jsp:419-421alert(...) 호출을 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

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:323admin-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