400 lines
14 KiB
Markdown
400 lines
14 KiB
Markdown
|
|
# 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<br/>500MB 업로드 허용]
|
||
|
|
end
|
||
|
|
|
||
|
|
subgraph "Docker 컨테이너"
|
||
|
|
Frontend[fromis9-frontend:80<br/>React + Express]
|
||
|
|
Meili[fromis9-meilisearch:7700<br/>검색 엔진]
|
||
|
|
end
|
||
|
|
|
||
|
|
subgraph "외부 서비스"
|
||
|
|
MariaDB[(MariaDB<br/>fromis9 DB)]
|
||
|
|
RustFS[RustFS S3<br/>이미지 스토리지]
|
||
|
|
YouTube[YouTube API]
|
||
|
|
Nitter[Nitter<br/>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 사용
|
||
|
|
<BrowserView> {/* PC 환경 */}
|
||
|
|
<PCLayout>
|
||
|
|
<Route path="/" element={<PCHome />} />
|
||
|
|
<Route path="/members" element={<PCMembers />} />
|
||
|
|
<Route path="/album" element={<PCAlbum />} />
|
||
|
|
<Route path="/album/:name" element={<PCAlbumDetail />} />
|
||
|
|
<Route path="/album/:name/gallery" element={<PCAlbumGallery />} />
|
||
|
|
<Route path="/schedule" element={<PCSchedule />} />
|
||
|
|
</PCLayout>
|
||
|
|
</BrowserView>
|
||
|
|
|
||
|
|
<MobileView> {/* Mobile 환경 */}
|
||
|
|
<MobileLayout>
|
||
|
|
<!-- 동일한 라우트, 다른 컴포넌트 -->
|
||
|
|
</MobileLayout>
|
||
|
|
</MobileView>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 관리자 페이지 (/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 수집 |
|