diff --git a/.gitignore b/.gitignore
index e9d027930..218c29a34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,8 +5,5 @@
.env
-CLAUDE.md
-.claude
-
dailyNote/
diff --git a/docs/experiments.md b/docs/experiments.md
new file mode 100644
index 000000000..9e31319f8
--- /dev/null
+++ b/docs/experiments.md
@@ -0,0 +1,121 @@
+# 프론트엔드 실험(A/B Test) 시스템 가이드
+
+## 개요
+
+`frontend/src/experiments/`는 클라이언트 사이드 A/B 실험을 위한 경량 구조다.
+외부 서비스 없이 localStorage 기반으로 배리언트를 배정하고, React 훅으로 컴포넌트에서 간편하게 사용할 수 있다.
+
+## 파일 구성
+
+```
+frontend/src/experiments/
+├── types.ts # 실험 관련 타입 정의
+├── definitions.ts # 실험 목록 정의 (여기만 수정하면 됨)
+├── ExperimentRepository.ts # 배리언트 배정 및 조회 로직
+└── initializeExperiments.ts # 앱 시작 시 일괄 배정
+
+frontend/src/hooks/Experiment/
+└── useExperimentVariant.ts # 컴포넌트에서 사용하는 React 훅
+```
+
+## 동작 원리
+
+1. **앱 시작 시** `initializeExperiments()`가 `ALL_EXPERIMENTS`를 순회하며 각 실험의 배리언트를 배정한다.
+2. 이미 배정된 실험은 재배정하지 않는다. (같은 유저는 항상 같은 배리언트를 본다)
+3. 배정 결과는 `localStorage`의 `moadong_experiments` 키에 JSON으로 저장된다.
+4. 컴포넌트에서는 `useExperimentVariant` 훅으로 배리언트를 읽어 분기한다.
+
+## 새 실험 추가 방법 (3단계)
+
+### 1단계 — `definitions.ts`에 실험 정의 추가
+
+```typescript
+// frontend/src/experiments/definitions.ts
+
+export const myNewExperiment = {
+ key: "my_new_experiment_v1", // 전역 고유값. 중복 금지
+ variants: ["A", "B"] as const,
+ defaultVariant: "A", // 배정 실패 또는 초기화 시 기본값
+ weights: {
+ A: 50,
+ B: 50,
+ },
+} satisfies ExperimentDefinition<"A" | "B">;
+
+// ALL_EXPERIMENTS 배열에도 추가
+export const ALL_EXPERIMENTS = [
+ mainBannerExperiment,
+ applyButtonCopyExperiment,
+ myNewExperiment, // ← 여기
+] as const;
+```
+
+### 2단계 — 컴포넌트에서 배리언트 분기
+
+```typescript
+import { useExperimentVariant } from '@/hooks/Experiment/useExperimentVariant';
+import { myNewExperiment } from '@/experiments/definitions';
+
+const MyComponent = () => {
+ const variant = useExperimentVariant(myNewExperiment);
+
+ return variant === 'B' ? : ;
+};
+```
+
+### 3단계 — Mixpanel 이벤트에 배리언트 속성 포함 (권장)
+
+실험 결과를 분석하려면 이벤트 전송 시 배리언트 값을 속성으로 넘겨야 한다.
+
+```typescript
+import { trackEvent } from "@/utils/mixpanel"; // 실제 트래킹 유틸 경로 참고
+
+trackEvent("클릭 이벤트", {
+ experiment_key: myNewExperiment.key,
+ experiment_variant: variant,
+});
+```
+
+## weights(가중치) 설정
+
+`weights`를 생략하면 균등 배분된다.
+비율을 조정하고 싶을 때만 명시한다.
+
+```typescript
+// 10%만 B를 보는 실험
+weights: {
+ A: 90,
+ B: 10,
+},
+```
+
+## 배리언트 초기화 (개발/QA용)
+
+브라우저 콘솔에서 실행하면 배정이 초기화된다.
+
+```javascript
+localStorage.removeItem("moadong_experiments");
+location.reload();
+```
+
+또는 코드에서 직접 호출:
+
+```typescript
+import { experimentRepository } from "@/experiments/ExperimentRepository";
+experimentRepository.resetAssignments();
+```
+
+## key 네이밍 규칙
+
+- 형식: `{실험_대상}_{버전}` (소문자 snake_case)
+- 예시: `main_banner_v1`, `apply_button_copy_v1`
+- 실험이 종료되고 새로 시작할 때는 버전을 올린다: `main_banner_v2`
+- 이미 배포된 key는 절대 재사용하지 않는다. (기존 유저 배정 오염 방지)
+
+## 실험 종료 후 정리
+
+1. `definitions.ts`에서 해당 실험 상수와 `ALL_EXPERIMENTS` 항목을 제거한다.
+2. 채택된 배리언트 코드만 남기고 분기 로직을 제거한다.
+3. 버려진 배리언트 코드를 삭제한다.
+
+## 현재 운영 중인 실험
diff --git a/docs/mixpanel-admin-weekly-report-prompts.md b/docs/mixpanel-admin-weekly-report-prompts.md
new file mode 100644
index 000000000..9c7b6a636
--- /dev/null
+++ b/docs/mixpanel-admin-weekly-report-prompts.md
@@ -0,0 +1,96 @@
+# Mixpanel MCP 관리자 주간 리포트 고정 프롬프트 세트
+
+## 목적
+
+- 관리자(Admin) 사용 흐름을 주 단위로 모니터링한다.
+- 설정/수정 관련 행동 데이터를 표준 템플릿으로 축적한다.
+
+## 운영 원칙
+
+- 기간은 항상 `지난주 (월요일 00:00 ~ 일요일 23:59, KST)`로 고정한다.
+- 비교 기준은 항상 `직전 주`로 고정한다.
+- 관리자 이벤트에는 "클릭" 위주 이벤트가 많으므로, 저장 성공/실패는 별도 백엔드 로그와 함께 해석한다.
+- 결과는 아래 "리포트 템플릿" 순서로 정리한다.
+
+## 고정 프롬프트 8개 (moadong ADMIN_EVENT 치환)
+
+1. `지난주(월~일, Asia/Seoul) 관리자 핵심 KPI를 보여줘. 다음 이벤트를 기준으로 유저 수와 이벤트 수를 각각 보여줘: "로그인페이지 Visited", "로그인 버튼클릭", "사이드바 탭 클릭", "동아리 기본 정보 수정 버튼클릭", "동아리 모집 정보 수정 버튼클릭", "활동 사진 업로드 버튼클릭", "비밀번호 변경 버튼클릭". 직전 주 대비 증감률(%) 포함.`
+
+2. `지난주 관리자 진입 퍼널을 보여줘: "로그인페이지 Visited" -> "로그인 버튼클릭" -> "동아리 기본 정보 수정 페이지 Visited"(또는 다른 관리자 탭 Visited). 단계별 전환율/이탈률과 직전 주 대비 변화를 계산해줘.`
+
+3. `지난주 사이드바 탭 이동 패턴을 분석해줘. "사이드바 탭 클릭" 이벤트의 tab_name 속성 기준으로 탭별 클릭 수, 유저 수, 전주 대비 증감을 표로 보여줘.`
+
+4. `지난주 동아리 기본 정보 수정 관련 행동을 요약해줘: "동아리 기본 정보 수정 버튼클릭", "분류/분과/자유태그 선택 버튼클릭", "자유태그 입력 초기화 버튼클릭", "SNS 링크 입력 초기화 버튼클릭". 이벤트별 유저 수/이벤트 수와 전주 대비 변화 포함.`
+
+5. `지난주 모집 정보 수정 관련 행동을 요약해줘: "동아리 모집 정보 수정 버튼클릭", "상시모집 버튼클릭", "모집 시작 날짜 변경", "모집 종료 날짜 변경", "모집 대상 입력 초기화 버튼클릭", "소개글 미리보기/편집 버튼클릭". 이벤트별 추세와 함께 보여줘.`
+
+6. `지난주 이미지 자산 편집 행동을 비교해줘: "동아리 커버 업로드 버튼클릭", "동아리 커버 초기화 버튼클릭", "동아리 로고 업로드 버튼클릭", "동아리 로고 초기화 버튼클릭", "활동 사진 업로드 버튼클릭", "활동 사진 삭제 버튼클릭". 업로드/초기화/삭제 비율도 계산해줘.`
+
+7. `지난주 계정 보안 관련 행동을 보여줘: "비밀번호 변경 버튼클릭", "새 비밀번호 입력 초기화 버튼클릭", "확인 비밀번호 입력 초기화 버튼클릭". 전주 대비 증감과 이상 패턴(초기화 클릭 과다 등)을 코멘트해줘.`
+
+8. `위 1~7 결과를 바탕으로 이번 주 관리자 UX 개선 액션 3개를 제안해줘. 각 액션마다 목표 KPI, 기대효과, 검증방법(A/B 또는 관찰지표), 우선순위를 포함해줘.`
+
+## 리포트 템플릿 (복붙용)
+
+```md
+# 관리자 주간 리포트 (YYYY-W##)
+
+## 1) 한 줄 요약
+
+-
+
+## 2) KPI 스냅샷 (지난주 vs 직전 주)
+
+- 로그인페이지 Visited (users/events):
+- 로그인 버튼클릭 (users/events):
+- 사이드바 탭 클릭 (users/events):
+- 동아리 기본 정보 수정 버튼클릭 (users/events):
+- 동아리 모집 정보 수정 버튼클릭 (users/events):
+- 활동 사진 업로드 버튼클릭 (users/events):
+- 비밀번호 변경 버튼클릭 (users/events):
+
+## 3) 핵심 변화 3가지
+
+1.
+2.
+3.
+
+## 4) 원인 가설
+
+-
+
+## 5) 이번 주 액션 3개
+
+1. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+2. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+3. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+
+## 6) 리스크 / 확인 필요
+
+-
+```
+
+## 커스터마이즈 체크리스트
+
+- 실제 운영 탭 이름 확인: `사이드바 탭 클릭`의 `tab_name` 속성 값
+- 수정 성공 지표 보강 여부 결정: 클릭 이벤트 외 백엔드 성공 로그 연동
+- 관리자 계정 식별 기준 확정 (distinct_id 또는 사용자 속성)
+- 페이지 체류 지표 활용 여부 결정 (`로그인페이지 Duration`, `동아리 기본 정보 수정 페이지 Duration` 등)
+
+## 실행 순서 권장
+
+1. 프롬프트 1~7 실행 후 사실 데이터 확정
+2. 프롬프트 8 실행으로 액션 도출
+3. 템플릿에 결과 이관 후 팀 공유
diff --git a/docs/mixpanel-reporting.md b/docs/mixpanel-reporting.md
new file mode 100644
index 000000000..1e2c9663e
--- /dev/null
+++ b/docs/mixpanel-reporting.md
@@ -0,0 +1,36 @@
+# Mixpanel 리포팅 문서 인덱스
+
+## 개요
+
+moadong에서 Mixpanel MCP로 주간 리포트를 만들 때 사용하는 문서 모음이다.
+
+## 문서 바로가기
+
+- 사용자(학생) 흐름 리포트: [mixpanel-weekly-report-prompts.md](./mixpanel-weekly-report-prompts.md)
+- 관리자(Admin) 흐름 리포트: [mixpanel-admin-weekly-report-prompts.md](./mixpanel-admin-weekly-report-prompts.md)
+
+## 최근 주간 리포트
+
+- 2026-W12 사용자 리포트: [2026-W12-user-mixpanel-report.md](./weekly-reports/2026-W12-user-mixpanel-report.md)
+
+## 주간 실행 순서
+
+1. Mixpanel MCP 연결 상태 확인
+ - `codex mcp list`
+2. 사용자 리포트 실행
+ - `docs/mixpanel-weekly-report-prompts.md`의 고정 프롬프트 1~8 순서대로 실행
+3. 관리자 리포트 실행
+ - `docs/mixpanel-admin-weekly-report-prompts.md`의 고정 프롬프트 1~8 순서대로 실행
+4. 각 템플릿에 결과 이관 후 팀 공유
+
+## 운영 규칙
+
+- 기간 기준 고정: `지난주 (월요일 00:00 ~ 일요일 23:59, KST)`
+- 비교 기준 고정: `직전 주`
+- 이벤트 누락/속성 불일치 시 추정하지 않고 리스크로 명시
+
+## 업데이트 규칙
+
+- 신규 이벤트 추가 시, 먼저 `frontend/src/constants/eventName.ts`를 기준으로 이벤트명을 확인한다.
+- 이벤트/속성 변경 시 사용자 문서와 관리자 문서를 함께 갱신한다.
+- 퍼널 정의가 바뀌면 템플릿 KPI 항목도 함께 수정한다.
diff --git a/docs/mixpanel-weekly-report-prompts.md b/docs/mixpanel-weekly-report-prompts.md
new file mode 100644
index 000000000..27e06b23a
--- /dev/null
+++ b/docs/mixpanel-weekly-report-prompts.md
@@ -0,0 +1,95 @@
+# Mixpanel MCP 주간 리포트 고정 프롬프트 세트
+
+## 목적
+
+- 매주 같은 기준으로 KPI를 조회하고, 비교와 해석을 일관되게 남긴다.
+- 리포트 품질을 사람 숙련도에 의존하지 않고 템플릿으로 표준화한다.
+
+## 운영 원칙
+
+- 기간은 항상 `지난주 (월요일 00:00 ~ 일요일 23:59, KST)`로 고정한다.
+- 비교 기준은 항상 `직전 주`로 고정한다.
+- 지표가 비어 있거나 이벤트명이 불일치하면, 추정하지 말고 누락 항목으로 명시한다.
+- 결과는 아래 "리포트 템플릿" 순서로 정리한다.
+
+## 고정 프롬프트 8개 (moadong 이벤트명 1차 치환)
+
+1. `지난주(월~일, Asia/Seoul) 핵심 KPI를 보여줘. 이벤트는 정확히 다음 기준으로 집계해줘: "MainPage Visited"(유저 수), "ClubDetailPage Visited"(유저 수), "ApplicationFormPage Visited"(유저 수), "Application Form Submitted"(이벤트 수/유저 수). 직전 주 대비 증감률(%)도 포함해줘.`
+
+2. `지난주(월~일, Asia/Seoul) 지원 퍼널 전환율을 보여줘: "MainPage Visited" -> "ClubCard Clicked" -> "ClubDetailPage Visited" -> "Club Apply Button Clicked" -> "ApplicationFormPage Visited" -> "Application Form Submitted". 각 단계 전환율/이탈률과 직전 주 대비 변화를 함께 보여줘.`
+
+3. `지난주 "ClubCard Clicked" 사용자 cohort 기준 D1, D7 재방문율을 보여줘. 재방문 기준 이벤트는 "MainPage Visited"로 계산하고, 최근 4주 추세를 표로 정리해줘.`
+
+4. `지난주 유입 채널 성과를 비교해줘. "MainPage Visited"의 referrer 속성 기준으로 채널을 나누고, 각 채널별 "ClubCard Clicked", "Club Apply Button Clicked", "Application Form Submitted" 전환율을 계산해줘.`
+
+5. `지난주 디바이스/플랫폼별 성과를 비교해줘. Mixpanel 기본 디바이스 속성($os, $browser)을 사용해서 "MainPage Visited" -> "Application Form Submitted" 전환율을 iOS/Android/Web 기준으로 정리해줘.`
+
+6. `직전 주 대비 "Application Form Submitted" 변화 원인을 이벤트 기여도로 분해해줘. 후보 이벤트는 "MainPage Visited", "ClubCard Clicked", "Club Apply Button Clicked", "ApplicationFormPage Visited"로 제한하고 증가/감소 기여 Top 5를 해석해줘.`
+
+7. `지난주 이탈 구간을 진단해줘. "Club Apply Button Clicked" 대비 "ApplicationFormPage Visited", "ApplicationFormPage Visited" 대비 "Application Form Submitted" 전환율을 클럽별(club_id 또는 club_name 속성)로 비교해서 하위 10개를 보여줘.`
+
+8. `위 1~7 결과를 기반으로 이번 주 실행 액션 3개를 제안해줘. 각 액션마다 목표 KPI(예: "Application Form Submitted" 유저 수), 기대효과, 검증방법(A/B 또는 관찰지표), 우선순위를 포함해줘.`
+
+## 리포트 템플릿 (복붙용)
+
+```md
+# 주간 리포트 (YYYY-W##)
+
+## 1) 한 줄 요약
+
+-
+
+## 2) KPI 스냅샷 (지난주 vs 직전 주)
+
+- MainPage Visited (users):
+- ClubDetailPage Visited (users):
+- ApplicationFormPage Visited (users):
+- Application Form Submitted (events/users):
+- D1/D7 재방문율 (ClubCard Clicked cohort):
+
+## 3) 핵심 변화 3가지
+
+1.
+2.
+3.
+
+## 4) 원인 가설
+
+-
+
+## 5) 이번 주 액션 3개
+
+1. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+2. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+3. 액션:
+ 목표 KPI:
+ 기대효과:
+ 검증방법:
+ 우선순위:
+
+## 6) 리스크 / 확인 필요
+
+-
+```
+
+## 커스터마이즈 체크리스트
+
+- 사용자 핵심 이벤트 확정: `MainPage Visited`, `ClubCard Clicked`, `ClubDetailPage Visited`, `Club Apply Button Clicked`, `ApplicationFormPage Visited`, `Application Form Submitted`
+- KPI 정의를 팀 문서와 일치시킴 (예: 제출 KPI를 이벤트 수로 볼지 유저 수로 볼지)
+- 채널 분류 기준 확정 (`referrer` 우선, 필요 시 UTM 속성 추가 수집)
+- 플랫폼 구분 기준 확정 (`$os`, `$browser` 사용 여부)
+- 관리자 리포트를 별도로 운영할지 결정 (`로그인 버튼클릭`, `동아리 모집 정보 수정 버튼클릭` 등 ADMIN_EVENT 기반)
+
+## 실행 순서 권장
+
+1. 프롬프트 1~7 실행 후 사실 데이터 확정
+2. 프롬프트 8 실행으로 액션 도출
+3. 템플릿에 결과 이관 후 팀 공유
diff --git a/docs/weekly-reports/2026-W12-user-mixpanel-report.md b/docs/weekly-reports/2026-W12-user-mixpanel-report.md
new file mode 100644
index 000000000..d6eda2759
--- /dev/null
+++ b/docs/weekly-reports/2026-W12-user-mixpanel-report.md
@@ -0,0 +1,49 @@
+# 주간 리포트 (2026-W12)
+
+## 1) 한 줄 요약
+
+- 트래픽(Main/Detail) 감소와 중후반 퍼널(Detail→Apply, Apply→Form) 전환 저하가 겹치며 제출 유저가 전주 대비 크게 감소했다.
+
+## 2) KPI 스냅샷 (지난주 vs 직전 주)
+
+- MainPage Visited (users): `822` vs `1,287` (`-36.1%`)
+- ClubDetailPage Visited (users): `531` vs `893` (`-40.5%`)
+- ApplicationFormPage Visited (users): `8` vs `55` (`-85.5%`)
+- Application Form Submitted (events/users): `11 / 3` vs `34 / 33` (`-67.6% / -90.9%`)
+- D1/D7 재방문율 (ClubCard Clicked cohort): `36% / 6%` (4주 추세 하락)
+
+## 3) 핵심 변화 3가지
+
+1. 퍼널 절대 볼륨 하락: `Main 822 → Card 483 → Detail 133 → Apply 18 → Form 2 → Submit 1`
+2. 단계 전환 하락: `Detail→Apply 14%`(전주 `19%`), `Apply→Form 11%`(전주 `18%`)
+3. 제출 표본 급감: 제출 유저 `33 → 3`로 감소, 채널/플랫폼별 해석 표본 부족
+
+## 4) 원인 가설
+
+- 상단 유입 감소(Main/Detail 동반 하락) + 지원 직전 구간 UX/정보 전달 부족이 동시 발생.
+- 이벤트 스키마 결측(`Club Apply Button Clicked`의 `club_id/club_name=undefined`)으로 클럽별 병목 탐지가 막혀, 최적화 속도가 느려짐.
+- 리텐션 추세(최근 4주) 하락으로 재방문 모수 자체가 줄어든 상태.
+
+## 5) 이번 주 액션 3개
+
+1. 액션: `Club Apply Button Clicked`에 `club_id`, `club_name` 필수 전송
+ 목표 KPI: 클럽별 `Apply→Form` 관측 가능률 `100%`
+ 기대효과: 병목 클럽 정확 식별
+ 검증방법: 다음 주 리포트에서 `undefined` 비중 0% 확인
+ 우선순위: `P0`
+2. 액션: ClubDetail 지원 CTA/가이드 개선(마감, 지원 조건, 외부폼 안내 즉시 노출)
+ 목표 KPI: `Detail→Apply` 전환율 `+3%p`
+ 기대효과: 중간 퍼널 회복
+ 검증방법: A/B 테스트 또는 주차 비교
+ 우선순위: `P1`
+3. 액션: ApplicationForm 이탈 방지(필수문항 즉시 표시, 에러메시지 개선, 임시저장 명확화)
+ 목표 KPI: `Apply→Form`, `Form→Submit` 개선
+ 기대효과: 제출 유저 회복
+ 검증방법: 단계 전환율 주간 추적
+ 우선순위: `P1`
+
+## 6) 리스크 / 확인 필요
+
+- 채널 분석은 `undefined` 비중이 높아 정밀 해석 제한.
+- 플랫폼별 제출 표본이 3명으로 매우 작아 결론 신뢰도 낮음.
+- `Apply→Form` 클럽별 하위 10은 이벤트 속성 누락으로 산출 불가 (우선 스키마 수정 필요).
diff --git "a/frontend/.claude/agents/API\355\233\205\353\266\200\354\204\234.md" "b/frontend/.claude/agents/API\355\233\205\353\266\200\354\204\234.md"
new file mode 100644
index 000000000..66057f240
--- /dev/null
+++ "b/frontend/.claude/agents/API\355\233\205\353\266\200\354\204\234.md"
@@ -0,0 +1,180 @@
+# API Hooks Agent
+
+React Query 기반 API 훅 생성 및 관리 전담 에이전트
+
+## 역할
+
+- React Query 훅 생성 및 수정
+- API 레이어와 훅 레이어 간 일관성 유지
+- 쿼리 키 관리 및 캐싱 전략 구현
+
+## 작업 프로세스
+
+### 1. 새로운 API 훅 생성 시
+
+1. **API 함수 확인**
+ - `src/apis/` 디렉토리에서 해당 도메인의 API 함수 확인
+ - API 함수가 없으면 먼저 생성 필요
+
+2. **쿼리 키 등록**
+ - `src/constants/queryKeys.ts`에 쿼리 키 추가
+ - 네이밍 컨벤션: `도메인.액션` 형식 (예: `club.list`, `application.detail`)
+
+3. **훅 파일 생성**
+ - `src/hooks/Queries/use도메인명.ts` 형식으로 생성
+ - 도메인별로 파일 분리
+
+4. **훅 구현**
+ - `useQuery` / `useMutation` 사용
+ - 에러 핸들링 포함
+ - 타입 안전성 보장
+
+### 2. API 레이어 패턴
+
+**모든 API 함수는 `apiHelpers.ts`의 헬퍼 사용:**
+
+```typescript
+import {
+ handleResponse,
+ secureFetch,
+ withErrorHandling,
+} from '@/apis/utils/apiHelpers';
+
+// GET 요청
+export const getClubs = withErrorHandling(async (): Promise => {
+ const response = await secureFetch(`${BASE_URL}/clubs`);
+ return handleResponse(response);
+});
+
+// POST 요청
+export const createClub = withErrorHandling(
+ async (data: CreateClubRequest): Promise => {
+ const response = await secureFetch(`${BASE_URL}/clubs`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+ return handleResponse(response);
+ },
+);
+```
+
+### 3. React Query 훅 패턴
+
+**Query 훅:**
+
+```typescript
+import { useQuery } from '@tanstack/react-query';
+import { getClubs } from '@/apis/club/clubApi';
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useClubs = () => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.club.list],
+ queryFn: getClubs,
+ staleTime: 5 * 60 * 1000, // 5분
+ gcTime: 10 * 60 * 1000, // 10분
+ });
+};
+```
+
+**Mutation 훅:**
+
+```typescript
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createClub } from '@/apis/club/clubApi';
+import { QUERY_KEYS } from '@/constants/queryKeys';
+
+export const useCreateClub = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createClub,
+ onSuccess: () => {
+ // 캐시 무효화
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEYS.club.list],
+ });
+ },
+ onError: (error) => {
+ console.error('Club 생성 실패:', error);
+ },
+ });
+};
+```
+
+### 4. 쿼리 키 관리
+
+**`src/constants/queryKeys.ts` 구조:**
+
+```typescript
+export const QUERY_KEYS = {
+ club: {
+ list: 'club.list',
+ detail: 'club.detail',
+ members: 'club.members',
+ },
+ application: {
+ list: 'application.list',
+ detail: 'application.detail',
+ },
+ // ...
+} as const;
+```
+
+## 주요 규칙
+
+### 네이밍 컨벤션
+
+- 훅 파일: `use도메인명.ts` (camelCase)
+- 훅 함수: `use도메인명액션` (예: `useClubs`, `useCreateClub`)
+- 쿼리 키: `도메인.액션` (dot notation)
+
+### 타입 안전성
+
+- API 응답 타입은 `src/types/` 또는 API 파일에 정의
+- 제네릭 활용하여 타입 추론 보장
+- 에러 타입도 명시적으로 처리
+
+### 캐싱 전략
+
+- `staleTime`: 데이터가 fresh한 시간 (기본: 0)
+- `gcTime` (구 cacheTime): 사용하지 않는 캐시 유지 시간 (기본: 5분)
+- 자주 변경되지 않는 데이터는 staleTime을 길게 설정
+
+### 에러 핸들링
+
+- API 레이어에서 `withErrorHandling`으로 기본 에러 처리
+- 훅에서는 `onError` 콜백으로 추가 처리
+- 사용자에게 보여줄 에러는 컴포넌트 레벨에서 처리
+
+### 캐시 무효화
+
+- Mutation 성공 시 관련 쿼리 무효화
+- `invalidateQueries`로 자동 리페칭
+- 낙관적 업데이트가 필요한 경우 `onMutate` 활용
+
+## 체크리스트
+
+새 API 훅 생성 시 확인:
+
+- [ ] API 함수가 `src/apis/`에 존재하는가?
+- [ ] 쿼리 키가 `src/constants/queryKeys.ts`에 등록되었는가?
+- [ ] 타입이 명시적으로 정의되었는가?
+- [ ] 에러 핸들링이 적절한가?
+- [ ] 캐싱 전략이 데이터 특성에 맞는가?
+- [ ] Mutation의 경우 캐시 무효화가 적절한가?
+- [ ] 인증이 필요한 경우 `secureFetch`를 사용하는가?
+
+## 참고 파일
+
+- `src/hooks/Queries/useClub.ts` - Club 관련 훅 예시
+- `src/hooks/Queries/useApplication.ts` - Application 관련 훅 예시
+- `src/apis/utils/apiHelpers.ts` - API 헬퍼 함수
+- `src/constants/queryKeys.ts` - 쿼리 키 중앙 관리
+
+## 기술 스택
+
+- @tanstack/react-query v5
+- TypeScript
+- React 19
+- Zustand (클라이언트 상태 관리)
diff --git a/frontend/.claude/commands/create-e2e-test.md b/frontend/.claude/commands/create-e2e-test.md
new file mode 100644
index 000000000..45c5a69b2
--- /dev/null
+++ b/frontend/.claude/commands/create-e2e-test.md
@@ -0,0 +1,12 @@
+# E2E 테스트 생성기
+
+너는 지금부터 Playwright 로 E2E 테스트를 생성하는 QA 전문가야.
+
+## 테스트 방식
+
+- $ARGUMENT로 입력한 테스트 요소들을 잘 이해해줘.
+- Playwright MCP를 사용해서 테스트를 진행해줘.
+- 테스트가 전부 끝나면 E2E 테스트를 작성해줘.
+- 작성한 테스트들을 전부 실행해줘.
+- 실패한 테스트가 있으면 원인을 분석해서 수정해줘.
+- 최대 3회까지만 재시도하고, 계속 실패하면 실패 원인/재현 절차/의심 구간을 리포트로 남겨줘.
diff --git a/frontend/.claude/commands/find-e2e-test.md b/frontend/.claude/commands/find-e2e-test.md
new file mode 100644
index 000000000..7141b9283
--- /dev/null
+++ b/frontend/.claude/commands/find-e2e-test.md
@@ -0,0 +1,9 @@
+# E2E 테스트 생성기
+
+너는 지금부터 Playwright 로 E2E 테스트를 생성하는 QA 전문가야.
+
+## 테스트 방식
+
+- $ARGUMENT로 입력한 테스트 요소들을 잘 이해해줘.
+- Playwright MCP를 사용해서 어떤 요소들을 테스트하면 좋을지 나열해줘.
+- 조사가 끝나면 요소들을 자연어로 마크다운 형태로 정리해줘.
diff --git a/frontend/.claude/commands/tm/auto-implement-tasks.md b/frontend/.claude/commands/tm/auto-implement-tasks.md
new file mode 100644
index 000000000..42d99226f
--- /dev/null
+++ b/frontend/.claude/commands/tm/auto-implement-tasks.md
@@ -0,0 +1 @@
+- Assess test coverage needs
diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
new file mode 100644
index 000000000..19e1b19d1
--- /dev/null
+++ b/frontend/CLAUDE.md
@@ -0,0 +1,220 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## 빌드 및 개발 명령어
+
+```bash
+# 개발 서버 (Vite - 주로 사용)
+npm run dev # 포트 3000에서 개발 서버 시작
+
+# 빌드
+npm run build # 기본 빌드 (타입 체크 + sitemap 생성)
+npm run build:dev # 개발 빌드
+npm run build:prod # 프로덕션 빌드
+npm run preview # 빌드 결과 미리보기
+npm run clean # dist 폴더 삭제
+
+# 테스트
+npm run test # 전체 테스트 실행
+npm run coverage # 커버리지 리포트와 함께 테스트 실행
+npx jest path/to/file.test.ts # 단일 테스트 파일 실행
+
+# 코드 품질
+npm run lint # ESLint 자동 수정
+npm run format # Prettier 포맷팅
+npm run typecheck # TypeScript 타입 체크만 실행
+
+# Storybook
+npm run storybook # 포트 6006에서 Storybook 시작
+npm run build-storybook # Storybook 빌드
+npm run chromatic # Chromatic으로 시각적 테스트 배포
+
+# 유틸리티
+npm run generate:sitemap # sitemap.xml 생성
+```
+
+## 아키텍처 개요
+
+### 기술 스택
+
+- React 19 + TypeScript
+- Vite 번들러 (webpack 설정도 있으나 Vite가 주력)
+- styled-components 스타일링
+- TanStack React Query v5 서버 상태 관리
+- Zustand 클라이언트 상태 관리
+- React Router v7
+- date-fns 날짜 처리
+- Framer Motion 애니메이션
+- Swiper 캐러셀
+- react-datepicker 날짜 선택
+- react-markdown 마크다운 렌더링
+
+### 외부 서비스 통합
+
+- **Mixpanel**: 사용자 분석 및 이벤트 트래킹
+- **Sentry**: 에러 모니터링 및 성능 추적
+- **Channel.io**: 고객 지원 채팅
+- **Kakao SDK**: 카카오 공유 기능
+
+모든 SDK는 `src/utils/initSDK.ts`에서 초기화되며, 각각 환경 변수 필요.
+
+### 환경 변수
+
+`.env` 파일에 다음 환경 변수 설정 필요 (모두 `VITE_` 접두사 사용):
+
+- `VITE_API_URL` - 백엔드 API URL
+- `VITE_MIXPANEL_TOKEN` - Mixpanel 프로젝트 토큰
+- `VITE_SENTRY_DSN` - Sentry DSN
+- `VITE_SENTRY_RELEASE` - Sentry 릴리즈 버전
+- `VITE_ENABLE_SENTRY_IN_DEV` - 개발 환경에서 Sentry 활성화 여부 (true/false)
+- `VITE_CHANNEL_PLUGIN_KEY` - Channel.io 플러그인 키
+- `VITE_KAKAO_JAVASCRIPT_KEY` - Kakao JavaScript 키
+
+### 프로젝트 구조
+
+**경로 별칭**: `@/*`는 `src/*`로 매핑
+
+**주요 디렉토리**:
+
+- `src/apis/` - 도메인별 API 함수 (club, auth, application, applicants)
+- `src/hooks/Queries/` - API를 래핑하는 React Query 훅 (useClub, useApplication, useApplicants)
+- `src/store/` - Zustand 스토어 (useCategoryStore, useSearchStore)
+- `src/pages/` - 라우트 기반 페이지 컴포넌트
+- `src/components/` - 공용 UI 컴포넌트
+- `src/context/` - React Context 프로바이더 (AdminClubContext - SSE 상태 관리)
+- `src/experiments/` - A/B 테스트 실험 정의 및 관리
+- `src/mocks/` - MSW(Mock Service Worker) 핸들러
+- `src/utils/` - 유틸리티 함수 (날짜 파싱, 유효성 검사, 디바운스, WebView 브릿지 등)
+- `src/errors/` - 커스텀 에러 클래스
+- `src/types/` - 공용 타입 정의
+- `src/constants/` - 상수 관리 (queryKeys, storageKeys, status, eventName, api, snsConfig 등)
+
+### API 레이어 패턴
+
+API는 `src/apis/utils/apiHelpers.ts`의 헬퍼 함수를 사용하는 일관된 패턴을 따름:
+
+- `handleResponse()` - 응답 파싱, `{ data: {...} }` 형식 자동 언래핑
+- `withErrorHandling()` - API 호출을 에러 로깅으로 래핑
+- `secureFetch()` - 인증된 요청, 403 시 토큰 자동 갱신
+
+쿼리 키는 `src/constants/queryKeys.ts`에 중앙 관리.
+
+### 인증 플로우
+
+- JWT는 localStorage에 저장 (`accessToken` 키, `src/constants/storageKeys.ts`에서 관리)
+- 리프레시 토큰은 쿠키로 처리
+- `src/apis/auth/secureFetch.ts`의 `secureFetch()`가 자동 토큰 갱신 담당
+- 어드민 라우트는 `PrivateRoute` 컴포넌트로 보호
+
+### 실험(A/B 테스트) 프레임워크
+
+`src/experiments/`에서 Mixpanel 기반 실험 관리:
+
+- `definitions.ts` - 실험 정의 (key, variants, weights)
+- `ExperimentRepository.ts` - 실험 할당 및 변형 조회 로직
+- `initializeExperiments.ts` - 앱 시작 시 실험 초기화
+- `useExperiment()` 훅으로 컴포넌트에서 실험 변형 사용
+
+**예시**:
+
+```typescript
+const { variant } = useExperiment(mainBannerExperiment);
+// variant는 'A' 또는 'B'
+```
+
+### MSW (Mock Service Worker)
+
+`src/mocks/`에서 API 모킹 관리:
+
+- `handlers/` - 도메인별 모킹 핸들러
+- `browser.ts` - MSW 브라우저 워커 설정
+- Storybook 및 개발 환경에서 사용
+
+### 주요 유틸리티 함수
+
+`src/utils/`에 공용 유틸리티 함수 모음:
+
+- `formatRelativeDateTime.ts` - 상대적 시간 표시 ("2시간 전")
+- `recruitmentDateParser.ts` - 모집 기간 파싱
+- `debounce.ts` - 디바운스 함수
+- `validateSocialLink.ts` - SNS 링크 유효성 검사
+- `isInAppWebView.ts` - 인앱 WebView 감지
+- `webviewBridge.ts` - 네이티브 앱과 통신
+- `initSDK.ts` - 외부 SDK 초기화 (Mixpanel, Sentry, Channel.io, Kakao)
+
+### 반응형 브레이크포인트
+
+`src/styles/mediaQuery.ts`에 정의:
+
+- mini_mobile: 375px
+- mobile: 500px
+- tablet: 700px
+- laptop: 1280px
+- Desktop: 1280px 초과 (기본값)
+
+### 테마 시스템
+
+테마는 `src/styles/theme/`에 colors, typography, transitions로 정의. styled-components `ThemeProvider`를 통해 접근.
+
+### 상수 관리
+
+`src/constants/`에 모든 상수 중앙 관리:
+
+- `queryKeys.ts` - React Query 쿼리 키 (도메인.액션 형식)
+- `storageKeys.ts` - localStorage 키 (`accessToken`, `hasConsentedPersonalInfo`)
+- `status.ts` - 지원 상태 정의 (PENDING, APPROVED, REJECTED 등)
+- `eventName.ts` - Mixpanel 이벤트명
+- `api.ts` - API 엔드포인트 URL
+- `snsConfig.ts` - SNS 플랫폼 설정
+- `applicationForm.ts` - 지원서 폼 설정
+- `uploadLimit.ts` - 파일 업로드 제한
+
+### 실시간 업데이트
+
+지원자 상태 업데이트를 위해 SSE(Server-Sent Events) 사용, `AdminClubContext`에서 관리.
+
+### 날짜 처리
+
+- `date-fns` 라이브러리 사용 (Moment.js 대신)
+- `formatRelativeDateTime` 유틸로 상대 시간 표시
+- `react-datepicker` 컴포넌트로 날짜 입력
+
+### 애니메이션
+
+- `framer-motion` 라이브러리로 페이지 전환, 모달, 제스처 등 애니메이션 구현
+- `src/styles/theme/transitions.ts`에 공통 트랜지션 정의
+
+### 캐러셀
+
+- `swiper` 라이브러리로 이미지 슬라이더, 카드 캐러셀 구현
+
+## 테스트
+
+- Jest + React Testing Library
+- MSW로 API 모킹
+- 테스트 파일은 `*.test.ts` 또는 `*.test.tsx` 형식
+- 커버리지 리포트: `npm run coverage`
+
+## Storybook
+
+- 컴포넌트 독립 개발 환경
+- MSW addon으로 API 모킹 지원
+- Chromatic으로 시각적 회귀 테스트
+
+## Claude Code Agent
+
+`.claude/agents/` 디렉토리에 전담 agent 정의:
+
+- `api-hooks-agent.md` - React Query 훅 생성 및 관리 전담
+
+Agent 사용 시 해당 문서를 참조하여 일관된 패턴 유지.
+
+## 코딩 컨벤션
+
+- **네이밍**: camelCase (변수, 함수), PascalCase (컴포넌트, 타입)
+- **파일명**: 컴포넌트는 PascalCase.tsx, 유틸은 camelCase.ts
+- **Import 순서**: 외부 라이브러리 → 내부 모듈 → 타입 → 스타일
+- **스타일**: styled-components 사용, 테마 시스템 활용
+- **타입**: any 금지, 명시적 타입 정의
+- **상수**: UPPER_SNAKE_CASE, `src/constants/`에서 관리
diff --git a/frontend/src/experiments/ExperimentRepository.ts b/frontend/src/experiments/ExperimentRepository.ts
new file mode 100644
index 000000000..d9d9fb23b
--- /dev/null
+++ b/frontend/src/experiments/ExperimentRepository.ts
@@ -0,0 +1,100 @@
+import type {
+ ExperimentAssignments,
+ ExperimentDefinition,
+ ExperimentVariant,
+} from './types';
+
+const ASSIGNMENT_STORAGE_KEY = 'moadong_experiments';
+
+const isObjectRecord = (value: unknown): value is Record =>
+ typeof value === 'object' && value !== null && !Array.isArray(value);
+
+const safeReadAssignments = (): ExperimentAssignments => {
+ try {
+ const raw = localStorage.getItem(ASSIGNMENT_STORAGE_KEY);
+ if (!raw) return {};
+ const parsed: unknown = JSON.parse(raw);
+ if (!isObjectRecord(parsed)) return {};
+ return Object.fromEntries(
+ Object.entries(parsed).filter(([, value]) => typeof value === 'string'),
+ ) as ExperimentAssignments;
+ } catch {
+ return {};
+ }
+};
+
+const writeAssignments = (assignments: ExperimentAssignments) => {
+ try {
+ localStorage.setItem(ASSIGNMENT_STORAGE_KEY, JSON.stringify(assignments));
+ } catch {
+ // localStorage 쓰기 실패(용량 초과, 권한 거부 등)는 무시하고 진행한다.
+ // 실패해도 배정값은 메모리에서 유효하며, 다음 새로고침 시 재배정된다.
+ }
+};
+
+const pickWeightedVariant = (
+ experiment: ExperimentDefinition,
+): V => {
+ if (experiment.variants.length === 0) return experiment.defaultVariant;
+ if (experiment.variants.length === 1) return experiment.variants[0];
+
+ const { variants, weights } = experiment;
+ if (!weights) {
+ const randomIndex = Math.floor(Math.random() * variants.length);
+ return variants[randomIndex];
+ }
+
+ const totalWeight = variants.reduce(
+ (sum, variant) => sum + (weights[variant] ?? 0),
+ 0,
+ );
+
+ if (totalWeight <= 0) return experiment.defaultVariant;
+
+ let randomPointer = Math.random() * totalWeight;
+ for (const variant of variants) {
+ randomPointer -= weights[variant] ?? 0;
+ if (randomPointer <= 0) return variant;
+ }
+
+ return experiment.defaultVariant;
+};
+
+class ExperimentRepository {
+ fetchAndAssignExperiments(experiments: readonly ExperimentDefinition[]) {
+ if (experiments.length === 0) return;
+
+ const assignments = safeReadAssignments();
+
+ experiments.forEach((experiment) => {
+ const existing = assignments[experiment.key];
+ const isValidExisting =
+ !!existing && experiment.variants.includes(existing);
+
+ if (isValidExisting) return;
+
+ assignments[experiment.key] = pickWeightedVariant(experiment);
+ });
+
+ writeAssignments(assignments);
+ }
+
+ getVariant(
+ experiment: ExperimentDefinition,
+ ): V {
+ const assignments = safeReadAssignments();
+ const assignedVariant = assignments[experiment.key];
+
+ if (assignedVariant && experiment.variants.includes(assignedVariant as V)) {
+ return assignedVariant as V;
+ }
+
+ return experiment.defaultVariant;
+ }
+
+ resetAssignments() {
+ localStorage.removeItem(ASSIGNMENT_STORAGE_KEY);
+ }
+}
+
+export const experimentRepository = new ExperimentRepository();
diff --git a/frontend/src/experiments/definitions.ts b/frontend/src/experiments/definitions.ts
new file mode 100644
index 000000000..6d1481338
--- /dev/null
+++ b/frontend/src/experiments/definitions.ts
@@ -0,0 +1,26 @@
+import type { ExperimentDefinition } from './types';
+
+export const mainBannerExperiment = {
+ key: 'main_banner_v1',
+ variants: ['A', 'B'] as const,
+ defaultVariant: 'A',
+ weights: {
+ A: 50,
+ B: 50,
+ },
+} satisfies ExperimentDefinition<'A' | 'B'>;
+
+export const applyButtonCopyExperiment = {
+ key: 'apply_button_copy_v1',
+ variants: ['A', 'B'] as const,
+ defaultVariant: 'A',
+ weights: {
+ A: 50,
+ B: 50,
+ },
+} satisfies ExperimentDefinition<'A' | 'B'>;
+
+export const ALL_EXPERIMENTS = [
+ mainBannerExperiment,
+ applyButtonCopyExperiment,
+] as const;
diff --git a/frontend/src/experiments/initializeExperiments.ts b/frontend/src/experiments/initializeExperiments.ts
new file mode 100644
index 000000000..ffa363b38
--- /dev/null
+++ b/frontend/src/experiments/initializeExperiments.ts
@@ -0,0 +1,6 @@
+import { ALL_EXPERIMENTS } from './definitions';
+import { experimentRepository } from './ExperimentRepository';
+
+export const initializeExperiments = () => {
+ experimentRepository.fetchAndAssignExperiments(ALL_EXPERIMENTS);
+};
diff --git a/frontend/src/experiments/types.ts b/frontend/src/experiments/types.ts
new file mode 100644
index 000000000..af885ff70
--- /dev/null
+++ b/frontend/src/experiments/types.ts
@@ -0,0 +1,10 @@
+export type ExperimentVariant = string;
+
+export type ExperimentDefinition = {
+ key: string;
+ variants: readonly V[];
+ defaultVariant: V;
+ weights?: Partial>;
+};
+
+export type ExperimentAssignments = Record;
diff --git a/frontend/src/hooks/Experiment/useExperimentVariant.ts b/frontend/src/hooks/Experiment/useExperimentVariant.ts
new file mode 100644
index 000000000..de1dc0357
--- /dev/null
+++ b/frontend/src/hooks/Experiment/useExperimentVariant.ts
@@ -0,0 +1,11 @@
+import { experimentRepository } from '@/experiments/ExperimentRepository';
+import type {
+ ExperimentDefinition,
+ ExperimentVariant,
+} from '@/experiments/types';
+
+export const useExperimentVariant = (
+ experiment: ExperimentDefinition,
+): V => {
+ return experimentRepository.getVariant(experiment);
+};
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 0906c2c8e..147899748 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,9 +1,11 @@
import ReactDOM from 'react-dom/client';
import App from './App';
+import { initializeExperiments } from './experiments/initializeExperiments';
import { initializeMixpanel, initializeSentry } from './utils/initSDK';
initializeMixpanel();
initializeSentry();
+initializeExperiments();
async function startApp() {
if (import.meta.env.DEV) {