refactor: 프론트엔드 개선 계획 수립

- 기존 마이그레이션 문서 삭제 (admin-migration.md, frontend-refactoring.md, migration.md)
- 새로운 개선 계획서 작성 (frontend-improvement.md)
- constants 정리: CATEGORY_NAMES, ALBUM_TYPES, 불필요한 SNS 링크 삭제
- schedule.js: getMemberList에서 쉼표 구분 로직 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-22 21:30:28 +09:00
parent 25d74d098d
commit b314b70014
6 changed files with 180 additions and 2684 deletions

View file

@ -1,549 +0,0 @@
# 관리자 페이지 마이그레이션 계획서
## 개요
- **목표**: `frontend/src/pages/pc/admin/``frontend-temp/src/pages/admin/`
- **현재 규모**: 12개 파일, 약 7,535 LOC
- **방식**: 일반 페이지와 동일한 Strangler Fig Pattern 적용
- **특이사항**: PC 전용 (모바일 관리자 페이지 없음)
---
## 현재 구조 분석
### 관리자 페이지 (12개)
| 파일 | LOC | 설명 | 복잡도 |
|------|-----|------|--------|
| AdminLogin.jsx | 168 | 로그인 폼 | 낮음 |
| AdminDashboard.jsx | 167 | 대시보드 (통계) | 낮음 |
| AdminMembers.jsx | 180 | 멤버 목록 | 낮음 |
| AdminMemberEdit.jsx | 382 | 멤버 수정 폼 | 중간 |
| AdminAlbums.jsx | 222 | 앨범 목록 | 낮음 |
| AdminAlbumForm.jsx | 666 | 앨범 생성/수정 폼 | 높음 |
| AdminAlbumPhotos.jsx | 1,538 | 사진 업로드 (SSE) | 매우 높음 |
| AdminSchedule.jsx | 1,465 | 일정 목록/검색 | 매우 높음 |
| AdminScheduleForm.jsx | 1,173 | 일정 생성/수정 폼 | 높음 |
| AdminScheduleCategory.jsx | 463 | 카테고리 관리 | 중간 |
| AdminScheduleDict.jsx | 664 | 일정 사전 관리 | 중간 |
| AdminScheduleBots.jsx | 447 | 봇 관리 | 중간 |
### 관리자 컴포넌트 (3개)
| 파일 | 설명 |
|------|------|
| AdminLayout.jsx | 관리자 레이아웃 (헤더 + 본문) |
| AdminHeader.jsx | 관리자 헤더 (네비게이션) |
| NumberPicker.jsx | 숫자 입력 컴포넌트 |
### 관리자 API (8개)
| 파일 | 설명 |
|------|------|
| auth.js | 로그인/인증 |
| albums.js | 앨범 CRUD |
| members.js | 멤버 CRUD |
| schedules.js | 일정 CRUD |
| categories.js | 카테고리 CRUD |
| suggestions.js | 검색어 추천 관리 |
| bots.js | 봇 관리 |
| stats.js | 통계 조회 |
---
## 마이그레이션 전략
### 우선순위
복잡도와 의존성을 고려하여 순차적으로 진행:
1. **1단계**: 폴더 구조 재편 (기존 파일을 새 구조로 이동)
2. **2단계**: 관리자 기반 설정 (레이아웃, API, 인증)
3. **3단계**: 간단한 페이지 (로그인, 대시보드, 멤버)
4. **4단계**: 앨범 관리
5. **5단계**: 일정 관리 (가장 복잡)
### 코딩 가이드라인
공개 영역과 동일한 패턴 적용:
- `useQuery` / `useMutation` 사용
- 서비스 레이어 분리
- 공통 컴포넌트 재사용
- TypeScript JSDoc 타입 정의
---
## 1단계: 폴더 구조 재편
기존 frontend-temp 파일들을 새로운 구조로 이동합니다.
### 1.1 현재 구조 → 새 구조
```
현재 → 새 구조
─────────────────────────────────────────────────────────
api/
├── client.js → api/common/client.js
├── index.js → (삭제, 각 폴더별 index.js로 대체)
├── auth.js → api/pc/admin/auth.js
├── schedules.js → api/pc/public/schedules.js
├── albums.js → api/pc/public/albums.js
└── members.js → api/pc/common/members.js
components/
├── common/ → components/common/
│ ├── Loading.jsx
│ ├── ErrorBoundary.jsx
│ ├── ErrorMessage.jsx
│ ├── Toast.jsx
│ ├── Lightbox.jsx
│ └── ...
├── pc/ → components/pc/public/
│ ├── Layout.jsx
│ ├── Header.jsx
│ ├── Footer.jsx
│ ├── Calendar.jsx
│ └── ScheduleCard.jsx
└── mobile/ → components/mobile/
├── Layout.jsx
├── Header.jsx
├── BottomNav.jsx
└── ...
hooks/
├── useMediaQuery.js → hooks/common/useMediaQuery.js
├── useLightbox.js → hooks/common/useLightbox.js
├── useCalendar.js → hooks/common/useCalendar.js
├── useAdminAuth.js → hooks/pc/admin/useAdminAuth.js
└── ...
pages/
├── home/pc/ → pages/pc/public/home/
├── home/mobile/ → pages/mobile/home/
├── schedule/pc/ → pages/pc/public/schedule/
├── schedule/mobile/ → pages/mobile/schedule/
├── album/pc/ → pages/pc/public/album/
├── album/mobile/ → pages/mobile/album/
├── members/pc/ → pages/pc/public/members/
└── members/mobile/ → pages/mobile/members/
```
### 1.2 import 경로 업데이트
파일 이동 후 모든 import 경로를 새 구조에 맞게 수정합니다.
```javascript
// Before
import { fetchApi } from '@/api/client';
import Loading from '@/components/common/Loading';
import Calendar from '@/components/pc/Calendar';
// After
import { fetchApi } from '@/api/common/client';
import Loading from '@/components/common/Loading';
import Calendar from '@/components/pc/public/Calendar';
```
### 1.3 index.js 파일 생성
각 폴더에 re-export용 index.js 생성:
```javascript
// api/pc/public/index.js
export * from './schedules';
export * from './albums';
// api/pc/admin/index.js
export * from './auth';
export * from './schedules';
// ...
```
---
## 2단계: 관리자 기반 설정
### 2.1 관리자 API 추가
**원칙**: 조회(GET)는 공개 API 재사용, CUD(Create/Update/Delete)는 관리자 API
```
api/
├── common/ # PC, Mobile 공통
│ └── client.js # fetch 래퍼 (fetchApi, fetchAuthApi)
├── pc/
│ ├── common/ # PC 내 공통 (public + admin 공유)
│ │ └── members.js # 멤버 조회 (일정 폼에서도 사용)
│ ├── public/ # PC 공개 API
│ │ ├── schedules.js
│ │ └── albums.js
│ └── admin/ # PC 관리자 API
│ ├── auth.js
│ ├── schedules.js
│ ├── albums.js
│ ├── members.js
│ ├── categories.js
│ ├── suggestions.js
│ ├── bots.js
│ └── stats.js
└── mobile/ # 모바일 (pc/public과 동일 API 사용 가능)
```
**사용 예시**:
```javascript
// PC 공개 페이지
import { fetchSchedules } from '@/api/pc/public';
// PC 관리자 페이지 - 조회는 pc/common 또는 pc/public 재사용
import { fetchMembers } from '@/api/pc/common';
import { createSchedule } from '@/api/pc/admin';
// 모바일 페이지
import { fetchSchedules } from '@/api/pc/public'; // 동일 API 재사용
```
### 2.2 관리자 레이아웃
`components/pc/admin/` 폴더 구조:
```
components/pc/admin/
├── Layout.jsx # AdminLayout
├── Header.jsx # AdminHeader (네비게이션)
└── NumberPicker.jsx
```
### 2.3 인증 훅 수정
현재 `useAdminAuth.js`를 관리자 전용으로 확장:
- 토큰 검증
- 자동 로그아웃
- 권한 체크
---
## 3단계: 간단한 페이지
### 3.1 AdminLogin
- 로그인 폼 컴포넌트화
- `useMutation`으로 로그인 처리
- 에러 처리 표준화
### 3.2 AdminDashboard
- 통계 API 호출 (`useQuery`)
- 차트/카드 컴포넌트화
- 로딩/에러 상태 처리
### 3.3 AdminMembers / AdminMemberEdit
- 멤버 목록 테이블
- 멤버 수정 폼
- 이미지 업로드 처리
---
## 4단계: 앨범 관리
### 4.1 AdminAlbums
- 앨범 목록 테이블
- 삭제 확인 모달
- 정렬/필터링
### 4.2 AdminAlbumForm
폼 필드:
- 기본 정보 (이름, 발매일, 타입)
- 트랙 목록 (동적 추가/삭제)
- 티저 목록 (동적 추가/삭제)
- 앨범 커버 업로드
컴포넌트 분리:
```
components/pc/admin/album/
├── AlbumBasicForm.jsx # 기본 정보
├── TrackListForm.jsx # 트랙 관리
├── TeaserListForm.jsx # 티저 관리
└── CoverUploader.jsx # 커버 업로드
```
### 4.3 AdminAlbumPhotos
가장 복잡한 페이지:
- SSE 스트리밍 업로드
- 진행률 표시
- 멤버 태깅
- 드래그 앤 드롭
훅 분리:
```
hooks/pc/admin/
├── usePhotoUpload.js # SSE 업로드 로직
├── usePhotoList.js # 사진 목록 관리
└── useMemberTag.js # 멤버 태깅
```
---
## 5단계: 일정 관리
### 5.1 AdminSchedule
기능:
- 일정 목록 (가상 스크롤)
- 검색/필터링
- 일괄 삭제
훅 재사용:
- `useScheduleSearch` (공개 영역과 동일)
- `useScheduleFiltering` (공개 영역과 동일)
### 5.2 AdminScheduleForm
폼 필드:
- 기본 정보 (제목, 날짜, 시간)
- 카테고리 선택
- 멤버 선택
- 출처 정보
- 카테고리별 추가 필드 (YouTube, X)
컴포넌트 분리:
```
components/pc/admin/schedule/
├── ScheduleBasicForm.jsx # 기본 정보
├── CategorySelect.jsx # 카테고리 선택
├── MemberSelect.jsx # 멤버 다중 선택
├── SourceInput.jsx # 출처 입력
├── YouTubeForm.jsx # YouTube 영상 정보
└── XForm.jsx # X 게시글 정보
```
### 5.3 AdminScheduleCategory
- 카테고리 CRUD
- 색상 선택기
- 순서 변경 (드래그)
### 5.4 AdminScheduleDict
- 일정 사전 관리
- 자동완성 데이터
### 5.5 AdminScheduleBots
- 봇 목록/상태
- 동기화 실행
- 로그 확인
---
## 목표 구조
```
frontend-temp/src/
├── api/
│ ├── common/ # PC, Mobile 공통
│ │ └── client.js # fetch 래퍼
│ ├── pc/
│ │ ├── common/ # PC 내 공통 (public + admin 공유)
│ │ │ └── members.js # 멤버 조회 (폼에서 공유)
│ │ ├── public/ # PC 공개 API
│ │ │ ├── schedules.js
│ │ │ └── albums.js
│ │ └── admin/ # PC 관리자 API
│ │ ├── auth.js
│ │ ├── schedules.js
│ │ ├── albums.js
│ │ ├── members.js
│ │ ├── categories.js
│ │ ├── suggestions.js
│ │ ├── bots.js
│ │ └── stats.js
│ └── mobile/ # 모바일 API
│ └── (public과 동일하게 사용)
├── components/
│ ├── common/ # PC, Mobile 공통
│ │ ├── Loading.jsx
│ │ ├── ErrorBoundary.jsx
│ │ ├── ErrorMessage.jsx
│ │ ├── Toast.jsx
│ │ └── Lightbox/
│ ├── pc/
│ │ ├── common/ # PC 내 공통
│ │ │ └── (필요시)
│ │ ├── public/ # PC 공개 컴포넌트
│ │ │ ├── Layout.jsx
│ │ │ ├── Header.jsx
│ │ │ ├── Footer.jsx
│ │ │ ├── Calendar.jsx
│ │ │ └── ScheduleCard.jsx
│ │ └── admin/ # PC 관리자 컴포넌트
│ │ ├── Layout.jsx
│ │ ├── Header.jsx
│ │ ├── NumberPicker.jsx
│ │ ├── ConfirmDialog.jsx
│ │ ├── album/
│ │ │ ├── AlbumBasicForm.jsx
│ │ │ ├── TrackListForm.jsx
│ │ │ ├── TeaserListForm.jsx
│ │ │ └── CoverUploader.jsx
│ │ └── schedule/
│ │ ├── ScheduleBasicForm.jsx
│ │ ├── CategorySelect.jsx
│ │ ├── MemberSelect.jsx
│ │ ├── SourceInput.jsx
│ │ ├── YouTubeForm.jsx
│ │ └── XForm.jsx
│ └── mobile/ # 모바일 컴포넌트
│ ├── Layout.jsx
│ ├── Header.jsx
│ ├── BottomNav.jsx
│ ├── Calendar.jsx
│ └── ScheduleCard.jsx
├── hooks/
│ ├── common/ # PC, Mobile 공통
│ │ ├── useMediaQuery.js
│ │ ├── useLightbox.js
│ │ └── useCalendar.js
│ ├── pc/
│ │ ├── common/ # PC 내 공통
│ │ │ └── (필요시)
│ │ ├── public/ # PC 공개 훅
│ │ │ └── (필요시)
│ │ └── admin/ # PC 관리자 훅
│ │ ├── useAdminAuth.js
│ │ ├── usePhotoUpload.js
│ │ ├── usePhotoList.js
│ │ └── useMemberTag.js
│ └── mobile/ # 모바일 훅
│ └── (필요시)
├── pages/
│ ├── pc/
│ │ ├── public/ # PC 공개 페이지
│ │ │ ├── Home.jsx
│ │ │ ├── Schedule.jsx
│ │ │ ├── Album.jsx
│ │ │ └── Members.jsx
│ │ └── admin/ # PC 관리자 페이지
│ │ ├── Login.jsx
│ │ ├── Dashboard.jsx
│ │ ├── Members.jsx
│ │ ├── MemberEdit.jsx
│ │ ├── Albums.jsx
│ │ ├── AlbumForm.jsx
│ │ ├── AlbumPhotos.jsx
│ │ ├── Schedule.jsx
│ │ ├── ScheduleForm.jsx
│ │ ├── ScheduleCategory.jsx
│ │ ├── ScheduleDict.jsx
│ │ └── ScheduleBots.jsx
│ └── mobile/ # 모바일 페이지
│ ├── Home.jsx
│ ├── Schedule.jsx
│ ├── Album.jsx
│ └── Members.jsx
├── stores/ # 전역 상태 (플랫)
│ ├── useAuthStore.js
│ ├── useScheduleStore.js
│ └── useUIStore.js
├── utils/ # 유틸리티 (플랫)
│ ├── date.js
│ ├── format.js
│ └── schedule.js
└── constants/ # 상수 (플랫)
└── index.js
```
---
## 작업 체크리스트
### 1단계: 폴더 구조 재편 ✅
- [x] api/ 구조 변경 (common/, pc/common/, pc/public/, pc/admin/)
- [x] components/ 구조 변경 (common/, pc/public/, pc/admin/, mobile/)
- [x] hooks/ 구조 변경 (common/, pc/admin/)
- [x] pages/ 구조 변경 (pc/public/, mobile/)
- [x] 모든 import 경로 업데이트
- [x] 각 폴더에 index.js 생성 (re-export)
- [x] 개발 서버 동작 확인
### 2단계: 관리자 기반 설정 ✅
- [x] 관리자 API 마이그레이션 (api/pc/admin/)
- [x] AdminLayout 마이그레이션 (components/pc/admin/)
- [x] AdminHeader 마이그레이션 (components/pc/admin/)
- [x] useAdminAuth 훅 이동 (hooks/pc/admin/)
- [x] 관리자 라우트 설정 (App.jsx)
- [x] AdminLogin 페이지 마이그레이션
### 3단계: 간단한 페이지 ✅
- [x] AdminLogin 마이그레이션 (2단계에서 완료)
- [x] AdminDashboard 마이그레이션
- [x] AdminMembers 마이그레이션
- [x] AdminMemberEdit 마이그레이션
- [x] useToast 훅 추가
### 4단계: 앨범 관리 ✅
- [x] AdminAlbums 마이그레이션
- [x] AdminAlbumForm 마이그레이션
- [x] AdminAlbumPhotos 마이그레이션 (SSE 처리 포함)
### 5단계: 일정 관리 ✅
- [x] AdminSchedule 마이그레이션
- [x] AdminScheduleForm 마이그레이션
- [x] AdminScheduleCategory 마이그레이션
- [x] AdminScheduleDict 마이그레이션
- [x] AdminScheduleBots 마이그레이션
- [x] 카테고리별 폼 분기 페이지 마이그레이션 (form/index.jsx)
- [x] YouTubeForm 마이그레이션 (form/YouTubeForm.jsx)
- [x] XForm 마이그레이션 (form/XForm.jsx)
- [x] CategorySelector 마이그레이션 (form/components/CategorySelector.jsx)
- [x] YouTubeEditForm 마이그레이션 (edit/YouTubeEditForm.jsx)
### 6단계: 검증
- [ ] 공개 페이지 동작 확인 (PC/Mobile)
- [ ] 로그인/로그아웃 테스트
- [ ] 멤버 CRUD 테스트
- [ ] 앨범 CRUD 테스트
- [ ] 앨범 사진 업로드 테스트
- [ ] 일정 CRUD 테스트
- [ ] YouTube/X 일정 등록 테스트
- [ ] 카테고리 관리 테스트
- [ ] 봇 관리 테스트
---
## 예상 효과
| 항목 | Before | After | 개선 |
|------|--------|-------|------|
| 총 LOC | ~7,535 | ~5,500 | -27% |
| 중복 코드 | 30%+ | <10% | 공통 /컴포넌트 활용 |
| 컴포넌트 분리 | 12개 대형 파일 | 30개+ 작은 파일 | 유지보수성 향상 |
---
## 주의사항
1. **SSE 스트리밍**: AdminAlbumPhotos의 SSE 업로드는 별도 훅으로 분리하여 처리
2. **인증 처리**: 모든 관리자 API 호출에 토큰 자동 첨부
3. **에러 처리**: 401 에러 시 자동 로그아웃 및 리다이렉트
4. **폼 검증**: React Hook Form 또는 자체 검증 로직 사용
5. **낙관적 업데이트**: 목록 삭제 등에 useMutation의 onMutate 활용
---
## 참고
- 기존 문서: [frontend-refactoring.md](./frontend-refactoring.md)
- API 명세: [api.md](./api.md)
- 개발 가이드: [development.md](./development.md)

