---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-30T02:41:09Z
concerns: []
source_confidence: high
workers_spawned: 5
---

# 프론트엔드 중복/이슈 분석

## 주제
JSP 기반 프론트엔드 코드에서 컴포넌트 중복 구현, 유지보수·확장성 이슈 조사

---

## 확정 중복 패턴 (이미 파악된 사실)

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

신뢰도: 확인됨 (입력 사실 보존)

### BibimbapModal.alert 체크 인라인 반복
- `window.BibimbapModal && typeof window.BibimbapModal.alert === 'function'` 패턴이 15개 이상 파일에 인라인 반복
- 신뢰도: 확인됨 (입력 사실 보존, 탐색 E에서 구체 위치 확인)

### CSRF 토큰 추출 방식 불일치
- admin-console/jam-scoring/jam-detail: `request.getAttribute("csrfToken")` rawCsrf 패턴
- admin-post-categories/admin-unity-feeds: `(String) request.getAttribute("csrfToken")` 캐스팅
- signup/login: `CsrfTokens.getOrCreate(request.getSession())` 직접 호출
- JS에서 일부는 `<meta name="_csrf">` 읽기, 일부는 `<%= csrfTokenJs %>` 직접 출력
- 신뢰도: 확인됨 (입력 사실 보존)

---

## CSS 중복 (탐색 A)

### 규모 요약
- 전체 27개 JSP 파일 중 26개에 인라인 `<style>` 블록 존재 (theme-init.jsp 제외)
- 누적 인라인 CSS: **5,969줄**
- 범위: footer.jsp 76줄 ~ game-detail.jsp 1,173줄
- 신뢰도: 확인됨

### 중복 패턴 1위 — `html {}` 라이트 테마 CSS 변수 선언 (23개 파일)
핵심 8개 색상 토큰(`--surface: #faf8f5`, `--card-bg: #fff`, `--text: #1a1a1a`, `--text-muted`, `--accent: #e8a54b`, `--border`, `--shadow`, `--accent-soft`)이 23개 파일에 동일 값으로 복사됨.
- 대표 위치: `admin-console.jsp:38`, `jam-list.jsp:21`, `login.jsp:12`
- 신뢰도: 확인됨

### 중복 패턴 2위 — `html[data-theme="dark"] {}` 다크 테마 CSS 변수 선언 (23개 파일)
다크 팔레트(`#121212`, `#1e1e1e`, `#ece8e1`, `#a39e96`, `rgba(255,255,255,0.1)`)가 23개 파일에 동일 반복.
- 대표 위치: `admin-console.jsp:51`, `posts-list.jsp:41`, `recruit-list.jsp:34`
- 신뢰도: 확인됨

### 중복 패턴 3위 — `body {}` 기본 리셋 + 폰트 + 배경 (23개 파일)
`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);` 블록이 23개 파일에 동일 반복.
- 대표 위치: `admin-console.jsp:62`, `jam-list.jsp:41`, `terms.jsp:29`
- 신뢰도: 확인됨

### 중복 패턴 4위 — 페이지 컨테이너 `safe-area` padding 패턴 (25개 파일)
`padding: 1.5rem max(1rem, env(safe-area-inset-left)) 3rem max(1rem, env(safe-area-inset-right));` 가 거의 모든 페이지에 반복. max-width 값만 48/56/64/72/78rem으로 파일별 상이.
- 대표 위치: `admin-console.jsp:70-72`, `posts-list.jsp:58-60`, `recruit-list.jsp:52-54`
- 신뢰도: 확인됨

### 중복 패턴 5위 — `.admin-btn` / `.admin-btn--primary` / `.admin-btn--danger` (4개 admin 파일)
버튼 컴포넌트 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`
- 신뢰도: 확인됨

### 중복 패턴 6위 — `.detail-button` / `.detail-actions` / `.detail-section` (2개 파일)
- `jam-detail.jsp:128`, `recruit-detail.jsp:116` — 거의 동일 (color 값 1곳만 다름: `#1a1a1a` vs `var(--button-text)`)
- 신뢰도: 확인됨

### CSS Grid 리스트 래퍼 패턴 (posts·recruit 동일)
- `posts-list.jsp:119-123` — `.posts-grid { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:1rem; }`
- `recruit-list.jsp:143-147` — `.recruit-grid { ... }` 동일 구조
- 신뢰도: 확인됨

---

## 리스트 페이지 패턴 (탐색 B)

