diff --git a/app/src/navigation/AppNavigator.tsx b/app/src/navigation/AppNavigator.tsx index da42709..06bae34 100644 --- a/app/src/navigation/AppNavigator.tsx +++ b/app/src/navigation/AppNavigator.tsx @@ -121,7 +121,16 @@ export default function AppNavigator() { > - + ({ + tabPress: (e) => { + // 앨범 탭 클릭 시 스택을 루트(목록)으로 리셋 + navigation.navigate('AlbumTab', { screen: 'AlbumList' }); + }, + })} + /> diff --git a/app/src/screens/AlbumScreen.tsx b/app/src/screens/AlbumScreen.tsx index 1a51c99..8e565c0 100644 --- a/app/src/screens/AlbumScreen.tsx +++ b/app/src/screens/AlbumScreen.tsx @@ -74,28 +74,32 @@ export default function AlbumScreen() { {/* 2열 그리드 */} {albums.map((album) => ( - navigation.navigate('AlbumDetail', { name: album.folder_name })} - style={styles.albumCard} - activeOpacity={0.7} + style={styles.albumCardWrapper} > - - - - - - {album.title} - - - {album.album_type_short} · {album.release_date?.slice(0, 4)} - - - + navigation.navigate('AlbumDetail', { name: album.folder_name })} + style={styles.albumCard} + activeOpacity={0.7} + > + + + + + + {album.title} + + + {album.album_type_short} · {album.release_date?.slice(0, 4)} + + + + ))} @@ -124,17 +128,19 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', justifyContent: 'space-between', }, - albumCard: { + albumCardWrapper: { width: '48%', + marginBottom: 16, + }, + albumCard: { backgroundColor: '#FFFFFF', borderRadius: 16, overflow: 'hidden', - marginBottom: 16, shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 3, - elevation: 2, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, }, albumImageContainer: { aspectRatio: 1, diff --git a/app/src/screens/HomeScreen.tsx b/app/src/screens/HomeScreen.tsx index f9ae906..2b4963c 100644 --- a/app/src/screens/HomeScreen.tsx +++ b/app/src/screens/HomeScreen.tsx @@ -36,8 +36,8 @@ export default function HomeScreen() { const [refreshing, setRefreshing] = useState(false); // 애니메이션 - const fadeAnim = useRef(new Animated.Value(0)).current; - const slideAnim = useRef(new Animated.Value(20)).current; + const fadeAnim = useRef(new Animated.Value(1)).current; + const slideAnim = useRef(new Animated.Value(0)).current; const fetchData = async () => { try { @@ -49,20 +49,6 @@ export default function HomeScreen() { setMembers(membersData.filter(m => !m.is_former)); setAlbums(albumsData.slice(0, 2)); setSchedules(schedulesData || []); - - // 애니메이션 시작 - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(slideAnim, { - toValue: 0, - duration: 500, - useNativeDriver: true, - }), - ]).start(); } catch (error) { console.error('데이터 로드 오류:', error); } finally { @@ -77,8 +63,6 @@ export default function HomeScreen() { const onRefresh = () => { setRefreshing(true); - fadeAnim.setValue(0); - slideAnim.setValue(20); fetchData(); }; diff --git a/app/src/screens/MembersScreen.tsx b/app/src/screens/MembersScreen.tsx index 3ba132d..0ee0810 100644 --- a/app/src/screens/MembersScreen.tsx +++ b/app/src/screens/MembersScreen.tsx @@ -74,17 +74,21 @@ export default function MembersScreen() { }) ).current; + const startAnimation = () => { + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }).start(); + }; + const fetchMembers = async () => { try { const data = await getMembers(); setMembers(data.filter(m => !m.is_former)); setFormerMembers(data.filter(m => m.is_former)); - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }).start(); + startAnimation(); } catch (error) { console.error('멤버 로드 오류:', error); } finally { @@ -99,7 +103,6 @@ export default function MembersScreen() { const onRefresh = () => { setRefreshing(true); - fadeAnim.setValue(0); fetchMembers(); }; diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 411cfef..532ea57 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -669,18 +669,129 @@ node scripts/extract-keywords.js --- -## 15. 오늘 작업 요약 (2026-01-11) +## 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 + ({ + 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) ### 커밋 히스토리 -| 커밋 | 설명 | -| --------- | ------------------------------------------------------- | -| `727b05f` | Redis 기반 bi-gram 추천 검색어 시스템 구현 | -| `9c2ff74` | Admin Schedule 추천 검색어 연동 + 빈 상태 드롭다운 숨김 | -| `2ad5341` | Mobile Schedule 추천 검색어 API 연동 | -| `8e3cab9` | 기본 카테고리 보호 및 ID 재정렬 | -| `de2e02f` | 모바일 앨범 상세 UI 개선 | -| `d6bc8d7` | 모바일 앨범 갤러리 UI 대폭 개선 (Swiper, 뒤로가기 등) | +| 커밋 | 설명 | +| --------- | ----------------------------------------------------------------- | +| `727b05f` | Redis 기반 bi-gram 추천 검색어 시스템 구현 | +| `9c2ff74` | Admin Schedule 추천 검색어 연동 + 빈 상태 드롭다운 숨김 | +| `2ad5341` | Mobile Schedule 추천 검색어 API 연동 | +| `8e3cab9` | 기본 카테고리 보호 및 ID 재정렬 | +| `de2e02f` | 모바일 앨범 상세 UI 개선 | +| `d6bc8d7` | 모바일 앨범 갤러리 UI 대폭 개선 (Swiper, 뒤로가기 등) | +| `bf6b7f7` | 앱: 앨범 화면 2열 그리드 레이아웃 개선, 탭 전환 시 앨범 스택 리셋 | ### 주요 변경 사항 @@ -688,3 +799,4 @@ node scripts/extract-keywords.js 2. **모바일 앨범 갤러리**: Swiper ViewPager + LightboxIndicator 3. **뒤로가기 처리**: History API로 모달/라이트박스 닫기 4. **카테고리 보호**: 시스템 기본 카테고리 삭제 방지 +5. **앱 앨범 스택 리셋**: 탭 전환 시 목록으로 자동 복귀 diff --git a/docs/handover.md b/docs/handover.md new file mode 100644 index 0000000..e6dcd22 --- /dev/null +++ b/docs/handover.md @@ -0,0 +1,149 @@ +# 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/` - 페이지 컴포넌트 + - `public/` - 공개 페이지 (Home, Members, Album 등) + - `mobile/public/` - 모바일 전용 페이지 + - `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 동기화**: 기능 추가 시 웹과 앱 모두 구현 필요 diff --git a/frontend/src/pages/mobile/public/Album.jsx b/frontend/src/pages/mobile/public/Album.jsx index 97b3d6d..22a4d2d 100644 --- a/frontend/src/pages/mobile/public/Album.jsx +++ b/frontend/src/pages/mobile/public/Album.jsx @@ -36,7 +36,7 @@ function MobileAlbum() { animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }} onClick={() => navigate(`/album/${album.folder_name}`)} - className="bg-white rounded-2xl overflow-hidden shadow-sm" + className="bg-white rounded-2xl overflow-hidden shadow-md" >
{album.cover_thumb_url && (