fromis_9/docs/PROJECT_STRUCTURE.md
caadiq 233b76355e refactor: 일정 페이지 레이아웃을 일정 관리 페이지와 동일하게 수정
- 화면 고정 레이아웃으로 변경 (h-[calc(100vh-64px)] overflow-hidden)
- grid grid-cols-3 레이아웃으로 변경
- 왼쪽 달력/카테고리 영역 고정, 오른쪽 일정 목록만 스크롤
- Layout.jsx에서 일정 페이지 Footer 숨김 처리
2026-01-11 11:26:17 +09:00

14 KiB

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. 시스템 아키텍처 개요

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 분기)

// 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시간 주기 전체 동기화

스케줄러 동작 흐름

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가지 해상도로 변환:

// 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

인덱스 설정

// 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)

# /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. 개발 환경 시작

# 개발 모드 (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 수집