앱: 애니메이션 및 네비게이션 개선, 문서 업데이트

This commit is contained in:
caadiq 2026-01-12 15:44:05 +09:00
parent e52951f43c
commit a89139f056
7 changed files with 324 additions and 61 deletions

View file

@ -121,7 +121,16 @@ export default function AppNavigator() {
> >
<Tab.Screen name="HomeTab" component={HomeScreen} /> <Tab.Screen name="HomeTab" component={HomeScreen} />
<Tab.Screen name="MembersTab" component={MembersScreen} /> <Tab.Screen name="MembersTab" component={MembersScreen} />
<Tab.Screen name="AlbumTab" component={AlbumStackNavigator} /> <Tab.Screen
name="AlbumTab"
component={AlbumStackNavigator}
listeners={({ navigation }) => ({
tabPress: (e) => {
// 앨범 탭 클릭 시 스택을 루트(목록)으로 리셋
navigation.navigate('AlbumTab', { screen: 'AlbumList' });
},
})}
/>
<Tab.Screen name="ScheduleTab" component={ScheduleScreen} /> <Tab.Screen name="ScheduleTab" component={ScheduleScreen} />
</Tab.Navigator> </Tab.Navigator>
</NavigationContainer> </NavigationContainer>

View file

@ -74,8 +74,11 @@ export default function AlbumScreen() {
{/* 2열 그리드 */} {/* 2열 그리드 */}
<View style={styles.grid}> <View style={styles.grid}>
{albums.map((album) => ( {albums.map((album) => (
<TouchableOpacity <View
key={album.id} key={album.id}
style={styles.albumCardWrapper}
>
<TouchableOpacity
onPress={() => navigation.navigate('AlbumDetail', { name: album.folder_name })} onPress={() => navigation.navigate('AlbumDetail', { name: album.folder_name })}
style={styles.albumCard} style={styles.albumCard}
activeOpacity={0.7} activeOpacity={0.7}
@ -96,6 +99,7 @@ export default function AlbumScreen() {
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View>
))} ))}
</View> </View>
</ScrollView> </ScrollView>
@ -124,17 +128,19 @@ const styles = StyleSheet.create({
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
albumCard: { albumCardWrapper: {
width: '48%', width: '48%',
marginBottom: 16,
},
albumCard: {
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', overflow: 'hidden',
marginBottom: 16,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05, shadowOpacity: 0.1,
shadowRadius: 3, shadowRadius: 4,
elevation: 2, elevation: 3,
}, },
albumImageContainer: { albumImageContainer: {
aspectRatio: 1, aspectRatio: 1,

View file

@ -36,8 +36,8 @@ export default function HomeScreen() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// 애니메이션 // 애니메이션
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
const slideAnim = useRef(new Animated.Value(20)).current; const slideAnim = useRef(new Animated.Value(0)).current;
const fetchData = async () => { const fetchData = async () => {
try { try {
@ -49,20 +49,6 @@ export default function HomeScreen() {
setMembers(membersData.filter(m => !m.is_former)); setMembers(membersData.filter(m => !m.is_former));
setAlbums(albumsData.slice(0, 2)); setAlbums(albumsData.slice(0, 2));
setSchedules(schedulesData || []); 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) { } catch (error) {
console.error('데이터 로드 오류:', error); console.error('데이터 로드 오류:', error);
} finally { } finally {
@ -77,8 +63,6 @@ export default function HomeScreen() {
const onRefresh = () => { const onRefresh = () => {
setRefreshing(true); setRefreshing(true);
fadeAnim.setValue(0);
slideAnim.setValue(20);
fetchData(); fetchData();
}; };

View file

@ -74,17 +74,21 @@ export default function MembersScreen() {
}) })
).current; ).current;
const startAnimation = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
};
const fetchMembers = async () => { const fetchMembers = async () => {
try { try {
const data = await getMembers(); const data = await getMembers();
setMembers(data.filter(m => !m.is_former)); setMembers(data.filter(m => !m.is_former));
setFormerMembers(data.filter(m => m.is_former)); setFormerMembers(data.filter(m => m.is_former));
Animated.timing(fadeAnim, { startAnimation();
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
} catch (error) { } catch (error) {
console.error('멤버 로드 오류:', error); console.error('멤버 로드 오류:', error);
} finally { } finally {
@ -99,7 +103,6 @@ export default function MembersScreen() {
const onRefresh = () => { const onRefresh = () => {
setRefreshing(true); setRefreshing(true);
fadeAnim.setValue(0);
fetchMembers(); fetchMembers();
}; };

View file

@ -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
<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)
### 커밋 히스토리 ### 커밋 히스토리
| 커밋 | 설명 | | 커밋 | 설명 |
| --------- | ------------------------------------------------------- | | --------- | ----------------------------------------------------------------- |
| `727b05f` | Redis 기반 bi-gram 추천 검색어 시스템 구현 | | `727b05f` | Redis 기반 bi-gram 추천 검색어 시스템 구현 |
| `9c2ff74` | Admin Schedule 추천 검색어 연동 + 빈 상태 드롭다운 숨김 | | `9c2ff74` | Admin Schedule 추천 검색어 연동 + 빈 상태 드롭다운 숨김 |
| `2ad5341` | Mobile Schedule 추천 검색어 API 연동 | | `2ad5341` | Mobile Schedule 추천 검색어 API 연동 |
| `8e3cab9` | 기본 카테고리 보호 및 ID 재정렬 | | `8e3cab9` | 기본 카테고리 보호 및 ID 재정렬 |
| `de2e02f` | 모바일 앨범 상세 UI 개선 | | `de2e02f` | 모바일 앨범 상세 UI 개선 |
| `d6bc8d7` | 모바일 앨범 갤러리 UI 대폭 개선 (Swiper, 뒤로가기 등) | | `d6bc8d7` | 모바일 앨범 갤러리 UI 대폭 개선 (Swiper, 뒤로가기 등) |
| `bf6b7f7` | 앱: 앨범 화면 2열 그리드 레이아웃 개선, 탭 전환 시 앨범 스택 리셋 |
### 주요 변경 사항 ### 주요 변경 사항
@ -688,3 +799,4 @@ node scripts/extract-keywords.js
2. **모바일 앨범 갤러리**: Swiper ViewPager + LightboxIndicator 2. **모바일 앨범 갤러리**: Swiper ViewPager + LightboxIndicator
3. **뒤로가기 처리**: History API로 모달/라이트박스 닫기 3. **뒤로가기 처리**: History API로 모달/라이트박스 닫기
4. **카테고리 보호**: 시스템 기본 카테고리 삭제 방지 4. **카테고리 보호**: 시스템 기본 카테고리 삭제 방지
5. **앱 앨범 스택 리셋**: 탭 전환 시 목록으로 자동 복귀

149
docs/handover.md Normal file
View file

@ -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 동기화**: 기능 추가 시 웹과 앱 모두 구현 필요

View file

@ -36,7 +36,7 @@ function MobileAlbum() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }} transition={{ delay: index * 0.05 }}
onClick={() => navigate(`/album/${album.folder_name}`)} onClick={() => navigate(`/album/${album.folder_name}`)}
className="bg-white rounded-2xl overflow-hidden shadow-sm" className="bg-white rounded-2xl overflow-hidden shadow-md"
> >
<div className="aspect-square bg-gray-200"> <div className="aspect-square bg-gray-200">
{album.cover_thumb_url && ( {album.cover_thumb_url && (