View file

@ -0,0 +1,179 @@
# 프론트엔드 개선 계획서
## 1. 현재 문제점
### 1.1 API 구조
```
현재 구조:
api/
├── common/client.js # fetch 래퍼
├── index.js # 통합 export (혼란)
├── pc/
│ ├── admin/ # 관리자 API
│ ├── common/members.js # PC 공통?
│ └── public/ # 공개 API (어드민 함수도 섞여있음)
```
**문제점:**
- PC와 모바일이 같은 API를 사용하는데 `pc/` 폴더 안에 있음
- `api/pc/public/schedules.js`에 어드민 API 함수도 포함됨
- `transformSchedule` 함수가 여러 파일에 중복
- `api/index.js`에서 같은 함수가 여러 번 export됨
### 1.2 컴포넌트 구조
```
현재 구조:
components/
├── common/ # 공통 (OK)
├── mobile/ # 모바일 (폴더 구분 없음)
├── pc/
│ ├── admin/ # 관리자 (schedule 폴더만 있음)
│ └── public/ # 공개 (폴더 구분 없음)
```
**문제점:**
- `admin/``public/` 내부에 기능별 폴더 구분이 없음
- 파일이 많아지면 찾기 어려움
### 1.3 중복 코드
- `getMemberList`: 16개 파일에서 사용, 일부는 로컬 정의
- `decodeHtmlEntities`: 여러 파일에 중복 정의
- `transformSchedule`: api/pc/public/schedules.js, api/pc/admin/schedules.js에 중복
- `Schedules.jsx`에 유틸 함수들이 로컬로 재정의됨
### 1.4 대형 파일
| 파일 | 라인 수 | 문제 |
|------|---------|------|
| AlbumPhotos.jsx | 1536 | 업로드, 관리, 일괄편집이 한 파일에 |
| Schedules.jsx | 1471 | 중복 유틸 함수, 컴포넌트 미분리 |
| ScheduleForm.jsx | 1046 | 폼 로직과 UI가 섞여있음 |
| ScheduleDict.jsx | 714 | 테이블과 모달이 한 파일에 |
| AlbumForm.jsx | 631 | 트랙/티저 관리가 인라인 |
---
## 2. 개선 계획
### 2.1 API 구조 개선
```
개선 후:
api/
├── client.js # fetch 래퍼
├── transforms.js # 공통 변환 함수
├── public/ # 공개 API (PC/Mobile 공용)
│ ├── schedules.js
│ ├── albums.js
│ └── members.js
└── admin/ # 관리자 API
├── schedules.js
├── albums.js
├── members.js
├── categories.js
├── bots.js
└── stats.js
```
### 2.2 컴포넌트 구조 개선
```
개선 후:
components/
├── common/ # 공통 (유지)
├── mobile/
│ ├── layout/ # Layout, Header, BottomNav
│ └── schedule/ # ScheduleCard, BirthdayCard 등
├── pc/
│ ├── public/
│ │ ├── layout/ # Layout, Header, Footer
│ │ └── schedule/ # Calendar, ScheduleCard 등
│ └── admin/
│ ├── layout/ # Layout, Header
│ ├── common/ # ConfirmDialog, DatePicker 등
│ ├── schedule/ # CategorySelector 등
│ └── album/ # PhotoUploader 등
```
### 2.3 중복 코드 제거
#### utils/schedule.js 통합 사용
- `Schedules.jsx`의 로컬 함수들 제거
- 모든 곳에서 `@/utils/schedule` import
#### utils/format.js에 decodeHtmlEntities 통합
- 중복 정의 제거
#### api/transforms.js 생성
- `transformSchedule` 함수 통합
### 2.4 대형 파일 분리
#### AlbumPhotos.jsx (1536줄)
```
pages/pc/admin/albums/
├── AlbumPhotos.jsx # 메인 (상태 관리)
├── components/
│ ├── PhotoUploader.jsx # 업로드 UI
│ ├── PhotoGrid.jsx # 사진 그리드
│ ├── PhotoPreview.jsx # 미리보기 모달
│ └── BulkEditTools.jsx # 일괄 편집 도구
└── hooks/
└── usePhotoUpload.js # 업로드 로직
```
#### Schedules.jsx (1471줄)
```
pages/pc/admin/schedules/
├── Schedules.jsx # 메인
├── components/
│ ├── ScheduleList.jsx # 목록 (가상화)
│ ├── ScheduleSearch.jsx # 검색 UI
│ └── MonthNavigator.jsx # 월 선택
└── hooks/
└── useScheduleList.js # 목록 조회 로직
```
#### ScheduleForm.jsx (1046줄)
```
pages/pc/admin/schedules/
├── ScheduleForm.jsx # 메인
└── components/
├── BasicInfoForm.jsx # 기본 정보
├── MemberSelector.jsx # 멤버 선택
└── SourceInput.jsx # 출처 입력
```
---
## 3. 작업 순서
### Phase 1: 구조 정리 (비파괴적)
1. [ ] API 구조 개선 (폴더 재배치, transforms.js 생성)
2. [ ] 중복 유틸 함수 제거 (Schedules.jsx 등)
3. [ ] 컴포넌트 폴더 구조화
### Phase 2: 대형 파일 분리
1. [ ] Schedules.jsx 분리
2. [ ] ScheduleForm.jsx 분리
3. [ ] AlbumPhotos.jsx 분리
4. [ ] ScheduleDict.jsx 분리
5. [ ] AlbumForm.jsx 분리
### Phase 3: 검증
1. [ ] 모든 페이지 동작 확인
2. [ ] 빌드 확인
---
## 4. 삭제할 항목
### constants/index.js에서 삭제됨
- `CATEGORY_NAMES` (미사용)
- `ALBUM_TYPES` (미사용)
- `SOCIAL_LINKS.tiktok` (불필요)
- `SOCIAL_LINKS.fancafe` (불필요)
### 삭제 예정
- `api/pc/` 폴더 구조 (→ `api/public/`, `api/admin/`으로 변경)
- `api/pc/common/` (→ `api/public/`으로 통합)
- 각 파일의 중복 유틸 함수

