diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..f5c3e05 --- /dev/null +++ b/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,399 @@ +# 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 수집 | diff --git a/frontend/src/components/pc/Layout.jsx b/frontend/src/components/pc/Layout.jsx index 5c8366d..38bb587 100644 --- a/frontend/src/components/pc/Layout.jsx +++ b/frontend/src/components/pc/Layout.jsx @@ -1,13 +1,18 @@ +import { useLocation } from 'react-router-dom'; import Header from './Header'; import Footer from './Footer'; import '../../pc.css'; function Layout({ children }) { + const location = useLocation(); + // 일정 페이지에서는 Footer 숨김 (화면 고정 레이아웃) + const hideFooter = location.pathname === '/schedule'; + return (
{children}
-
); } diff --git a/frontend/src/pages/pc/public/Schedule.jsx b/frontend/src/pages/pc/public/Schedule.jsx index 02f5882..22066d7 100644 --- a/frontend/src/pages/pc/public/Schedule.jsx +++ b/frontend/src/pages/pc/public/Schedule.jsx @@ -392,10 +392,10 @@ function Schedule() { }, [categories, categoryCounts]); return ( -
-
+
+
{/* 헤더 */} -
+
-
+
{/* 왼쪽: 달력 + 카테고리 */} -
+
{/* 달력 */} {/* 스케줄 리스트 */} -
+
{/* 헤더 */}
@@ -824,7 +824,7 @@ function Schedule() {
{loading ? (
로딩 중...