### 카드 HTML 구조 비교
| 항목 | posts-list.jsp | recruit-list.jsp | jam-list.jsp |
|---|---|---|---|
| 래퍼 클래스 | `.posts-grid` (section) | `.recruit-grid` (section) | `.jam-grid` (div) |
| Grid 열 | `repeat(3, minmax(0,1fr))` | `repeat(3, minmax(0,1fr))` | `repeat(auto-fill, minmax(16rem,1fr))` |
| 카드 태그 | `<a class="post-card">` | `<a class="recruit-card">` | `<a class="jam-card">` |
| 이미지 | 있음 (16/9 aspect-ratio) | 없음 | 없음 |
| 데이터 속성 | 없음 | `data-role`, `data-type`, `data-search` | 없음 |

신뢰도: 확인됨

### 검색/필터 방식 비교
- **posts-list.jsp** — 검색 UI 없음. 카테고리 탭(`<nav class="posts-tabs">`)은 `<a>` 링크 방식으로 서버 재요청(`?categoryId=`). JS 없음.
- **recruit-list.jsp** — `<input type="search" id="recruit-search">` (라인 261) + 역할·참여형태 `<button class="filter-chip">` (aria-pressed 토글). `applyFilters()` 함수가 `card.hidden`으로 클라이언트 필터 (라인 328-340). 검색 텍스트는 서버가 `data-search` 속성에 pre-encode.
- **jam-list.jsp** — 검색/필터 모두 없음.
- 신뢰도: 확인됨

### 페이지네이션 방식 비교
- **posts-list.jsp** — 커서 기반 "더보기" 링크. 서버 Java가 `cursorCreatedAt`, `cursorId` 파라미터를 URL로 조립 (라인 263-270). JS 없음.
- **recruit-list.jsp** — 페이지네이션 없음. 전체 목록 한 번에 렌더.
- **jam-list.jsp** — 커서 기반 "더보기" 링크. 서버 Java가 `cursor` 파라미터 URL 조립 (라인 177-180). JS 없음.
- 신뢰도: 확인됨

### 공통 중복 블록
- CSS 변수 + body 스타일 블록: 세 파일 모두 동일 (상위 CSS 중복 패턴과 동일)
- 히어로 섹션 flex 레이아웃: posts·recruit 동일 (`align-items:flex-end; justify-content:space-between; gap:1rem`)
- 쓰기 버튼 스타일: posts·recruit 동일 (`min-height:2.75rem; border-radius:10px; background:var(--accent)`)
- 카드 hover 효과: posts·recruit 동일, jam은 transform 없이 border-color만
- empty-state 박스: 세 파일 유사 (border-style만 dashed vs solid 차이)
- 반응형 미디어 쿼리(3열→2열→1열): posts·recruit 동일 (@media 900px/640px)
- `request.getContextPath()` + 목록 attribute 캐스팅 패턴: 세 파일 공통
- `<jsp:include page="/WEB-INF/views/header.jsp"/>` / `footer.jsp`: 세 파일 동일
- 신뢰도: 확인됨

---

## 폼 페이지 패턴 (탐색 C)

### 공통 골격 (거의 복사-붙여넣기 수준)
두 파일은 fetch 기반 비동기 폼 제출, `checkValidity()` 검증, `BibimbapModal.alert` 성공/실패 처리, `BibimbapCsrf` CSRF 헤더 주입이라는 동일한 골격을 공유.

| 항목 | posts-form.jsp | recruit-form.jsp |
|---|---|---|
| body 구성 | `new URLSearchParams(new FormData(form))` (라인 232) | 동일 (라인 383) |
| Content-Type | `application/x-www-form-urlencoded;charset=UTF-8` (라인 225) | 동일 (라인 387) |
| 클라이언트 검증 | `form.checkValidity()` + `form.reportValidity()` (라인 219-222) | 동일 (라인 378-381) |
| res.ok 판정 | `if (!res.ok) { var error = new Error(...); error.status = res.status; throw error; }` (라인 241-246) | 동일 (라인 400-404) |
| 성공 리다이렉트 | `window.location.href = ctx + (data.location \|\| '/posts')` (라인 250) | `window.location.href = '<%= ctx %>' + (data.location \|\| '/recruit')` (라인 409) |

신뢰도: 확인됨

### 차별화 부분 (다른 점)
1. **CSRF 처리 방식 불일치 (High)**
   - `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` 미존재 시 fallback 헤더에 CSRF 토큰이 전혀 포함되지 않는 보안 갭
   - 신뢰도: 확인됨

