docs: 프로젝트 개발환경 문서 추가
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
abe9687cc8
commit
f762302689
7 changed files with 433 additions and 1015 deletions
23
CLAUDE.md
Normal file
23
CLAUDE.md
Normal file
|
|
@ -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) - 개발/배포 가이드
|
||||
|
|
@ -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<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 수집 |
|
||||
|
||||
---
|
||||
|
||||
## 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";
|
||||
|
||||
<Swiper
|
||||
modules={[Virtual]}
|
||||
virtual
|
||||
initialSlide={selectedIndex}
|
||||
onSwiper={(swiper) => {
|
||||
swiperRef.current = swiper;
|
||||
}}
|
||||
onSlideChange={(swiper) => setSelectedIndex(swiper.activeIndex)}
|
||||
slidesPerView={1}
|
||||
resistance={true}
|
||||
resistanceRatio={0.5}
|
||||
>
|
||||
{photos.map((photo, index) => (
|
||||
<SwiperSlide key={index} virtualIndex={index}>
|
||||
<img src={photo.medium_url} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>;
|
||||
```
|
||||
|
||||
### LightboxIndicator 사용법
|
||||
|
||||
```jsx
|
||||
import LightboxIndicator from '../../../components/common/LightboxIndicator';
|
||||
|
||||
// PC (기본 width 200px)
|
||||
<LightboxIndicator
|
||||
count={photos.length}
|
||||
currentIndex={selectedIndex}
|
||||
goToIndex={(i) => swiperRef.current?.slideTo(i)}
|
||||
/>
|
||||
|
||||
// 모바일 (width 120px로 축소)
|
||||
<LightboxIndicator
|
||||
count={photos.length}
|
||||
currentIndex={selectedIndex}
|
||||
goToIndex={(i) => 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() 호출
|
||||
<button onClick={() => window.history.back()}>
|
||||
<X size={24} />
|
||||
</button>;
|
||||
```
|
||||
|
||||
### 바텀시트 (정보 표시)
|
||||
|
||||
```jsx
|
||||
<motion.div
|
||||
initial={{ y: "100%" }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: "100%" }}
|
||||
drag="y"
|
||||
dragConstraints={{ top: 0, bottom: 0 }}
|
||||
dragElastic={{ top: 0, bottom: 0.5 }}
|
||||
onDragEnd={(_, info) => {
|
||||
if (info.offset.y > 100 || info.velocity.y > 300) {
|
||||
window.history.back();
|
||||
}
|
||||
}}
|
||||
className="bg-zinc-900 rounded-t-3xl"
|
||||
>
|
||||
{/* 드래그 핸들 */}
|
||||
<div className="flex justify-center pt-3 pb-2">
|
||||
<div className="w-10 h-1 bg-zinc-600 rounded-full" />
|
||||
</div>
|
||||
{/* 내용 */}
|
||||
</motion.div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 && (
|
||||
<div className="absolute top-full left-0 right-0 bg-white border rounded-lg shadow-lg z-50">
|
||||
{suggestions.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSuggestionClick(s)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-gray-100"
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 모바일 - 유튜브 스타일 리스트
|
||||
|
||||
```jsx
|
||||
// 검색창 아래 전체 화면 리스트
|
||||
{
|
||||
showSuggestions && suggestions.length > 0 && (
|
||||
<div className="absolute inset-x-0 top-12 bottom-0 bg-white z-50">
|
||||
{suggestions.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSuggestionClick(s)}
|
||||
className="w-full px-4 py-3 flex items-center gap-3 border-b"
|
||||
>
|
||||
<Search size={16} className="text-gray-400" />
|
||||
<span>{s}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 키워드 일괄 추출 스크립트
|
||||
|
||||
```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
|
||||
<Tab.Screen
|
||||
name="AlbumTab"
|
||||
component={AlbumStackNavigator}
|
||||
listeners={({ navigation }) => ({
|
||||
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')`
|
||||
132
docs/api.md
Normal file
132
docs/api.md
Normal file
|
|
@ -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 스펙
|
||||
100
docs/architecture.md
Normal file
100
docs/architecture.md
Normal file
|
|
@ -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
|
||||
124
docs/development.md
Normal file
124
docs/development.md
Normal file
|
|
@ -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 <token>"
|
||||
|
||||
# Redis 확인
|
||||
docker exec fromis9-redis redis-cli KEYS "*"
|
||||
```
|
||||
163
docs/handover.md
163
docs/handover.md
|
|
@ -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 동기화**: 기능 추가 시 웹과 앱 모두 구현 필요
|
||||
54
docs/migration.md
Normal file
54
docs/migration.md
Normal file
|
|
@ -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 스키마는 변경 없음 (기존 테이블 그대로 사용)
|
||||
Loading…
Add table
Reference in a new issue