File diff suppressed because it is too large Load diff

View file

@ -1,455 +0,0 @@
# Express → Fastify 마이그레이션
## 개요
`backend-backup/` (Express) → `backend/` (Fastify)로 마이그레이션 완료
## 완료된 작업
### 서버 기반
- [x] Fastify 앱 구조 (`src/app.js`, `src/server.js`)
- [x] 플러그인 시스템 (`src/plugins/`)
- db.js (MariaDB)
- redis.js
- auth.js (JWT)
- meilisearch.js
- scheduler.js (봇 스케줄러)
### API 라우트 (`src/routes/`)
- [x] 인증 (`/api/auth`)
- POST /login - 로그인
- GET /verify - 토큰 검증
- [x] 멤버 (`/api/members`)
- GET / - 목록 조회
- GET /:name - 상세 조회
- PUT /:name - 수정 (이미지 업로드 포함)
- [x] 앨범 (`/api/albums`)
- GET / - 목록 조회
- GET /:id - ID로 조회
- GET /by-name/:name - 이름으로 조회
- GET /by-name/:albumName/track/:trackTitle - 트랙 조회
- POST / - 생성
- PUT /:id - 수정
- DELETE /:id - 삭제
- 사진 관리 (`/api/albums/:id/photos`)
- GET / - 목록
- POST / - 업로드
- PUT /:photoId - 수정
- DELETE /:photoId - 삭제
- 티저 관리 (`/api/albums/:id/teasers`)
- GET / - 목록
- POST / - 업로드
- DELETE /:teaserId - 삭제
- [x] 일정 (`/api/schedules`)
- GET / - 월별 조회 (생일 포함)
- GET /?search= - Meilisearch 검색
- GET /:id - 상세 조회
- DELETE /:id - 삭제
- POST /sync-search - Meilisearch 동기화
- [x] 추천 검색어 (`/api/schedules/suggestions`)
- GET / - 추천 검색어 조회
- GET /popular - 인기 검색어 조회
- POST /save - 검색어 저장
- GET /dict - 사용자 사전 조회 (관리자)
- PUT /dict - 사용자 사전 저장 (관리자)
- [x] 통계 (`/api/stats`)
- GET / - 대시보드 통계
### 관리자 API (`src/routes/admin/`)
- [x] 봇 관리 (`/api/admin/bots`)
- GET / - 봇 목록
- POST /:id/start - 봇 시작
- POST /:id/stop - 봇 정지
- POST /:id/sync-all - 전체 동기화
- GET /quota-warning - 할당량 경고 조회
- DELETE /quota-warning - 할당량 경고 해제
- [x] YouTube 관리 (`/api/admin/youtube`)
- GET /video-info - 영상 정보 조회
- POST /schedule - 일정 저장
- PUT /schedule/:id - 일정 수정
- [x] X 관리 (`/api/admin/x`)
- GET /post-info - 게시글 정보 조회
- POST /schedule - 일정 저장
### 서비스 (`src/services/`)
- [x] YouTube 봇 (`services/youtube/`)
- 영상 자동 수집
- 채널별 필터링 (제목 필터, 멤버 추출)
- [x] X(Twitter) 봇 (`services/x/`)
- Nitter 스크래핑
- 이미지 URL 추출
- [x] Meilisearch 검색 (`services/meilisearch/`)
- 일정 검색
- 전체 동기화
- [x] 추천 검색어 (`services/suggestions/`)
- 형태소 분석 (kiwi-nlp)
- bi-gram 빈도
- 초성 검색
- 사용자 사전 관리
- [x] 이미지 업로드 (`services/image.js`)
- 앨범 커버
- 멤버 이미지
- 앨범 사진/티저
## 남은 작업 (미구현)
### 일반 일정 CRUD
- [ ] POST /api/schedules - 일정 생성 (일반)
- [ ] PUT /api/schedules/:id - 일정 수정 (일반)
※ 현재는 YouTube/X 전용 일정 생성 API만 구현됨
### 카테고리 관리
- [ ] POST /api/schedule-categories - 생성
- [ ] PUT /api/schedule-categories/:id - 수정
- [ ] DELETE /api/schedule-categories/:id - 삭제
- [ ] PUT /api/schedule-categories-order - 순서 변경
※ GET은 구현됨 (목록 조회)
### 기타
- [ ] GET /api/kakao/places - 카카오 장소 검색 프록시
## 파일 비교표
| Express (backend-backup) | Fastify (backend) | 상태 |
|--------------------------|-------------------|------|
| routes/admin.js (로그인) | routes/auth.js | 완료 |
| routes/admin.js (앨범 CRUD) | routes/albums/index.js | 완료 |
| routes/admin.js (사진/티저) | routes/albums/photos.js, teasers.js | 완료 |
| routes/admin.js (멤버 수정) | routes/members/index.js | 완료 |
| routes/admin.js (일정 삭제) | routes/schedules/index.js | 완료 |
| routes/admin.js (일정 생성/수정) | - | 미완료 |
| routes/admin.js (카테고리 CUD) | - | 미완료 |
| routes/admin.js (봇 관리) | routes/admin/bots.js | 완료 |
| routes/admin.js (할당량) | routes/admin/bots.js | 완료 |
| routes/admin.js (카카오) | - | 미완료 |
| - | routes/admin/youtube.js | 신규 |
| - | routes/admin/x.js | 신규 |
| routes/albums.js | routes/albums/index.js | 완료 |
| routes/members.js | routes/members/index.js | 완료 |
| routes/schedules.js | routes/schedules/index.js | 완료 |
| routes/stats.js | routes/stats/index.js | 완료 |
| services/youtube-bot.js | services/youtube/ | 완료 |
| services/youtube-scheduler.js | plugins/scheduler.js | 완료 |
| services/x-bot.js | services/x/ | 완료 |
| services/meilisearch.js | services/meilisearch/ | 완료 |
| services/meilisearch-bot.js | services/meilisearch/ | 완료 |
| services/suggestions.js | services/suggestions/ | 완료 |
## 참고 사항
- 기존 Express 코드는 `backend-backup/` 폴더에 보존
- 마이그레이션 시 기존 코드 참조하여 동일 기능 구현
- DB 스키마 변경 사항:
- `tracks``album_tracks` (이름 변경)
- `venues``concert_venues` (이름 변경)
---
# 프론트엔드 마이그레이션 (Strangler Fig)
## 개요
`frontend/` (레거시) → `frontend-temp/` (신규)로 점진적 마이그레이션
## 설계 원칙
1. **react-device-detect 사용**
- App.jsx에서 `BrowserView`/`MobileView`로 PC/Mobile 완전 분리
- User Agent 기반 디바이스 감지
2. **기능별 폴더 + pc/mobile 하위 폴더**
- 각 페이지 폴더 내에 `pc/`, `mobile/` 서브폴더
- 비즈니스 로직(api, hooks, stores)은 최상위에서 공유
3. **React Query 사용**
- `useQuery`로 데이터 패칭 및 캐싱
4. **관리자 페이지는 PC 전용**
- mobile 폴더 없이 단일 구조
## App.jsx 라우팅 구조
```jsx
import { BrowserView, MobileView } from 'react-device-detect';
function App() {
return (
<BrowserRouter>
<BrowserView>
<Routes>
{/* Admin routes */}
<Route path="/admin" element={<AdminLogin />} />
<Route path="/admin/*" element={<AdminRoutes />} />
{/* Public routes with PC Layout */}
<Route element={<PCLayout />}>
<Route path="/" element={<PCHome />} />
<Route path="/members" element={<PCMembers />} />
<Route path="/album" element={<PCAlbum />} />
<Route path="/album/:name" element={<PCAlbumDetail />} />
...
</Route>
</Routes>
</BrowserView>
<MobileView>
<Routes>
<Route element={<MobileLayout />}>
<Route path="/" element={<MobileHome />} />
<Route path="/members" element={<MobileMembers />} />
...
</Route>
</Routes>
</MobileView>
</BrowserRouter>
);
}
```
## 전체 마이그레이션 체크리스트
### API 계층
#### 공개 API
- [x] client.js (fetchApi, fetchAuthApi)
- [x] albums.js
- [x] members.js
- [x] schedules.js
- [x] auth.js
#### 관리자 API (`api/pc/admin/`)
- [x] albums.js
- [x] members.js
- [x] schedules.js
- [x] categories.js
- [x] stats.js
- [x] bots.js
- [x] suggestions.js
### 훅 (hooks/)
- [x] useAlbumData.js
- [x] useMemberData.js
- [x] useScheduleData.js
- [x] useScheduleSearch.js
- [x] useScheduleFiltering.js
- [x] useCalendar.js
- [x] useMediaQuery.js
- [x] useAdminAuth.js
### 관리자 훅 (hooks/pc/admin/, hooks/common/)
- [x] useAdminAuth.js (hooks/pc/admin/)
- [x] useToast.js (hooks/common/)
### 스토어 (stores/)
- [x] useScheduleStore.js
- [x] useAuthStore.js
- [x] useUIStore.js
### 상수 (constants/)
- [x] index.js (CATEGORY_ID, MIN_YEAR, SEARCH_LIMIT 등)
### 유틸리티 (utils/)
- [x] index.js
- [x] date.js
- [x] format.js
- [x] cn.js (className 유틸)
- [x] schedule.js (일정 관련 유틸)
### 공통 컴포넌트 (components/common/)
- [x] Loading.jsx
- [x] ErrorBoundary.jsx
- [x] Toast.jsx
- [x] Lightbox.jsx (PC용)
- [x] MobileLightbox.jsx (Mobile용, Swiper 기반)
- [x] LightboxIndicator.jsx
- [x] Tooltip.jsx
- [x] ScrollToTop.jsx
### PC 컴포넌트 (components/pc/)
- [x] Layout.jsx
- [x] Header.jsx
- [x] Footer.jsx
- [x] Calendar.jsx
- [x] ScheduleCard.jsx
- [x] BirthdayCard.jsx
- [x] CategoryFilter.jsx
### Mobile 컴포넌트 (components/mobile/)
- [x] Layout.jsx
- [x] Header.jsx (MobileHeader)
- [x] BottomNav.jsx (MobileBottomNav)
- [x] Calendar.jsx
- [x] ScheduleCard.jsx
- [x] ScheduleListCard.jsx
- [x] ScheduleSearchCard.jsx
- [x] BirthdayCard.jsx
### 공용 일정 컴포넌트 (components/schedule/)
- [x] confetti.js (fireBirthdayConfetti)
- [x] AdminScheduleCard.jsx
### 관리자 컴포넌트 (components/pc/admin/)
- [x] Layout.jsx
- [x] Header.jsx
- [x] ConfirmDialog.jsx
- [x] CustomDatePicker.jsx
- [x] CustomTimePicker.jsx
- [x] NumberPicker.jsx
### 페이지 - Home (pages/home/)
- [x] pc/Home.jsx
- [x] mobile/Home.jsx
### 페이지 - Members (pages/members/)
- [x] pc/Members.jsx
- [x] mobile/Members.jsx
### 페이지 - Album (pages/album/)
- [x] pc/Album.jsx
- [x] pc/AlbumDetail.jsx
- [x] pc/AlbumGallery.jsx
- [x] pc/TrackDetail.jsx
- [x] mobile/Album.jsx
- [x] mobile/AlbumDetail.jsx
- [x] mobile/AlbumGallery.jsx
- [x] mobile/TrackDetail.jsx
### 페이지 - Schedule (pages/schedule/)
- [x] sections/DefaultSection.jsx
- [x] sections/XSection.jsx
- [x] sections/YoutubeSection.jsx
- [x] sections/utils.js
- [x] sections/index.js
- [x] pc/Schedule.jsx
- [x] pc/ScheduleDetail.jsx
- [x] pc/Birthday.jsx
- [x] mobile/Schedule.jsx
- [x] mobile/ScheduleDetail.jsx
- [x] mobile/Birthday.jsx
### 페이지 - Common (pages/common/)
- [x] pc/NotFound.jsx
- [x] mobile/NotFound.jsx
### 페이지 - Admin (pages/pc/admin/) - PC 전용
- [x] login/Login.jsx
- [x] dashboard/Dashboard.jsx
- [x] members/Members.jsx
- [x] albums/Albums.jsx
- [x] albums/AlbumForm.jsx
- [x] albums/AlbumPhotos.jsx
- [x] albums/AlbumTeasers.jsx
- [x] schedules/Schedules.jsx
- [x] schedules/ScheduleForm.jsx
- [x] schedules/ScheduleCategory.jsx
- [x] schedules/ScheduleDict.jsx
- [x] schedules/ScheduleBots.jsx
### 기타
- [x] App.jsx (BrowserView/MobileView 라우팅)
- [x] main.jsx
### CSS 파일
- [x] index.css
- [x] mobile.css (모바일 전용 스타일, 달력 등)
- [x] pc.css (PC 전용 스타일)
### 기타 파일
- [ ] data/dummy.js (개발용 더미 데이터, 필요 시 추가)
- [ ] .env (VITE_KAKAO_JS_KEY - 콘서트 장소 검색용, 관리자 기능에서 사용)
- [ ] public/favicon.ico (필요 시 추가)
## 사용 라이브러리 (package.json)
| 라이브러리 | 용도 | 사용 위치 |
|-----------|------|----------|
| react-device-detect | PC/Mobile 분기 | App.jsx |
| @tanstack/react-query | 데이터 패칭 | 모든 페이지 |
| @tanstack/react-virtual | 가상화 리스트 | 관리자 일정 목록 |
| react-calendar | 캘린더 | 일정 페이지 |
| react-colorful | 색상 선택 | 카테고리 관리 |
| react-photo-album | 앨범 갤러리 | 앨범 갤러리 |
| react-infinite-scroll-component | 무한 스크롤 | 일정 검색 |
| react-intersection-observer | 뷰포트 감지 | 애니메이션 |
| react-ios-time-picker | 시간 선택 | 일정 폼 |
| react-linkify | URL 자동 링크 | 일정 상세 |
| react-window | 가상화 리스트 | 긴 목록 |
| swiper | 슬라이더 | 앨범 상세 |
| canvas-confetti | 축하 효과 | 생일 페이지 |
| framer-motion | 애니메이션 | 전체 |
| dayjs | 날짜 처리 | 전체 |
| zustand | 상태 관리 | 전체 |
## 특수 패턴 및 주의사항
### React Query 고급 사용
- `useInfiniteQuery` - 일정 검색 무한 스크롤 (mobile/Schedule)
- `useQuery` - 일반 데이터 패칭
### Swiper 사용 시
```jsx
import { Swiper, SwiperSlide } from 'swiper/react';
import { Virtual } from 'swiper/modules';
import 'swiper/css';
```
- 사용 위치: mobile/Members, mobile/AlbumDetail, mobile/AlbumGallery
### 가상화 리스트
- `useVirtualizer` from `@tanstack/react-virtual`
- 사용 위치: mobile/Schedule, admin/AdminSchedule
### 교차 관찰자
- `useInView` from `react-intersection-observer`
- 사용 위치: mobile/Schedule (무한 스크롤 트리거)
### 축하 효과
- `canvas-confetti` - 생일 페이지 폭죽 효과
- 사용 위치: pc/Birthday, mobile/Schedule (생일 일정)
### 색상 선택기
- `HexColorPicker` from `react-colorful`
- 사용 위치: admin/AdminScheduleCategory
### 앨범 갤러리
- `RowsPhotoAlbum` from `react-photo-album`
- 사용 위치: pc/AlbumGallery, mobile/AlbumGallery
### Kakao API (미구현)
- 콘서트 장소 검색: `/api/admin/kakao/places`
- 환경변수: `VITE_KAKAO_JS_KEY`
- 사용 예정: 콘서트 일정 추가 시 장소 검색
## 마이그레이션 진행 상황
### 완료된 작업
- [x] Phase 1: 프로젝트 셋업
- [x] Phase 2: 유틸리티 및 상수
- [x] Phase 3: Zustand 스토어
- [x] Phase 4: API 계층 (공개 API)
- [x] Phase 5: 커스텀 훅
- [x] Phase 6: 공통 컴포넌트
- [x] Phase 7: PC/Mobile 레이아웃 컴포넌트
- [x] Phase 8: App.jsx (BrowserView/MobileView 라우팅)
- [x] Phase 9: 공개 페이지 전체
- Home, Members, Album, AlbumDetail, AlbumGallery, TrackDetail
- Schedule, ScheduleDetail, Birthday
### 미완료 작업
#### 공개 영역
- ✅ 모두 완료
#### 관리자 영역
- [x] 관리자 API 전체 (api/pc/admin/)
- [x] 관리자 컴포넌트 전체 (components/pc/admin/)
- [x] 관리자 페이지 전체 (pages/pc/admin/)
- [x] useToast 훅 (hooks/common/)
- [x] useAdminAuth 훅 (hooks/pc/admin/)
### 최종 검증
- [ ] 모든 라우트 동작 확인
- [ ] PC/Mobile 전환 테스트
- [ ] 관리자 기능 테스트
- [ ] frontend-temp → frontend 교체