2. **성공 시 모달 없을 때 분기 차이**
   - `posts-form.jsp:260-261` — `} else { go(); }` (alert 없음)
   - `recruit-form.jsp:419-421` — `} else { alert('팀원 모집글이 등록되었습니다.'); go(); }`
   - 신뢰도: 확인됨

3. **401 리다이렉트 처리 패턴 불일치**
   - `posts-form.jsp:264-268` — `redirectLogin` 함수 분리, `onConfirm`에서 호출
   - `recruit-form.jsp:430-431, 437-439` — 모달 `onConfirm`과 else 분기 양쪽에 각각 인라인 중복 작성
   - 신뢰도: 확인됨

4. **수정 모드**: `posts-form.jsp:9-10, 215` — `mode`, `postId` 로 등록/수정 URL 분기. `recruit-form.jsp`는 수정 모드 분기 없음.
5. **실시간 미리보기**: `recruit-form.jsp:340-373` — `pairs` 배열 + `render()` + input/change 이벤트로 우측 preview-card 갱신. posts-form에는 없음.
6. **제출 버튼 disabled 처리**: 두 파일 모두 없음. fetch 중 비활성화 처리 미구현.

---

## 날짜 함수 (탐색 D)

### 발견 요약
- **날짜/시간 포맷 JavaScript 함수가 선언된 파일**: `game-detail.jsp` 단 하나
- 나머지 6개 파일(posts-detail, recruit-detail, jam-detail, posts-list, jam-list, recruit-list)에는 날짜 포맷 JS 함수가 전혀 없음
- 신뢰도: 확인됨

### game-detail.jsp 날짜 함수 (라인 1516-1551)
| 함수 | 위치 | 구현 방식 |
|---|---|---|
| `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>` 요소 + "(수정됨)" 뱃지 반환 |

- `Intl.RelativeTimeFormat` / `Intl.DateTimeFormat` 직접 사용: 조사 범위 7개 파일 전체에서 **0 hit**
- 신뢰도: 확인됨

### 사용 위치
- `game-detail.jsp:1878` — `buildTimeEl` 사용 (댓글 항목)
- `game-detail.jsp:2183-2186` — `fmtAbsolute`/`fmtRelative` 직접 사용 (리뷰 항목)
- 신뢰도: 확인됨

### 서버사이드 날짜 포매팅
- 조사 범위 내 Java Date 포매팅으로 화면 출력하는 경우: 0건
- `posts-list.jsp:6-7,19` — `OffsetDateTime` import하지만 커서 URL 생성에만 사용 (화면 출력 아님)
- `jam-detail.jsp:442` — `OffsetDateTime.now().isAfter(...)` 서버사이드 분기 조건 (화면 포맷 출력 아님)
- 신뢰도: 확인됨

### 구조적 위험
`fmtAbsolute`/`fmtRelative`/`buildTimeEl`이 game-detail.jsp 인라인 스크립트에만 존재 → 다른 detail 페이지가 동일 기능이 필요할 때 복사 중복 발생 구조.

---

## 모달 패턴 (탐색 E)

### BibimbapModal API (modal.jsp:174-238)
| 메서드 | 시그니처 | 설명 |
|---|---|---|
| `alert(options)` | `{ title, message, confirmText, onConfirm }` | 취소 버튼 없음, 확인 버튼만 |
| `confirm(options)` | `{ title, message, confirmText, cancelText, onConfirm, onCancel }` | 확인+취소 양쪽 콜백 |
| `prompt(options)` | `{ title, message, label, value, placeholder, maxLength, confirmText, cancelText, onConfirm(inputValue), onCancel }` | 텍스트 입력 필드 포함 |

- 구현 방식: 단일 `<div id="site-modal">` 재사용, 콜백 기반 (Promise 아님), Enter=확인/Escape=취소 키보드 처리, 포커스 복귀
- 신뢰도: 확인됨

### 모달 사용 패턴 분류 (12개 JSP, modal.jsp 제외)

**패턴 A — `openModal()` 헬퍼 래퍼 (4개 파일, High 중복)**
`login.jsp`, `signup.jsp`, `game-register.jsp`, `profile.jsp`가 동일 시그니처의 `openModal(title, message, confirmText, onConfirm)` 로컬 함수를 각자 정의.
- 내부 BibimbapModal 체크 후 폴백 `alert(message)` + 동기 `onConfirm()` 호출
- 위치: `login.jsp:256`, `signup.jsp:269`, `game-register.jsp:490`, `profile.jsp:539`
- 신뢰도: 확인됨

