diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e7c1c20 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# fromis_9 프로젝트 + +K-pop 그룹 프로미스나인 팬사이트 + +## 기술 스택 + +- **Frontend**: React 18, Vite, Tailwind CSS, React Query, Zustand +- **Backend**: Fastify, MySQL2, Meilisearch, Redis, AWS S3 +- **Infrastructure**: Docker, Caddy + +## 개발 환경 + +```bash +docker compose up -d --build +docker compose logs -f fromis9-frontend +``` + +## 문서 + +- [docs/migration.md](docs/migration.md) - 마이그레이션 현황 및 남은 작업 +- [docs/architecture.md](docs/architecture.md) - 프로젝트 구조 +- [docs/api.md](docs/api.md) - API 명세 +- [docs/development.md](docs/development.md) - 개발/배포 가이드 diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md deleted file mode 100644 index 47a33d5..0000000 --- a/docs/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,852 +0,0 @@ -# fromis_9 팬사이트 프로젝트 분석 결과 - -> **분석일**: 2026-01-11 -> **프로젝트 경로**: `/docker/fromis_9` -> **사이트 URL**: `https://fromis9.caadiq.co.kr` - ---- - -> [!IMPORTANT] > **현재 개발 환경 활성화 상태** -> `docker-compose.dev.yml`로 실행 중이며, 프론트엔드는 Vite HMR, 백엔드는 Node.js로 분리 운영됩니다. -> -> - **프론트엔드**: `fromis9-frontend` (Vite dev server, 포트 80) -> - **백엔드**: `fromis9-backend` (Express, 포트 3000) -> - 파일 수정 시 **자동 반영** (빌드 불필요) - -## 1. 시스템 아키텍처 개요 - -```mermaid -graph TB - subgraph "클라이언트" - PC[PC 브라우저] - Mobile[모바일 브라우저] - end - - subgraph "Caddy 역방향 프록시" - Caddy[fromis9.caadiq.co.kr
500MB 업로드 허용] - end - - subgraph "Docker 컨테이너" - Frontend[fromis9-frontend:80
React + Express] - Meili[fromis9-meilisearch:7700
검색 엔진] - end - - subgraph "외부 서비스" - MariaDB[(MariaDB
fromis9 DB)] - RustFS[RustFS S3
이미지 스토리지] - YouTube[YouTube API] - Nitter[Nitter
X/Twitter 브릿지] - end - - PC --> Caddy - Mobile --> Caddy - Caddy --> Frontend - Frontend --> MariaDB - Frontend --> Meili - Frontend --> RustFS - Frontend --> YouTube - Frontend --> Nitter -``` - -### 기술 스택 - -| 계층 | 기술 | -| ------------------- | ----------------------------- | -| **프론트엔드** | React 18 + Vite + TailwindCSS | -| **백엔드** | Node.js (Express) | -| **데이터베이스** | MariaDB (`fromis9` DB) | -| **검색 엔진** | Meilisearch v1.6 | -| **미디어 스토리지** | RustFS (S3 호환) | -| **역방향 프록시** | Caddy (SSL 자동화) | - ---- - -## 2. 디렉토리 구조 - -``` -/docker/fromis_9 -├── .env # 환경 변수 (DB, S3, API 키) -├── docker-compose.yml # 프로덕션 오케스트레이션 -├── docker-compose.dev.yml # 개발 환경 (HMR 지원) -├── Dockerfile # 빌드 정의 -│ -├── backend/ # Express API 서버 -│ ├── server.js # 진입점, 라우팅, Meilisearch 초기화 -│ ├── routes/ -│ │ ├── admin.js # 관리자 CRUD (60KB, 핵심 로직) -│ │ ├── albums.js # 앨범 조회 API -│ │ ├── members.js # 멤버 조회 API -│ │ ├── schedules.js # 일정 조회/검색 API -│ │ └── stats.js # 통계 API -│ ├── services/ -│ │ ├── meilisearch.js # 검색 인덱스 관리 -│ │ ├── meilisearch-bot.js # 1시간 주기 동기화 봇 -│ │ ├── youtube-bot.js # YouTube API 수집 봇 -│ │ ├── youtube-scheduler.js # Cron 스케줄러 -│ │ └── x-bot.js # X(Nitter) 수집 봇 -│ └── lib/ -│ ├── db.js # MariaDB 커넥션 풀 -│ └── date.js # Day.js 기반 날짜 유틸리티 -│ -└── frontend/ # React SPA - ├── vite.config.js # 빌드 및 프록시 설정 - ├── tailwind.config.js # 테마 (Primary: #FF4D8D) - └── src/ - ├── App.jsx # 라우팅 (PC/Mobile 분기) - ├── main.jsx # 진입점 - ├── index.css # 글로벌 스타일 - ├── pc.css # PC 전용 스타일 - ├── mobile.css # Mobile 전용 스타일 - ├── pages/ - │ ├── pc/public/ # PC 공개 페이지 - │ ├── pc/admin/ # PC 관리자 페이지 - │ ├── mobile/public/ # Mobile 공개 페이지 - │ └── mobile/admin/ # Mobile 관리자 페이지 - ├── components/ # 재사용 컴포넌트 - ├── api/ # API 호출 유틸리티 - ├── stores/ # Zustand 상태 관리 - └── utils/ # 공통 유틸리티 -``` - ---- - -## 3. 데이터베이스 스키마 (MariaDB `fromis9`) - -### 테이블 목록 (14개) - -``` -admin_users # 관리자 계정 -members # 그룹 멤버 프로필 -albums # 앨범 메타데이터 -tracks # 앨범 트랙 목록 -album_photos # 앨범 컨셉 포토 -album_photo_members # 포토-멤버 매핑 -album_teasers # 티저 미디어 -schedules # 일정/활동 -schedule_categories # 일정 카테고리 -schedule_members # 일정-멤버 매핑 -schedule_images # 일정 이미지 -bots # 자동화 봇 설정 -bot_youtube_config # YouTube 봇 설정 -bot_x_config # X 봇 설정 -``` - -### 주요 테이블 상세 - -#### `members` - 멤버 프로필 - -| 필드 | 타입 | 설명 | -| ---------- | ------------ | ------------- | -| id | int | PK | -| name | varchar(50) | 이름 | -| birth_date | date | 생년월일 | -| position | varchar(100) | 포지션 | -| image_url | varchar(500) | 프로필 이미지 | -| instagram | varchar(200) | 인스타그램 | -| is_former | tinyint | 전 멤버 여부 | - -#### `albums` - 앨범 정보 - -| 필드 | 타입 | 설명 | -| ------------------ | -------------------------- | -------------------- | -| id | int | PK | -| title | varchar(200) | 앨범명 | -| album_type | varchar(100) | 전체 타입명 | -| album_type_short | enum('정규','미니','싱글') | 축약 타입 | -| release_date | date | 발매일 | -| cover_original_url | varchar(500) | 원본 커버 (lossless) | -| cover_medium_url | varchar(500) | 중간 커버 (800px) | -| cover_thumb_url | varchar(500) | 썸네일 (400px) | -| folder_name | varchar(200) | S3 폴더명 | -| description | text | 앨범 설명 | - -#### `schedules` - 일정 - -| 필드 | 타입 | 설명 | -| ------------------ | ------------ | ---------------------------- | -| id | int | PK | -| title | varchar(500) | 일정 제목 | -| category_id | int | FK → schedule_categories | -| date | date | 날짜 | -| time | time | 시간 | -| end_date, end_time | date, time | 종료 시간 | -| description | text | 상세 설명 | -| location\_\* | various | 위치 정보 (이름, 주소, 좌표) | -| source_url | varchar(500) | 출처 URL | -| source_name | varchar(100) | 출처명 | - ---- - -## 4. API 라우트 구조 - -### 공개 API (`/api/*`) - -| 엔드포인트 | 메서드 | 설명 | -| --------------------------- | ------ | ------------------------ | -| `/api/health` | GET | 헬스체크 | -| `/api/members` | GET | 멤버 목록 | -| `/api/albums` | GET | 앨범 목록 (트랙 포함) | -| `/api/albums/by-name/:name` | GET | 앨범명으로 상세 조회 | -| `/api/albums/:id` | GET | ID로 앨범 상세 조회 | -| `/api/schedules` | GET | 일정 목록 (검색, 필터링) | -| `/api/schedules/categories` | GET | 카테고리 목록 | -| `/api/schedules/:id` | GET | 개별 일정 조회 | -| `/api/stats` | GET | 사이트 통계 | - -### 관리자 API (`/api/admin/*`) - -| 엔드포인트 | 메서드 | 설명 | -| ----------------------------------- | --------------- | ----------------- | -| `/api/admin/login` | POST | 로그인 (JWT 발급) | -| `/api/admin/verify` | GET | 토큰 검증 | -| `/api/admin/albums` | POST/PUT/DELETE | 앨범 CRUD | -| `/api/admin/albums/:albumId/photos` | POST/DELETE | 컨셉 포토 관리 | -| `/api/admin/schedules` | POST/PUT/DELETE | 일정 CRUD | -| `/api/admin/bots` | GET/POST/PUT | 봇 관리 | - ---- - -## 5. 프론트엔드 라우팅 (PC/Mobile 분기) - -```jsx -// App.jsx - react-device-detect 사용 - {/* PC 환경 */} - - } /> - } /> - } /> - } /> - } /> - } /> - - - - {/* Mobile 환경 */} - - - - -``` - -### 관리자 페이지 (/admin/\*) - -- `/admin` - 로그인 -- `/admin/dashboard` - 대시보드 -- `/admin/members` - 멤버 관리 -- `/admin/albums` - 앨범 관리 -- `/admin/schedule` - 일정 관리 -- `/admin/schedule/bots` - 봇 관리 - ---- - -## 6. 자동화 봇 시스템 - -### 봇 유형 및 동작 - -| 봇 타입 | 수집 대상 | 동작 방식 | -| --------------- | ------------------ | ---------------------------------------------- | -| **YouTube** | 채널 영상 | YouTube API로 최근 영상 수집, Shorts 자동 판별 | -| **X** | @realfromis_9 트윗 | Nitter 브릿지 → RSS 파싱 | -| **Meilisearch** | 일정 데이터 | 1시간 주기 전체 동기화 | - -### 스케줄러 동작 흐름 - -```mermaid -sequenceDiagram - participant Server as server.js - participant Scheduler as youtube-scheduler.js - participant Bot as youtube-bot.js / x-bot.js - participant DB as MariaDB - participant Meili as Meilisearch - - Server->>Scheduler: initScheduler() - Scheduler->>DB: SELECT * FROM bots WHERE status='running' - Scheduler->>Scheduler: node-cron 등록 - - loop 매 N분 (cron_expression) - Scheduler->>Bot: syncNewVideos() / syncNewTweets() - Bot->>DB: 중복 체크 (source_url) - Bot->>DB: INSERT INTO schedules - Bot->>Meili: addOrUpdateSchedule() - end -``` - ---- - -## 7. 이미지 처리 파이프라인 - -### Sharp 3단계 변환 - -모든 업로드 이미지는 자동으로 3가지 해상도로 변환: - -```javascript -// admin.js 에서 처리 -const [originalBuffer, mediumBuffer, thumbBuffer] = await Promise.all([ - sharp(buffer).webp({ lossless: true }).toBuffer(), // original/ - sharp(buffer).resize(800, null).webp({ quality: 80 }), // medium_800/ - sharp(buffer).resize(400, null).webp({ quality: 80 }), // thumb_400/ -]); -``` - -### RustFS 저장 구조 - -``` -s3.caadiq.co.kr/fromis-9/ -├── albums/{folder_name}/ -│ ├── original/cover.webp -│ ├── medium_800/cover.webp -│ └── thumb_400/cover.webp -└── photos/{album_id}/ - ├── original/{filename}.webp - ├── medium_800/{filename}.webp - └── thumb_400/{filename}.webp -``` - ---- - -## 8. 검색 시스템 (Meilisearch) - -### 검색 특징 - -- **영한 자판 변환**: Inko 라이브러리로 영문 자판 입력 → 한글 변환 -- **유사도 임계값**: 0.5 미만 결과 필터링 -- **검색 대상 필드**: title, member_names, description, source_name, category_name - -### 인덱스 설정 - -```javascript -// meilisearch.js -await index.updateSearchableAttributes([ - "title", - "member_names", - "description", - "source_name", - "category_name", -]); - -await index.updateRankingRules([ - "words", - "typo", - "proximity", - "attribute", - "exactness", - "date:desc", // 최신 날짜 우선 -]); -``` - ---- - -## 9. 네트워크 설정 (Caddy) - -```caddyfile -# /docker/caddy/Caddyfile - -fromis9.caadiq.co.kr { - import custom_errors - - # 대용량 업로드 허용 (500MB) - 컨셉 포토 일괄 업로드용 - request_body { - max_size 500MB - } - - reverse_proxy fromis9-frontend:80 -} -``` - ---- - -## 10. 환경 변수 (.env) - -| 변수 | 용도 | -| ------------------------ | -------------------- | -| `DB_HOST=mariadb` | MariaDB 컨테이너 | -| `DB_NAME=fromis9` | 데이터베이스명 | -| `DB_USER/PASSWORD` | DB 접속 정보 | -| `RUSTFS_ENDPOINT` | RustFS S3 엔드포인트 | -| `RUSTFS_PUBLIC_URL` | 공개 S3 URL | -| `RUSTFS_BUCKET=fromis-9` | S3 버킷명 | -| `YOUTUBE_API_KEY` | YouTube Data API | -| `KAKAO_REST_KEY` | 카카오 API (지도) | -| `MEILI_MASTER_KEY` | Meilisearch 인증 | -| `JWT_SECRET` | 관리자 JWT 서명 | - ---- - -## 11. 개발 환경 시작 - -```bash -# 개발 모드 (HMR 활성화) -cd /docker/fromis_9 -docker compose -f docker-compose.dev.yml up -d - -# 프론트엔드: fromis9-frontend (Vite dev server) -# 백엔드: fromis9-backend (Express) -# 검색: fromis9-meilisearch -``` - -> **참고**: Vite HMR이 활성화되어 있으므로 파일 수정 시 자동 반영됩니다. - ---- - -## 12. 주요 파일 크기 참고 - -| 파일 | 크기 | 비고 | -| ----------------------------------------------- | -------------- | -------------------- | -| `backend/routes/admin.js` | 60KB (1,986줄) | 핵심 CRUD 로직 | -| `frontend/src/pages/pc/public/Schedule.jsx` | 62KB | 일정 페이지 (가상화) | -| `frontend/src/pages/mobile/public/Schedule.jsx` | 52KB | 모바일 일정 | -| `backend/services/youtube-bot.js` | 17KB | YouTube 수집 | -| `backend/services/x-bot.js` | 16KB | X 수집 | - ---- - -## 13. 모바일 앨범 갤러리 UI - -### 주요 컴포넌트 - -| 파일 | 설명 | -| ------------------------------------------------------ | ----------------------------- | -| `frontend/src/pages/mobile/public/AlbumGallery.jsx` | 모바일 앨범 갤러리 (전체보기) | -| `frontend/src/pages/mobile/public/AlbumDetail.jsx` | 모바일 앨범 상세 | -| `frontend/src/components/common/LightboxIndicator.jsx` | 공통 슬라이딩 점 인디케이터 | - -### Swiper ViewPager 스타일 라이트박스 - -```jsx -import { Swiper, SwiperSlide } from "swiper/react"; -import { Virtual } from "swiper/modules"; - - { - swiperRef.current = swiper; - }} - onSlideChange={(swiper) => setSelectedIndex(swiper.activeIndex)} - slidesPerView={1} - resistance={true} - resistanceRatio={0.5} -> - {photos.map((photo, index) => ( - - - - ))} -; -``` - -### LightboxIndicator 사용법 - -```jsx -import LightboxIndicator from '../../../components/common/LightboxIndicator'; - -// PC (기본 width 200px) - swiperRef.current?.slideTo(i)} -/> - -// 모바일 (width 120px로 축소) - swiperRef.current?.slideTo(i)} - width={120} -/> -``` - -### 2열 지그재그 Masonry 그리드 - -```jsx -// 1,3,5번 → 왼쪽 열 / 2,4,6번 → 오른쪽 열 -const distributePhotos = () => { - const leftColumn = []; - const rightColumn = []; - photos.forEach((photo, index) => { - if (index % 2 === 0) leftColumn.push({ ...photo, originalIndex: index }); - else rightColumn.push({ ...photo, originalIndex: index }); - }); - return { leftColumn, rightColumn }; -}; -``` - -### 뒤로가기 처리 패턴 - -```jsx -// 모달/라이트박스 열 때 히스토리 추가 -const openLightbox = useCallback((images, index, options = {}) => { - setLightbox({ open: true, images, index, ...options }); - window.history.pushState({ lightbox: true }, ""); -}, []); - -// popstate 이벤트로 닫기 -useEffect(() => { - const handlePopState = () => { - if (showModal) setShowModal(false); - else if (lightbox.open) setLightbox((prev) => ({ ...prev, open: false })); - }; - window.addEventListener("popstate", handlePopState); - return () => window.removeEventListener("popstate", handlePopState); -}, [showModal, lightbox.open]); - -// X 버튼도 history.back() 호출 -; -``` - -### 바텀시트 (정보 표시) - -```jsx - { - if (info.offset.y > 100 || info.velocity.y > 300) { - window.history.back(); - } - }} - className="bg-zinc-900 rounded-t-3xl" -> - {/* 드래그 핸들 */} -
-
-
- {/* 내용 */} - -``` - ---- - -## 14. Redis 기반 Bi-gram 추천 검색어 시스템 - -### 아키텍처 개요 - -```mermaid -graph LR - User[사용자 검색] --> API[/api/schedules/suggestions] - API --> Redis[(Redis)] - Redis --> |bi-gram 매칭| Results[추천 검색어] - - Admin[관리자 일정 CRUD] --> Extract[키워드 추출] - Extract --> Redis -``` - -### 주요 파일 - -| 파일 | 설명 | -| ----------------------------------------------- | ---------------------------------------------- | -| `backend/routes/schedules.js` | 추천 검색어 API (`/api/schedules/suggestions`) | -| `backend/scripts/extract-keywords.js` | 기존 일정에서 키워드 일괄 추출 스크립트 | -| `frontend/src/pages/pc/admin/Schedule.jsx` | 관리자 검색창 드롭다운 | -| `frontend/src/pages/pc/public/Schedule.jsx` | PC 검색 추천 UI | -| `frontend/src/pages/mobile/public/Schedule.jsx` | 모바일 유튜브 스타일 추천 리스트 | - -### Redis 데이터 구조 - -``` -fromis9:search:suggestions (Sorted Set) -├── "쇼케이스" → score: 15 -├── "팬미팅" → score: 12 -├── "라디오" → score: 8 -└── ... - -fromis9:search:bigrams (Hash) -├── "쇼케" → "쇼케이스,쇼케이스투어" -├── "케이" → "쇼케이스,케이팝" -└── ... -``` - -### API 엔드포인트 - -```javascript -// GET /api/schedules/suggestions?q=쇼케 -// Response: ["쇼케이스", "쇼케이스 투어", ...] - -router.get("/suggestions", async (req, res) => { - const query = req.query.q?.trim(); - if (!query || query.length < 2) return res.json([]); - - // bi-gram 매칭 - const bigram = query.slice(0, 2); - const cached = await redis.hget("fromis9:search:bigrams", bigram); - - if (cached) { - const keywords = cached - .split(",") - .filter((k) => k.toLowerCase().includes(query.toLowerCase())) - .slice(0, 10); - return res.json(keywords); - } - - res.json([]); -}); -``` - -### 키워드 추출 로직 (일정 저장 시) - -```javascript -// admin.js - 일정 저장 시 키워드 추출 -const extractKeywords = (title) => { - // 특수문자 제거, 공백으로 분리 - const words = title.replace(/[^\w\s가-힣]/g, " ").split(/\s+/); - return words.filter((w) => w.length >= 2); -}; - -// Redis에 저장 -for (const keyword of keywords) { - await redis.zincrby("fromis9:search:suggestions", 1, keyword); - - // bi-gram 인덱스 - for (let i = 0; i < keyword.length - 1; i++) { - const bigram = keyword.slice(i, i + 2); - const existing = await redis.hget("fromis9:search:bigrams", bigram); - const set = new Set(existing ? existing.split(",") : []); - set.add(keyword); - await redis.hset("fromis9:search:bigrams", bigram, [...set].join(",")); - } -} -``` - -### 프론트엔드 UI - -#### PC 관리자/공개 페이지 - 드롭다운 - -```jsx -// 검색창 아래 드롭다운 -{ - suggestions.length > 0 && ( -
- {suggestions.map((s, i) => ( - - ))} -
- ); -} -``` - -#### 모바일 - 유튜브 스타일 리스트 - -```jsx -// 검색창 아래 전체 화면 리스트 -{ - showSuggestions && suggestions.length > 0 && ( -
- {suggestions.map((s, i) => ( - - ))} -
- ); -} -``` - -### 키워드 일괄 추출 스크립트 - -```bash -# 기존 일정에서 키워드 추출하여 Redis에 저장 -cd /docker/fromis_9/backend -node scripts/extract-keywords.js -``` - ---- - -## 15. 모바일 앱 (`/app`) - -### 기술 스택 - -| 계층 | 기술 | -| -------------- | ------------------------------------------------------------------------ | -| **프레임워크** | Expo (React Native) | -| **언어** | TypeScript | -| **네비게이션** | React Navigation (Tab + Stack) | -| **UI 효과** | expo-blur, expo-linear-gradient, react-native-color-matrix-image-filters | -| **미디어** | expo-file-system, expo-media-library, react-native-pager-view | - -### 디렉토리 구조 - -``` -app/src/ -├── api/ # API 호출 함수 -│ ├── albums.ts # 앨범 API (AlbumPhoto에 width/height 포함) -│ ├── members.ts # 멤버 API -│ └── schedules.ts # 일정 API -├── components/ # 공통 컴포넌트 -│ └── common/ -│ └── Header.tsx # 공통 헤더 (title, showBack, rightElement) -├── constants/ -│ └── colors.ts # 테마 색상 (primary: #FF4D8D) -├── navigation/ -│ └── AppNavigator.tsx # 탭 + 스택 네비게이션 -└── screens/ - ├── HomeScreen.tsx # 홈 (멤버, 앨범, 일정 요약) - ├── MembersScreen.tsx # 멤버 목록 + 바텀시트 상세 - ├── AlbumScreen.tsx # 앨범 목록 (2열 그리드) - ├── AlbumDetailScreen.tsx # 앨범 상세 (트랙, 티저, 포토) - ├── AlbumGalleryScreen.tsx # 컨셉포토 갤러리 (라이트박스) - └── ScheduleScreen.tsx # 일정 목록 -``` - -### 네비게이션 구조 - -```mermaid -graph TB - TabNav[TabNavigator 하단 탭] - TabNav --> HomeTab[홈] - TabNav --> MembersTab[멤버] - TabNav --> AlbumTab[앨범] - TabNav --> ScheduleTab[일정] - - AlbumTab --> AlbumStack[AlbumStackNavigator] - AlbumStack --> AlbumList[AlbumScreen] - AlbumStack --> AlbumDetail[AlbumDetailScreen] - AlbumStack --> AlbumGallery[AlbumGalleryScreen] -``` - -### 주요 기능 - -#### 탭 전환 시 앨범 스택 리셋 - -```tsx -// AppNavigator.tsx - ({ - tabPress: (e) => { - // 앨범 탭 클릭 시 스택을 루트(목록)으로 리셋 - navigation.navigate("AlbumTab", { screen: "AlbumList" }); - }, - })} -/> -``` - -#### AlbumGalleryScreen (컨셉포토 라이트박스) - -- **PagerView**: 스와이프로 이미지 탐색 -- **페이지 인디케이터**: `n / total` 형식 -- **다운로드 기능**: `expo-file-system` + `expo-media-library` -- 웹 버전과 1:1 동일한 UI - -#### MembersScreen (멤버 상세) - -- **바텀시트 모달**: PanResponder 드래그로 닫기 -- **전 멤버 흑백 처리**: `Grayscale` 필터 -- **글래스모피즘**: `BlurView` (intensity 30, dimezisBlurView) - -### 개발 명령어 - -```bash -# 개발 서버 실행 -cd /docker/fromis_9/app -npx expo start --lan - -# Android APK 빌드 -npx expo run:android --variant release - -# 로컬 네이티브 빌드 (android/ 폴더에서) -./gradlew assembleDebug -``` - -### 웹-앱 동기화 체크리스트 - -| 화면 | 웹 경로 | 앱 파일 | -| ----------- | -------------------------------- | ------------------------ | -| 홈 | `mobile/public/Home.jsx` | `HomeScreen.tsx` | -| 멤버 | `mobile/public/Members.jsx` | `MembersScreen.tsx` | -| 앨범 목록 | `mobile/public/Album.jsx` | `AlbumScreen.tsx` | -| 앨범 상세 | `mobile/public/AlbumDetail.jsx` | `AlbumDetailScreen.tsx` | -| 앨범 갤러리 | `mobile/public/AlbumGallery.jsx` | `AlbumGalleryScreen.tsx` | -| 일정 | `mobile/public/Schedule.jsx` | `ScheduleScreen.tsx` | - ---- - -## 16. 오늘 작업 요약 (2026-01-11 ~ 2026-01-12) - -### 최근 커밋 히스토리 - -| 커밋 | 설명 | -| --------- | ---------------------------------------------------------- | -| `7e570d3` | 모바일 곡 상세: 뒤로가기 헤더 제거 | -| `db6949d` | 모바일 곡 상세: YouTube 전체화면 시 자동 가로 회전 시도 | -| `5f2c86b` | 모바일 곡 상세: 가사 더보기/접기 기능 추가, 하단 여백 조정 | -| `e5d4036` | 모바일: 곡 상세 화면 구현 (TrackDetail 페이지) | -| `67cd681` | PC 곡 상세: TITLE 배지를 노래 제목 옆으로 이동 | -| `0232edc` | PC 곡 상세: 수록곡 섹션 디자인 개선 | -| `4e52f79` | 백엔드: 트랙 상세 API 라우트 순서 수정 | -| `b18183a` | 웹: PC 곡 상세 화면 구현 (TrackDetail 페이지) | -| `dc65858` | 웹: AlbumDetail, AlbumGallery 페이지 useQuery로 리팩토링 | - -### 주요 변경 사항 - -1. **곡 상세 화면 (PC/Mobile)**: 트랙 정보, 크레딧, 가사, 수록곡 목록, 뮤직비디오 임베드 -2. **트랙 상세 API**: `/api/albums/by-name/:albumName/track/:trackTitle` 엔드포인트 추가 -3. **useQuery 리팩토링**: AlbumDetail, AlbumGallery 페이지에 @tanstack/react-query 적용 -4. **모바일 UX 개선**: 가사 더보기/접기, YouTube 전체화면 시 자동 가로 회전 - ---- - -## 17. 곡 상세 화면 (TrackDetail) - -### API 엔드포인트 - -| 엔드포인트 | 메서드 | 설명 | -| -------------------------------------------------- | ------ | --------------------- | -| `/api/albums/by-name/:albumName/track/:trackTitle` | GET | 트랙 상세 + 앨범 정보 | - -### 반환 데이터 - -```json -{ - "id": 1, - "title": "LIKE YOU BETTER", - "track_number": 1, - "is_title_track": 1, - "duration": "3:05", - "lyrics": "...", - "lyricist": "Tomy, HANIHAS(XYXX), ...", - "composer": "HONEY NOISE, ...", - "arranger": "...", - "music_video_url": "https://youtube.com/...", - "album": { - "id": 7, - "title": "From Our 20's", - "album_type": "미니 6집", - "cover_medium_url": "..." - }, - "otherTracks": [...] -} -``` - -### 파일 구조 - -| 플랫폼 | 파일 경로 | 주요 기능 | -| ------ | -------------------------------------------------- | ------------------------------- | -| PC | `frontend/src/pages/pc/public/TrackDetail.jsx` | 크레딧, 가사, 수록곡, MV 임베드 | -| Mobile | `frontend/src/pages/mobile/public/TrackDetail.jsx` | 가사 더보기/접기, 자동 회전 | - -### 주요 기능 - -- **뮤직비디오 임베드**: is_title_track인 경우 YouTube 영상 표시 -- **크레딧 줄바꿈**: 쉼표 기준으로 각 항목 분리 -- **가사 더보기 (모바일)**: 기본 일부만 표시, 버튼으로 전체 펼침 -- **수록곡 목록 (PC)**: 현재 곡 강조, 재생 시간 표시 -- **자동 가로 회전 (모바일)**: YouTube 전체화면 시 `screen.orientation.lock('landscape')` diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..18a793a --- /dev/null +++ b/docs/api.md @@ -0,0 +1,132 @@ +# API 명세 + +Base URL: `/api` + +## 인증 + +### POST /auth/login +로그인 (JWT 토큰 발급) + +### GET /auth/me +현재 사용자 정보 (인증 필요) + +--- + +## 멤버 + +### GET /members +멤버 목록 조회 + +### GET /members/:id +멤버 상세 조회 + +--- + +## 앨범 + +### GET /albums +앨범 목록 조회 + +### GET /albums/:id +앨범 상세 조회 + +--- + +## 일정 + +### GET /schedules +일정 조회 + +**Query Parameters:** +- `year`, `month` - 월별 조회 (필수, search 없을 때) +- `search` - 검색어 (Meilisearch 사용) +- `offset`, `limit` - 페이징 + +**월별 조회 응답:** +```json +{ + "2026-01-18": { + "categories": [ + { "id": 2, "name": "유튜브", "color": "#ff0033", "count": 3 } + ], + "schedules": [ + { + "id": 123, + "title": "...", + "time": "19:00:00", + "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, + "source_name": "fromis_9" + } + ] + } +} +``` + +**검색 응답:** +```json +{ + "schedules": [ + { + "id": 123, + "title": "...", + "datetime": "2026-01-18T19:00:00", + "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, + "source_name": "fromis_9", + "members": ["송하영"], + "_rankingScore": 0.95 + } + ], + "total": 100, + "offset": 0, + "limit": 20, + "hasMore": true +} +``` + +### GET /schedules/:id +일정 상세 조회 + +### POST /schedules/sync-search +Meilisearch 전체 동기화 (인증 필요) + +--- + +## 추천 검색어 + +### GET /schedules/suggestions +추천 검색어 조회 + +**Query Parameters:** +- `q` - 검색어 (2자 이상) +- `limit` - 결과 개수 (기본 10) + +**응답:** +```json +{ + "suggestions": ["송하영", "송하영 직캠", "하영"] +} +``` + +--- + +## 봇 상태 + +### GET /bots +봇 상태 조회 + +--- + +## 헬스 체크 + +### GET /health +서버 상태 확인 + +--- + +## API 문서 + +### GET /docs +Scalar API Reference UI + +### GET /docs/json +OpenAPI JSON 스펙 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..94a83b6 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,100 @@ +# 프로젝트 구조 + +## 디렉토리 구조 + +``` +fromis_9/ +├── backend/ # Fastify 백엔드 (현재 사용) +│ ├── src/ +│ │ ├── config/ +│ │ │ ├── index.js # 환경변수 통합 관리 +│ │ │ └── bots.js # 봇 설정 (YouTube, X) +│ │ ├── plugins/ # Fastify 플러그인 +│ │ │ ├── db.js # MariaDB 연결 +│ │ │ ├── redis.js # Redis 연결 +│ │ │ ├── auth.js # JWT 인증 +│ │ │ ├── meilisearch.js # 검색 엔진 +│ │ │ └── scheduler.js # 봇 스케줄러 +│ │ ├── routes/ # API 라우트 +│ │ │ ├── auth/ +│ │ │ ├── members/ +│ │ │ ├── albums/ +│ │ │ ├── schedules/ +│ │ │ │ ├── index.js # 일정 조회/검색 +│ │ │ │ └── suggestions.js +│ │ │ └── index.js # 라우트 등록 +│ │ ├── services/ # 비즈니스 로직 +│ │ │ ├── youtube/ # YouTube 봇 +│ │ │ ├── x/ # X(Twitter) 봇 +│ │ │ ├── meilisearch/ # 검색 서비스 +│ │ │ └── suggestions/ # 추천 검색어 +│ │ ├── app.js # Fastify 앱 설정 +│ │ └── server.js # 진입점 +│ └── package.json +│ +├── backend-backup/ # Express 백엔드 (참조용, 마이그레이션 원본) +│ +├── frontend/ # React 프론트엔드 +│ ├── src/ +│ │ ├── api/ # API 클라이언트 +│ │ │ ├── index.js # fetchApi 유틸 +│ │ │ ├── public/ # 공개 API +│ │ │ └── admin/ # 어드민 API +│ │ ├── components/ # 공통 컴포넌트 +│ │ ├── pages/ +│ │ │ ├── pc/ # PC 페이지 +│ │ │ └── mobile/ # 모바일 페이지 +│ │ ├── stores/ # Zustand 스토어 +│ │ └── App.jsx +│ ├── vite.config.js +│ └── package.json +│ +├── Dockerfile # 개발/배포 통합 (주석 전환) +├── docker-compose.yml +└── .env +``` + +## 서비스 구성 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Caddy │ +│ (리버스 프록시) │ +└─────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ fromis9-frontend (Docker) │ +│ ┌─────────────────┐ ┌─────────────────────────────┐ │ +│ │ Vite (:80) │───▶│ Fastify (:3000) │ │ +│ │ 프론트엔드 │ │ 백엔드 API │ │ +│ └─────────────────┘ └──────────┬──────────────────┘ │ +└─────────────────────────────────────┼───────────────────┘ + │ + ┌────────────────────────────┼────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ MariaDB │ │ Meilisearch │ │ Redis │ +│ (외부 DB망) │ │ (검색 엔진) │ │ (캐시) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 데이터베이스 + +### 주요 테이블 +- `members` - 멤버 정보 +- `member_nicknames` - 멤버 별명 (검색용) +- `albums` - 앨범 정보 +- `schedules` - 일정 +- `schedule_categories` - 일정 카테고리 +- `schedule_youtube` - YouTube 영상 정보 +- `schedule_x` - X(Twitter) 게시물 정보 +- `schedule_members` - 일정-멤버 연결 +- `images` - 이미지 메타데이터 + +### 검색 인덱스 (Meilisearch) +- `schedules` - 일정 검색용 인덱스 + - 검색 필드: title, member_names, description, source_name, category_name + - 필터: category_id, date + - 정렬: date, time diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..48ececf --- /dev/null +++ b/docs/development.md @@ -0,0 +1,124 @@ +# 개발/배포 가이드 + +## 개발 모드 + +### 실행 +```bash +cd /docker/fromis_9 +docker compose up -d --build +``` + +### 구성 +- **Vite** (포트 80): 프론트엔드 개발 서버, HMR 지원 +- **Fastify** (포트 3000): 백엔드 API, --watch 모드 +- Vite가 `/api`, `/docs` 요청을 localhost:3000으로 프록시 + +### 로그 확인 +```bash +docker compose logs -f fromis9-frontend +``` + +### 코드 수정 +- `frontend/`, `backend/` 폴더가 볼륨 마운트됨 +- 코드 수정 시 자동 반영 (HMR, watch) + +--- + +## 배포 모드 전환 + +### 1. Dockerfile 수정 +```dockerfile +# 개발 모드 주석처리 +# FROM node:20-alpine +# WORKDIR /app +# ... + +# 배포 모드 주석해제 +FROM node:20-alpine AS frontend-builder +... +``` + +### 2. docker-compose.yml 수정 +```yaml +# volumes 주석처리 +# volumes: +# - ./backend:/app/backend +# - ./frontend:/app/frontend +# - backend_modules:/app/backend/node_modules +# - frontend_modules:/app/frontend/node_modules +``` + +### 3. 빌드 및 실행 +```bash +docker compose up -d --build +``` + +--- + +## 환경 변수 (.env) + +```env +# 서버 +PORT=80 + +# 데이터베이스 +DB_HOST=mariadb +DB_PORT=3306 +DB_USER=... +DB_PASSWORD=... +DB_NAME=fromis9 + +# Redis +REDIS_HOST=fromis9-redis +REDIS_PORT=6379 + +# Meilisearch +MEILI_HOST=http://fromis9-meilisearch:7700 +MEILI_MASTER_KEY=... + +# JWT +JWT_SECRET=... + +# AWS S3 +AWS_ACCESS_KEY_ID=... +AWS_SECRET_ACCESS_KEY=... +AWS_REGION=... +S3_BUCKET=... + +# YouTube API +YOUTUBE_API_KEY=... +``` + +--- + +## Caddy 설정 + +`/docker/caddy/Caddyfile`: +```caddyfile +fromis9.caadiq.co.kr { + import custom_errors + request_body { + max_size 500MB + } + reverse_proxy fromis9-frontend:80 +} +``` + +--- + +## 유용한 명령어 + +```bash +# 컨테이너 재시작 +docker compose restart fromis9-frontend + +# 볼륨 포함 완전 재시작 +docker compose down -v && docker compose up -d --build + +# Meilisearch 동기화 +curl -X POST https://fromis9.caadiq.co.kr/api/schedules/sync-search \ + -H "Authorization: Bearer " + +# Redis 확인 +docker exec fromis9-redis redis-cli KEYS "*" +``` diff --git a/docs/handover.md b/docs/handover.md deleted file mode 100644 index 9ec101a..0000000 --- a/docs/handover.md +++ /dev/null @@ -1,163 +0,0 @@ -# fromis_9 프로젝트 인수인계서 - -## 프로젝트 개요 - -fromis_9 K-pop 아이돌 팬사이트 - 웹 프론트엔드, 백엔드 API, 모바일 앱으로 구성 - ---- - -## 1. 디렉토리 구조 - -``` -/docker/fromis_9/ -├── frontend/ # React 웹 프론트엔드 (Vite) -├── backend/ # Express.js 백엔드 API -├── app/ # React Native 모바일 앱 (Expo) -└── .env # 환경변수 (DB 접속정보 등) -``` - ---- - -## 2. 웹 프론트엔드 (`/frontend`) - -### 기술 스택 - -- React + Vite -- TailwindCSS -- framer-motion (애니메이션) - -### 주요 경로 - -- `src/pages/` - 페이지 컴포넌트 - - `pc/public/` - PC 공개 페이지 (Home, Members, Album, AlbumDetail, TrackDetail 등) - - `mobile/public/` - 모바일 전용 페이지 - - `pc/admin/` - 관리자 페이지 -- `src/api/` - API 호출 함수 -- `src/components/` - 재사용 컴포넌트 - ---- - -## 3. 백엔드 (`/backend`) - -### 기술 스택 - -- Express.js -- MariaDB (mysql2) -- RustFS (파일 스토리지) - -### 주요 경로 - -- `routes/` - API 라우트 - - `public/` - 공개 API - - `admin/` - 관리자 API -- `lib/` - 유틸리티 (DB, 파일 업로드 등) - ---- - -## 4. 모바일 앱 (`/app`) - -### 기술 스택 - -- **Expo** (React Native) -- **TypeScript** -- React Navigation (탭 + 스택 네비게이션) -- expo-blur, expo-linear-gradient (UI 효과) - -### 주요 경로 - -``` -app/src/ -├── api/ # API 호출 함수 -│ ├── albums.ts # 앨범 API -│ ├── members.ts # 멤버 API -│ └── schedules.ts # 일정 API -├── components/ # 공통 컴포넌트 -│ └── common/ -│ └── Header.tsx # 공통 헤더 (뒤로가기, 타이틀, rightElement) -├── navigation/ # 네비게이션 설정 -│ └── AppNavigator.tsx -├── screens/ # 화면 컴포넌트 -│ ├── HomeScreen.tsx -│ ├── MembersScreen.tsx -│ ├── AlbumScreen.tsx -│ ├── AlbumDetailScreen.tsx -│ ├── AlbumGalleryScreen.tsx # 컨셉포토 갤러리 (라이트박스) -│ └── ScheduleScreen.tsx -└── constants/ # 상수 (colors 등) -``` - -### 네비게이션 구조 - -``` -TabNavigator (하단 탭) -├── HomeTab → HomeScreen -├── MembersTab → MembersScreen -├── AlbumTab → AlbumStackNavigator -│ ├── AlbumList → AlbumScreen -│ ├── AlbumDetail → AlbumDetailScreen -│ └── AlbumGallery → AlbumGalleryScreen -└── ScheduleTab → ScheduleScreen -``` - -### 주요 기능 - -- **탭 전환 시 앨범 스택 리셋**: 다른 탭 갔다가 앨범 탭 클릭 시 목록으로 돌아감 -- **AlbumGalleryScreen**: 웹과 1:1 동일한 컨셉포토 갤러리 (PagerView 라이트박스, 다운로드) -- **MembersScreen**: 바텀시트 모달, 전 멤버 흑백 처리 - -### 개발 서버 실행 - -```bash -cd /docker/fromis_9/app -npx expo start --lan -``` - -### APK 빌드 - -```bash -npx expo run:android --variant release -# 또는 로컬 빌드 -./gradlew assembleDebug # android/ 폴더에서 -``` - ---- - -## 5. 분석 절차 - -### 5.1 코드 전수 조사 - -```bash -# 프로젝트 구조 확인 -find /docker/fromis_9 -type f -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" | head -50 -``` - -### 5.2 DB 구조 파악 - -```bash -# .env에서 DB 정보 확인 -cat /docker/fromis_9/.env - -# MariaDB 접속 (컨테이너명: mariadb) -docker exec -it mariadb mysql -u [USER] -p[PASSWORD] fromis9 - -# 테이블 목록 -SHOW TABLES; - -# 테이블 스키마 -DESCRIBE [table_name]; -``` - -### 5.3 Caddy 설정 확인 - -```bash -cat /docker/caddy/Caddyfile | grep -A 20 "fromis9" -``` - ---- - -## 6. 주의사항 - -- **앱 HMR**: Vite처럼 자동 반영, 빌드 불필요 -- **앱 테스트**: 흔들어서 → Reload로 확인 -- **DB 접속**: `.env` 파일의 실제 자격증명 사용 -- **웹/앱 1:1 동기화**: 기능 추가 시 웹과 앱 모두 구현 필요 diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..03b68c3 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,54 @@ +# 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`) +- [x] 멤버 (`/api/members`) +- [x] 앨범 (`/api/albums`) +- [x] 일정 (`/api/schedules`) + - 월별 조회 (생일 일정 포함) + - Meilisearch 검색 + - 별명 → 멤버이름 변환 + - 영문자판 → 한글 변환 +- [x] 추천 검색어 (`/api/schedules/suggestions`) + - kiwi-nlp 형태소 분석 + - bi-gram 자동완성 + +### 서비스 (`src/services/`) +- [x] YouTube 봇 - 영상 자동 수집 +- [x] X(Twitter) 봇 - Nitter 스크래핑 +- [x] Meilisearch 검색 +- [x] 추천 검색어 + +## 남은 작업 + +### 어드민 API +- [ ] 일정 CRUD (`POST/PUT/DELETE /api/schedules`) +- [ ] 이미지 업로드 (`/api/images`) +- [ ] 멤버 관리 (`POST/PUT/DELETE /api/members`) +- [ ] 앨범 관리 (`POST/PUT/DELETE /api/albums`) +- [ ] 카테고리 관리 (`/api/categories`) + +### 기타 +- [ ] 통계 API (`/api/stats`) +- [ ] 어드민 사전 관리 (형태소 분석용) + +## 참고 사항 + +- 기존 Express 코드는 `backend-backup/` 폴더에 보존 +- 마이그레이션 시 기존 코드 참조하여 동일 기능 구현 +- DB 스키마는 변경 없음 (기존 테이블 그대로 사용)