View file

@ -9,29 +9,11 @@ export const CATEGORY_ID = {
X: 3, X: 3,
}; };
/** 카테고리 이름 매핑 */
export const CATEGORY_NAMES = {
[CATEGORY_ID.DEFAULT]: '기본',
[CATEGORY_ID.YOUTUBE]: 'YouTube',
[CATEGORY_ID.X]: 'X (Twitter)',
};
/** 공식 SNS 링크 */ /** 공식 SNS 링크 */
export const SOCIAL_LINKS = { export const SOCIAL_LINKS = {
youtube: 'https://www.youtube.com/@fromis9_official', youtube: 'https://www.youtube.com/@fromis9_official',
instagram: 'https://www.instagram.com/officialfromis_9', instagram: 'https://www.instagram.com/officialfromis_9',
twitter: 'https://twitter.com/realfromis_9', x: 'https://twitter.com/realfromis_9',
tiktok: 'https://www.tiktok.com/@officialfromis_9',
fancafe: 'https://cafe.daum.net/officialfromis9',
};
/** 앨범 타입 */
export const ALBUM_TYPES = {
FULL: '정규',
MINI: '미니',
SINGLE: '싱글',
DIGITAL: '디지털',
OST: 'OST',
}; };
/** 타임존 */ /** 타임존 */

View file

@ -50,16 +50,10 @@ export function getScheduleTime(schedule) {
/** /**
* 스케줄에서 멤버 이름 목록 추출 * 스케줄에서 멤버 이름 목록 추출
* 다양한 형식 처리 (member_names 문자열, 문자열 배열, 객체 배열)
* @param {object} schedule - 스케줄 객체 * @param {object} schedule - 스케줄 객체
* @returns {string[]} 멤버 이름 배열 * @returns {string[]} 멤버 이름 배열
*/ */
export function getMemberList(schedule) { export function getMemberList(schedule) {
// member_names 문자열이 있으면 사용 (쉼표 구분)
if (schedule.member_names) {
return schedule.member_names.split(',').map((n) => n.trim()).filter(Boolean);
}
const members = schedule.members || []; const members = schedule.members || [];
if (members.length === 0) return []; if (members.length === 0) return [];