**패턴 B — `notify()` 헬퍼 래퍼 (4개 admin 파일, High 중복)**
`admin-console.jsp`, `admin-jam-list.jsp`, `admin-post-categories.jsp`, `admin-unity-feeds.jsp`가 `notify(message)` 로컬 함수를 각자 정의 (alert 전용, title은 하드코딩된 페이지명).
- 위치: `admin-jam-list.jsp:389`, `admin-console.jsp:401`, `admin-post-categories.jsp:251`, `admin-unity-feeds.jsp:305`
- 신뢰도: 확인됨

**패턴 C — 인라인 직접 체크 (3개 파일)**
`recruit-form.jsp`, `posts-form.jsp`, `game-detail.jsp`가 호출 지점마다 인라인으로 체크.
- `recruit-form.jsp:411, 424`, `posts-form.jsp:252, 269`, `game-detail.jsp:1425, 1439, 1474, 1482, 1553, 1558`
- 신뢰도: 확인됨

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

### window.confirm/alert 직접 사용 (BibimbapModal 없이)
| 종류 | 위치 | 비고 |
|---|---|---|
| `window.confirm()` 직접 호출 (체크 없음) | `admin-post-categories.jsp:323` | 삭제 확인 대화상자 |
| `window.confirm()` 직접 호출 (체크 없음) | `admin-unity-feeds.jsp:401` | 삭제 확인 대화상자 |
| `window.alert()` 폴백 (else 브랜치) | 전 파일의 BibimbapModal 없을 때 else 브랜치 | 정상 폴백 |
| `window.prompt()` 폴백 | `profile.jsp:686` | BibimbapModal.prompt 실패 시 |

- `admin-post-categories.jsp`와 `admin-unity-feeds.jsp`는 알림에는 `notify()` 헬퍼(BibimbapModal 체크)를 쓰면서 삭제 확인에는 `window.confirm()`을 직접 사용하여 동일 파일 내 불일치 존재
- 신뢰도: 확인됨

---

## 종합 판단

### 상위 패턴
1. **CSS 인라인 토큰 중복이 가장 심각**: 라이트/다크 테마 CSS 변수 블록과 `body` 기본 스타일이 23개 파일에 동일하게 복사됨. 디자인 토큰 변경 시 23개 파일을 모두 수정해야 하는 유지보수 고비용 구조. `theme-init.jsp`가 존재하나 CSS 변수는 여기에 없고 JS 테마 초기화만 담당하는 것으로 보임.

2. **JS 유틸리티 함수 인라인 중복 이중 구조**: `notify()` (admin 4개), `openModal()` (일반 4개), `post(url, params)` (admin 4개) 등이 페이지마다 별도 선언됨. 특히 `post()` 함수는 파일마다 구현이 미묘하게 달라 동작 불일치 위험 내재.

3. **BibimbapModal 체크 패턴 3가지 혼존**: 헬퍼 래퍼 A, 헬퍼 래퍼 B, 인라인 직접 체크 C가 서로 다른 파일에 혼재. 일관성 없고 신규 페이지 작성 시 어떤 패턴을 따라야 할지 기준 없음.

4. **폼 제출 골격 복사**: posts-form과 recruit-form이 fetch 제출 코드를 거의 복사 수준으로 공유하면서 CSRF 처리 방식에 불일치 존재 (recruit-form의 BibimbapCsrf 미존재 시 CSRF 토큰 누락 위험).

5. **날짜 포맷 함수 편재**: game-detail.jsp에만 날짜 함수가 존재하며 다른 detail 페이지는 날짜 렌더링 자체가 없거나 JS 미사용. 추후 다른 페이지에서 날짜 포맷이 필요해지면 복사 중복 발생 구조.

### 충돌/갭
- CSS 변수 선언이 파일마다 포함하는 변수 수가 8~14개로 약간씩 달라, 특정 파일에서 일부 변수가 미선언된 상태일 수 있음.
- `admin-post-categories.jsp`와 `admin-unity-feeds.jsp`는 알림에는 BibimbapModal 체크를 쓰면서 삭제 확인은 `window.confirm()` 직접 사용 — 같은 파일 내 불일치.
- `jam-detail.jsp:128`과 `recruit-detail.jsp:116`의 `.detail-button--primary color` 값 불일치 (`#1a1a1a` vs `var(--button-text)`) — 디자인 일관성 갭.

---

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

- `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()` 함수가 실제로 다른 구현인지 정밀 비교 미수행
