# 프로젝트 구조 ## 디렉토리 구조 ``` 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 라우트 │ │ │ ├── admin/ # 관리자 API │ │ │ │ ├── bots.js # 봇 관리 │ │ │ │ ├── youtube.js # YouTube 일정 관리 │ │ │ │ └── x.js # X 일정 관리 │ │ │ ├── albums/ │ │ │ │ ├── index.js # 앨범 CRUD │ │ │ │ ├── photos.js # 앨범 사진 관리 │ │ │ │ └── teasers.js # 앨범 티저 관리 │ │ │ ├── auth.js # 인증 (로그인, 토큰 검증) │ │ │ ├── members/ │ │ │ │ └── index.js # 멤버 조회/수정 │ │ │ ├── schedules/ │ │ │ │ ├── index.js # 일정 조회/검색/삭제 │ │ │ │ └── suggestions.js # 추천 검색어 │ │ │ ├── stats/ │ │ │ │ └── index.js # 통계 조회 │ │ │ └── index.js # 라우트 등록 │ │ ├── services/ # 비즈니스 로직 │ │ │ ├── youtube/ # YouTube 봇 │ │ │ ├── x/ # X(Twitter) 봇 │ │ │ ├── meilisearch/ # 검색 서비스 │ │ │ └── suggestions/ # 추천 검색어 │ │ ├── app.js # Fastify 앱 설정 │ │ └── server.js # 진입점 │ ├── Dockerfile # 백엔드 컨테이너 │ └── package.json │ ├── frontend/ # React 프론트엔드 │ ├── src/ │ │ ├── api/ # API 클라이언트 │ │ │ ├── index.js │ │ │ ├── client.js # fetchApi, fetchAuthApi │ │ │ ├── public/ # 공개 API │ │ │ │ ├── albums.js │ │ │ │ ├── members.js │ │ │ │ └── schedules.js │ │ │ └── admin/ # 관리자 API │ │ │ ├── albums.js │ │ │ ├── members.js │ │ │ ├── schedules.js │ │ │ ├── categories.js │ │ │ ├── stats.js │ │ │ ├── bots.js │ │ │ ├── auth.js │ │ │ └── suggestions.js │ │ │ │ │ ├── hooks/ # 커스텀 훅 │ │ │ ├── index.js │ │ │ ├── common/ # 공통 훅 │ │ │ │ └── useToast.js │ │ │ └── pc/ │ │ │ └── admin/ # 관리자 훅 │ │ │ ├── useAdminAuth.js │ │ │ └── useScheduleSearch.js │ │ │ │ │ ├── stores/ # Zustand 스토어 │ │ │ ├── index.js │ │ │ ├── useScheduleStore.js │ │ │ └── useAuthStore.js │ │ │ │ │ ├── utils/ # 유틸리티 │ │ │ ├── index.js │ │ │ ├── cn.js # className 병합 │ │ │ ├── color.js # 색상 상수/유틸 │ │ │ ├── confetti.js # 생일 축하 효과 │ │ │ ├── date.js # 날짜 포맷 │ │ │ ├── format.js # 문자열 포맷 │ │ │ ├── schedule.js # 일정 관련 유틸 │ │ │ └── youtube.js # YouTube URL 파싱 │ │ │ │ │ ├── constants/ │ │ │ └── index.js # 상수 정의 │ │ │ │ │ ├── components/ │ │ │ ├── index.js │ │ │ ├── common/ # 공통 컴포넌트 │ │ │ │ ├── Loading.jsx │ │ │ │ ├── ErrorBoundary.jsx │ │ │ │ ├── ErrorMessage.jsx │ │ │ │ ├── Toast.jsx │ │ │ │ ├── Tooltip.jsx │ │ │ │ ├── Lightbox.jsx │ │ │ │ ├── MobileLightbox.jsx │ │ │ │ ├── LightboxIndicator.jsx │ │ │ │ ├── AnimatedNumber.jsx │ │ │ │ └── ScrollToTop.jsx │ │ │ │ │ │ │ ├── pc/ │ │ │ │ ├── public/ # PC 공개 컴포넌트 │ │ │ │ │ ├── layout/ │ │ │ │ │ │ ├── Layout.jsx │ │ │ │ │ │ ├── Header.jsx │ │ │ │ │ │ └── Footer.jsx │ │ │ │ │ └── schedule/ │ │ │ │ │ ├── Calendar.jsx │ │ │ │ │ ├── ScheduleCard.jsx │ │ │ │ │ ├── BirthdayCard.jsx │ │ │ │ │ └── CategoryFilter.jsx │ │ │ │ │ │ │ │ │ └── admin/ # PC 관리자 컴포넌트 │ │ │ │ ├── layout/ │ │ │ │ │ ├── Layout.jsx │ │ │ │ │ └── Header.jsx │ │ │ │ ├── common/ │ │ │ │ │ ├── ConfirmDialog.jsx │ │ │ │ │ ├── DatePicker.jsx │ │ │ │ │ ├── TimePicker.jsx │ │ │ │ │ ├── NumberPicker.jsx │ │ │ │ │ └── CustomSelect.jsx │ │ │ │ ├── schedule/ │ │ │ │ │ ├── AdminScheduleCard.jsx │ │ │ │ │ ├── ScheduleItem.jsx │ │ │ │ │ ├── CategorySelector.jsx │ │ │ │ │ ├── CategoryFormModal.jsx │ │ │ │ │ ├── MemberSelector.jsx │ │ │ │ │ ├── ImageUploader.jsx │ │ │ │ │ ├── LocationSearchDialog.jsx │ │ │ │ │ └── WordItem.jsx │ │ │ │ ├── album/ │ │ │ │ │ ├── TrackItem.jsx │ │ │ │ │ ├── PhotoGrid.jsx │ │ │ │ │ ├── PhotoPreviewModal.jsx │ │ │ │ │ ├── PendingFileItem.jsx │ │ │ │ │ └── BulkEditPanel.jsx │ │ │ │ └── bot/ │ │ │ │ └── BotCard.jsx │ │ │ │ │ │ │ └── mobile/ # 모바일 컴포넌트 │ │ │ ├── layout/ │ │ │ │ ├── Layout.jsx │ │ │ │ ├── Header.jsx │ │ │ │ └── BottomNav.jsx │ │ │ └── schedule/ │ │ │ ├── Calendar.jsx │ │ │ ├── ScheduleCard.jsx │ │ │ ├── ScheduleListCard.jsx │ │ │ ├── ScheduleSearchCard.jsx │ │ │ └── BirthdayCard.jsx │ │ │ │ │ ├── pages/ │ │ │ ├── pc/ │ │ │ │ ├── public/ # PC 공개 페이지 │ │ │ │ │ ├── home/ │ │ │ │ │ │ └── Home.jsx │ │ │ │ │ ├── members/ │ │ │ │ │ │ └── Members.jsx │ │ │ │ │ ├── album/ │ │ │ │ │ │ ├── Album.jsx │ │ │ │ │ │ ├── AlbumDetail.jsx │ │ │ │ │ │ ├── AlbumGallery.jsx │ │ │ │ │ │ └── TrackDetail.jsx │ │ │ │ │ ├── schedule/ │ │ │ │ │ │ ├── Schedule.jsx │ │ │ │ │ │ ├── ScheduleDetail.jsx │ │ │ │ │ │ ├── Birthday.jsx │ │ │ │ │ │ └── sections/ │ │ │ │ │ │ ├── DefaultSection.jsx │ │ │ │ │ │ ├── YoutubeSection.jsx │ │ │ │ │ │ └── XSection.jsx │ │ │ │ │ └── common/ │ │ │ │ │ └── NotFound.jsx │ │ │ │ │ │ │ │ │ └── admin/ # PC 관리자 페이지 │ │ │ │ ├── Login.jsx │ │ │ │ ├── Dashboard.jsx │ │ │ │ ├── members/ │ │ │ │ │ ├── Members.jsx │ │ │ │ │ └── MemberEdit.jsx │ │ │ │ ├── albums/ │ │ │ │ │ ├── Albums.jsx │ │ │ │ │ ├── AlbumForm.jsx │ │ │ │ │ └── AlbumPhotos.jsx │ │ │ │ └── schedules/ │ │ │ │ ├── Schedules.jsx │ │ │ │ ├── ScheduleForm.jsx │ │ │ │ ├── ScheduleDict.jsx │ │ │ │ ├── ScheduleBots.jsx │ │ │ │ ├── ScheduleCategory.jsx │ │ │ │ ├── form/ │ │ │ │ │ ├── YouTubeForm.jsx │ │ │ │ │ └── XForm.jsx │ │ │ │ └── edit/ │ │ │ │ └── YouTubeEdit.jsx │ │ │ │ │ │ │ └── mobile/ # 모바일 페이지 │ │ │ ├── home/ │ │ │ │ └── Home.jsx │ │ │ ├── members/ │ │ │ │ └── Members.jsx │ │ │ ├── album/ │ │ │ │ ├── Album.jsx │ │ │ │ ├── AlbumDetail.jsx │ │ │ │ ├── AlbumGallery.jsx │ │ │ │ └── TrackDetail.jsx │ │ │ ├── schedule/ │ │ │ │ ├── Schedule.jsx │ │ │ │ └── ScheduleDetail.jsx │ │ │ └── common/ │ │ │ └── NotFound.jsx │ │ │ │ │ ├── routes/ # 라우트 정의 │ │ │ ├── index.js # 라우트 export │ │ │ ├── pc/ │ │ │ │ ├── admin/ │ │ │ │ │ └── index.jsx # PC 관리자 라우트 │ │ │ │ └── public/ │ │ │ │ └── index.jsx # PC 공개 라우트 │ │ │ └── mobile/ │ │ │ └── index.jsx # 모바일 라우트 │ │ │ │ │ ├── App.jsx # PC/모바일 분기 │ │ └── main.jsx │ │ │ ├── vite.config.js │ ├── tailwind.config.js │ ├── Dockerfile │ └── package.json │ ├── docker-compose.yml └── .env ``` ## 서비스 구성 ``` ┌─────────────────────────────────────────────────────────┐ │ Caddy │ │ (리버스 프록시) │ └─────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ fromis9-frontend (:80) │ │ Vite 개발서버 │ │ (프록시: /api → backend) │ └─────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ fromis9-backend (:80) │ │ Fastify API │ └─────────────────────┬───────────────────────────────────┘ │ ┌────────────┼────────────┬────────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ MariaDB │ │Meilisearch│ │ Redis │ │ Nitter │ │ (외부 DB망) │ │ (검색엔진) │ │ (캐시) │ │ (X 스크랩) │ └───────────────┘ └───────────┘ └───────────┘ └───────────┘ ``` ## 데이터베이스 ### 테이블 목록 (25개) #### 사용자/인증 - `admin_users` - 관리자 계정 #### 멤버 - `members` - 멤버 정보 (이름, 생년월일, 인스타그램 등) - `member_nicknames` - 멤버 별명 (검색용) #### 앨범 - `albums` - 앨범 정보 (제목, 발매일, 커버 이미지 등) - `album_tracks` - 앨범 트랙 (곡명, 작사/작곡, 가사 등) - `album_photos` - 앨범 컨셉 포토 - `album_photo_members` - 컨셉 포토-멤버 연결 - `album_teasers` - 앨범 티저 이미지/영상 #### 일정 - `schedules` - 일정 (제목, 날짜, 시간 등) - `schedule_categories` - 일정 카테고리 (유튜브, X, 콘서트 등) - `schedule_members` - 일정-멤버 연결 - `schedule_images` - 일정 첨부 이미지 - `schedule_youtube` - YouTube 영상 연결 정보 - `schedule_x` - X(Twitter) 게시물 연결 정보 - `schedule_concert` - 콘서트 일정 추가 정보 #### 콘서트 - `concert_venues` - 콘서트 장소 정보 - `concert_series` - 콘서트 시리즈 (투어 등) - `concert_series_md` - 콘서트 MD 상품 - `concert_setlists` - 콘서트 셋리스트 - `concert_setlist_members` - 셋리스트-멤버 연결 #### X(Twitter) 프로필 - `x_profiles` - X 프로필 캐시 (프로필 이미지, 이름 등) #### 이미지 - `images` - 이미지 메타데이터 (3개 해상도 URL) #### 추천 검색어 - `suggestion_queries` - 검색 쿼리 로그 - `suggestion_word_pairs` - 단어 bi-gram 빈도 - `suggestion_chosung` - 초성 검색 매핑 ### 검색 인덱스 (Meilisearch) - `schedules` - 일정 검색용 인덱스 - 검색 필드: title, member_names, description, source_name, category_name - 필터: category_id, date - 정렬: date, time