---
kind: analysis
title: 프론트엔드 컴포넌트 중복/유지보수·확장성 검토 (2026-06-30)
description: JSP 기반 프론트엔드에서 발견된 CSS 토큰 23개 파일 중복, JS 유틸리티 함수 중복, BibimbapModal 호출 패턴 혼존, recruit-form CSRF 폴백 갭(보안), 날짜 포맷 함수 편재를 분석하고 권고 구현 순서를 기록한다.
perspective: neutral
valid_starting_point_for: 프론트엔드 중복 제거 작업 착수, CSRF 갭 수정(보안 P0), CSS/JS 공용 파일 설계
owner: art
stability: snapshot
last_reviewed: 2026-06-30
source: work-session 20260630-113258 (research-advisor frontend-analysis.md, design-advisor frontend-design-recommendations.md) 에서 distill
superseded_note: 후속 결정으로 전제가 바뀌면 이 줄을 갱신한다 — 현재 없음
---

# 프론트엔드 컴포넌트 중복/유지보수·확장성 검토 (2026-06-30)

> 조사 범위: 27개 JSP 파일 전수 직접 읽기. 모든 사실은 코드 직접 확인(`확인됨`). 미탐색 항목은 [미발견/조사 불충분](#미발견--조사-불충분-항목)에 명시.
>
> 출처: work-session `20260630-113258` — `research/frontend-analysis.md` (research-advisor) + `artifacts/frontend-design-recommendations.md` (design-advisor).

## 개요

JSP 기반 프론트엔드에서 5개 유형의 중복/이슈가 확인됐다. 가장 심각한 것은 라이트/다크 테마 CSS 변수 블록과 `body` 기본 스타일이 23개 파일에 동일하게 복사된 구조이며, 보안 측면에서는 `recruit-form.jsp`의 CSRF 폴백 분기에서 토큰이 전혀 포함되지 않는 갭이 존재한다.

전체 27개 JSP 중 26개에 인라인 `<style>` 블록이 존재하며, 누적 인라인 CSS는 5,969줄이다.

## 발견 이슈 요약 (우선순위 매트릭스)

| 이슈 | 영향도 | 구현 비용 | 우선순위 |
|---|---|---|---|
| CSS 토큰 23개 파일 중복 | 높음 (유지보수 23배 비용) | 낮음 (파일 추가 + include 삽입) | P0 |
| 폼 제출 CSRF 갭 (recruit-form) | 높음 (보안) | 낮음 (10줄 이내 수정) | P0 — 보안 |
| JS 공용 유틸리티 분리 | 중간 (동작 불일치 방지) | 중간 (파일 작성 + 기존 인라인 제거) | P1 |
| 모달 패턴 표준화 | 중간 (신규 페이지 기준 제공) | 중간 (패턴 A/B 대체 + admin 2곳 수정) | P1 |
| 날짜 포맷 유틸리티 모듈 | 낮음 (예방적 분리) | 낮음 (파일 추출) | P2 |

## 이슈별 상세

### CSS 디자인 토큰 23개 파일 중복 (P0)

#### 현황

전체 27개 JSP 중 26개에 인라인 `<style>` 블록이 존재한다 (`theme-init.jsp` 제외). 다음 3개 블록이 23개 파일에 동일하게 복사된다.

| 중복 패턴 | 반복 파일 수 | 대표 위치 |
|---|---|---|
| `html {}` 라이트 테마 CSS 변수 선언 (8개 색상 토큰) | 23개 | `admin-console.jsp:38`, `jam-list.jsp:21`, `login.jsp:12` |
| `html[data-theme="dark"] {}` 다크 테마 CSS 변수 선언 | 23개 | `admin-console.jsp:51`, `posts-list.jsp:41`, `recruit-list.jsp:34` |
| `body {}` 기본 리셋 + 폰트 + 배경 | 23개 | `admin-console.jsp:62`, `jam-list.jsp:41`, `terms.jsp:29` |

핵심 공통 토큰: `--surface`, `--card-bg`, `--text`, `--text-muted`, `--accent`, `--accent-soft`, `--border`, `--shadow` (라이트) / `#121212`, `#1e1e1e`, `#ece8e1`, `#a39e96`, `rgba(255,255,255,0.1)` (다크).

추가 중복: `.admin-btn` / `.admin-btn--primary` / `.admin-btn--danger` 컴포넌트 CSS 20줄 이상이 4개 admin 파일에 동일 복사됨 (`admin-console.jsp:171-194`, `admin-jam-list.jsp:167-190`, `admin-post-categories.jsp:100`, `admin-unity-feeds.jsp:108`).

`safe-area` padding 패턴(`padding: 1.5rem max(1rem, env(safe-area-inset-left)) 3rem max(1rem, env(safe-area-inset-right))`)은 25개 파일에 반복되나, `max-width` 값은 48/56/64/72/78rem으로 파일별 상이하다.

#### 충돌/갭

- CSS 변수 선언이 파일마다 포함하는 변수 수가 8~14개로 약간씩 달라, 특정 파일에서 일부 변수가 미선언된 상태일 수 있다.
- `jam-detail.jsp:128`과 `recruit-detail.jsp:116`의 `.detail-button--primary color` 값 불일치 (`#1a1a1a` vs `var(--button-text)`).

#### 권고 (design-advisor)

`src/main/webapp/css/global.css` 정적 파일을 신규 생성하여 공통 토큰, `body` 기본 스타일, `.admin-btn` 컴포넌트를 선언한다. 각 JSP `<head>`에 `<link rel="stylesheet" href="${pageContext.request.contextPath}/css/global.css">` 를 삽입하고 인라인 중복 블록을 제거한다.

**포함 대상**: 공통 8+4개 토큰, `body` 기본 스타일, `.admin-btn*` 컴포넌트.

**포함하지 않는 것**: 각 JSP 고유 레이아웃 클래스, `max-width` 값(파일마다 다름), `--danger` 변수(`admin-unity-feeds.jsp` 단독 사용), 페이지 전용 컴포넌트 CSS.

**배포 경로**: `src/main/webapp/` 하위 정적 파일은 Tomcat DefaultServlet이 직접 서빙한다. `src/main/resources/static/` 경로 불필요.

**마이그레이션 3단계**: Phase 1(파일 생성 + login.jsp 1개 검증) → Phase 2(admin 4개 파일) → Phase 3(나머지 19개 JSP 일괄).

**위험**: `game-detail.jsp`처럼 `--radius`, `--webgl-bg` 등 파일 전용 변수가 있는 경우 해당 변수는 인라인 유지. FOUC는 `theme-init.jsp`가 `<head>` 최상단에서 `data-theme` 속성을 먼저 설정하므로 낮음.

---

### JS 유틸리티 함수 중복 (P1)

#### 현황

admin 4개 파일(`admin-console.jsp`, `admin-jam-list.jsp`, `admin-post-categories.jsp`, `admin-unity-feeds.jsp`)에 동일 패턴의 함수가 각각 선언된다.

| 함수 | 파일 | 영향도 |
|---|---|---|
| `notify(message)` | admin 4개 파일 각각 선언 | Medium |
| `post(url, params)` | 동일 4개 파일. 구현이 약간씩 다름 (params 처리, _csrf 추가 여부) | High |
| `handleResult(res)`, `handleError()` | 동일 4개 파일 동일 패턴 | Medium |

특히 `post()` 함수는 파일마다 구현이 미묘하게 달라 동작 불일치 위험이 내재한다.

#### 권고 (design-advisor)

`src/main/webapp/js/bibimbap-utils.js` 신규 생성. `window.BibimbapUtils` 네임스페이스에 `post(url, params, csrfToken)`, `handleResult(res, onError)`, `makeErrorHandler(onError)` 를 노출한다.

**시그니처 변경 이유**:
- `post(url, params, csrfToken)`: 기존 클로저 캡처 방식 대신 토큰을 명시 파라미터로 받는다.
- `handleResult(res, onError)`: 기존의 `notify()` 직접 호출 대신 오류 메시지 콜백으로 위임한다 (title이 파일마다 달라 전역화 불가).
- `makeErrorHandler(onError)`: 매직 문자열 하드코딩 제거.

**`notify()` / `openModal()` 통합 불가**: title이 파일마다 달라 단순 통합이 불가능하므로 권고 3(모달 표준화)에서 별도 처리.

**위험**: admin 파일마다 `post()` 구현이 미묘하게 달라 통합 시 동작 변경 가능. 통합 전 4개 파일 3-way diff 수행 권고.

---

### BibimbapModal 호출 패턴 혼존 (P1)

#### 현황

`BibimbapModal` API는 `modal.jsp:174-238`에 단일 `<div id="site-modal">` 재사용 방식으로 구현되어 있으며, `alert`, `confirm`, `prompt` 메서드를 제공한다. 그러나 호출 패턴이 3가지로 혼존한다.

**패턴 A — `openModal()` 헬퍼 래퍼 (4개 파일)**

`login.jsp:256`, `signup.jsp:269`, `game-register.jsp:490`, `profile.jsp:539` 가 동일 시그니처의 `openModal(title, message, confirmText, onConfirm)` 로컬 함수를 각자 정의.

**패턴 B — `notify()` 헬퍼 래퍼 (4개 admin 파일)**

`admin-jam-list.jsp:389`, `admin-console.jsp:401`, `admin-post-categories.jsp:251`, `admin-unity-feeds.jsp:305` 가 `notify(message)` 로컬 함수를 각자 정의 (alert 전용, title 하드코딩).

**패턴 C — 인라인 직접 체크 (3개 파일)**

`recruit-form.jsp:411, 424`, `posts-form.jsp:252, 269`, `game-detail.jsp:1425, 1439, 1474, 1482, 1553, 1558` 가 호출 지점마다 인라인으로 체크.

**특이 케이스**: `game-detail.jsp:1553-1561`에 `notifyError()`, `confirmAction()` 헬퍼를 별도 정의하면서, 동일 파일 `1425-1449`에 인라인 체크도 병존하여 단일 파일 내 두 패턴이 혼재한다.

**`window.confirm` 직접 사용**: `admin-post-categories.jsp:323`과 `admin-unity-feeds.jsp:401`에서 삭제 확인 대화상자에 `window.confirm()`을 직접 사용. 알림에는 `notify()` 헬퍼(BibimbapModal 체크)를 쓰면서 삭제 확인에는 직접 사용하는 동일 파일 내 불일치.

#### 권고 (design-advisor)

**표준 패턴**: `window.BibimbapModal.alert / .confirm / .prompt` 직접 호출. 기존 헬퍼 래퍼는 신규 작성 금지.

- 패턴 A: login/signup/game-register/profile 4개 파일의 `openModal()` 제거 → 직접 호출로 대체.
- 패턴 B: admin 4개 파일의 `notify()` 제거 → 권고 2의 `BibimbapUtils.handleResult(res, onError)` 의 `onError` 콜백에서 직접 호출.
- 패턴 C: posts-form/recruit-form/game-detail 현재 형태 유지. `game-detail.jsp` 내 `notifyError()` / `confirmAction()` 파일 전용 헬퍼는 제거하지 않는다 (단일 파일 로컬 헬퍼 허용).
- `window.confirm` 2곳을 `BibimbapModal.confirm({...})` 으로 교체.

---

### recruit-form CSRF 폴백 갭 (P0 — 보안)

#### 현황

`posts-form.jsp`와 `recruit-form.jsp`는 fetch 기반 비동기 폼 제출, `checkValidity()` 검증, `BibimbapModal.alert` 처리, `BibimbapCsrf` CSRF 헤더 주입이라는 동일 골격을 공유하지만, CSRF 처리에 불일치가 존재한다.

- `posts-form.jsp:169` — `<input type="hidden" name="_csrf" value="<%= csrfTokenHtml %>">` hidden input 존재 + JS 변수 이중 포함 (`posts-form.jsp:211, 229`).
- `recruit-form.jsp:386-394` — hidden CSRF input 없음. `BibimbapCsrf` 미존재 시 폴백 헤더에 CSRF 토큰이 전혀 포함되지 않는 보안 갭.

추가 불일치:

| 항목 | posts-form.jsp | recruit-form.jsp |
|---|---|---|
| 성공 시 폴백 | `} else { go(); }` (alert 없음) | `} else { alert('팀원 모집글이 등록되었습니다.'); go(); }` |
| 401 처리 | `redirectLogin` 함수 분리, `onConfirm`에서 호출 | 모달 `onConfirm`과 else 분기 양쪽에 인라인 중복 |
| 수정 모드 | `mode`, `postId` 로 등록/수정 URL 분기 있음 | 수정 모드 분기 없음 |

#### 권고 (design-advisor)

**즉시 수정 (방법 A 채택)**: `recruit-form.jsp`에 `<input type="hidden" name="_csrf" value="...">` hidden input 추가 + JS 폴백 분기에 `X-CSRF-Token: csrfToken` 헤더 삽입.

```javascript
// 수정안 요지
var csrfToken = (document.querySelector('input[name="_csrf"]') || {}).value || '';
// ... BibimbapCsrf 미존재 시 폴백 헤더에 'X-CSRF-Token': csrfToken 추가
```

hidden input 추가 시 `new FormData(form)` 이 자동으로 `_csrf` 파라미터를 포함하므로 서버사이드 form 파라미터 검증도 함께 강화된다.

**방법 B (미채택)**: BibimbapCsrf를 항상 신뢰하여 폴백 분기 자체 제거 — BibimbapCsrf 로드 실패 시 더 큰 갭이 생기므로 채택하지 않는다.

**추가 수정**: 401 처리를 `redirectLogin` 함수로 분리, 성공 폴백 alert 제거.

---

### 날짜 포맷 함수 편재 (P2)

#### 현황

날짜/시간 포맷 JavaScript 함수가 `game-detail.jsp` 단 하나의 파일에만 존재한다. 조사 범위 7개 파일(`posts-detail`, `recruit-detail`, `jam-detail`, `posts-list`, `jam-list`, `recruit-list`, `game-detail`) 중 나머지 6개에는 날짜 포맷 JS 함수가 전혀 없다.

| 함수 | 위치 | 구현 방식 |
|---|---|---|
| `fmtAbsolute(iso)` | `game-detail.jsp:1516-1519` | `new Date(iso).toLocaleString('ko-KR')` |
| `fmtRelative(iso)` | `game-detail.jsp:1521-1536` | 경과 시간 직접 산술 계산 (60/3600/86400초 분기) |
| `buildTimeEl(iso, edited)` | `game-detail.jsp:1538-1551` | 두 함수 합성, `<time>` 요소 + "(수정됨)" 뱃지 반환 |

사용 위치: `game-detail.jsp:1878` (댓글), `game-detail.jsp:2183-2186` (리뷰).

`Intl.RelativeTimeFormat` / `Intl.DateTimeFormat` 직접 사용은 조사 범위 7개 파일 전체에서 0 hit.

**구조적 위험**: 다른 detail 페이지가 동일 기능이 필요해지면 복사 중복 발생 구조.

#### 권고 (design-advisor)

`src/main/webapp/js/bibimbap-date.js` 신규 생성. `window.BibimbapDate` 네임스페이스에 3개 함수 노출. `game-detail.jsp`는 script 태그 추가 + 인라인 선언 제거 + `BibimbapDate.` 프리픽스 적용.

현재 다른 detail 페이지에서 날짜 포맷이 필요하지 않으므로 강제 적용하지 않는다. 필요 시 include.

---

## 권고 구현 순서

```
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 연동 — 독립 작업
```

**검증 기준(AC)**: 구현 완료 후 design-advisor 산출물의 AC-1~AC-11 항목을 기준으로 verification-advisor가 점검한다.

**롤백 경로**: 추가한 `<link>`, `<script>` 태그를 되돌리고 인라인 블록 복원. 서버사이드 Java 코드, MyBatis, API 계약에 영향 없음.

**캐시 버스팅**: 빌드 시스템 없음 → 쿼리 스트링 버전 파라미터(`?v=20260630`) 수동 관리.

---

## 미발견 / 조사 불충분 항목

- `theme-init.jsp` 내부 구현 미탐색 — JS 테마 초기화 방식과 CSS 변수 일원화 가능성 미확인
- `header.jsp`, `footer.jsp`에서 이미 CSS 변수를 선언하는지 여부 미탐색 — 이중 선언 여부 불명
- `modal.jsp`가 실제로 어느 레이아웃 include 경로를 통해 로드되는지 미탐색 — BibimbapModal 미존재 상황의 실제 발생 가능성 미확인
- `game-detail.jsp` 외 admin-* 파일들의 날짜 렌더링 패턴 미탐색
- `BibimbapCsrf` 실제 구현체(별도 JS 파일) 미탐색 — `headers()` 메서드 폴백 동작 미확인
- `posts-form.jsp`의 `post()` 함수와 admin 4개 파일의 `post()` 함수가 실제로 다른 구현인지 정밀 비교 미수행

---

## 관련 파일

### 분석 대상 (주요)

| 파일 | 이슈 |
|---|---|
| `src/main/webapp/WEB-INF/views/recruit-form.jsp` | CSRF 폴백 갭 (P0-보안), 401 처리 불일치 |
| `src/main/webapp/WEB-INF/views/posts-form.jsp` | CSRF 기준 파일 (변경 없음) |
| `src/main/webapp/WEB-INF/views/admin-console.jsp` | CSS 토큰 중복, JS 함수 중복, notify 패턴 |
| `src/main/webapp/WEB-INF/views/admin-jam-list.jsp` | CSS 토큰 중복, JS 함수 중복, notify 패턴 |
| `src/main/webapp/WEB-INF/views/admin-post-categories.jsp` | CSS 토큰 중복, JS 함수 중복, window.confirm 직접 사용 |
| `src/main/webapp/WEB-INF/views/admin-unity-feeds.jsp` | CSS 토큰 중복, JS 함수 중복, window.confirm 직접 사용 |
| `src/main/webapp/WEB-INF/views/game-detail.jsp` | 날짜 함수 편재, 모달 패턴 혼재 |
| `src/main/webapp/WEB-INF/views/login.jsp` | openModal 패턴 A |
| `src/main/webapp/WEB-INF/views/signup.jsp` | openModal 패턴 A |
| `src/main/webapp/WEB-INF/views/game-register.jsp` | openModal 패턴 A |
| `src/main/webapp/WEB-INF/views/profile.jsp` | openModal 패턴 A |

### 신규 생성 예정 (권고안)

| 파일 | 역할 |
|---|---|
| `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` | fmtAbsolute / fmtRelative / buildTimeEl 날짜 포맷 유틸리티 |

### 관련 기존 분석

- [2026-06-16-project-analysis.md](./2026-06-16-project-analysis.md) — bibimbap 전면 코드 분석. D2 보안 섹션에서 CSRF 더블서밋 기초 확인. 이 문서의 recruit-form CSRF 갭은 해당 분석 이후 발견된 추가 이슈다.
