앨범 갤러리: 헤더 추가, 이미지 원본 비율 적용
This commit is contained in:
parent
64e12e47ae
commit
e52951f43c
5 changed files with 224 additions and 96 deletions
11
app/package-lock.json
generated
11
app/package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
|
"expo-media-library": "^18.2.1",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-video-thumbnails": "^10.0.8",
|
"expo-video-thumbnails": "^10.0.8",
|
||||||
|
|
@ -5503,6 +5504,16 @@
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-media-library": {
|
||||||
|
"version": "18.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-18.2.1.tgz",
|
||||||
|
"integrity": "sha512-dV1acx6Aseu+I5hmF61wY8UkD4vdt8d7YXHDfgNp6ZSs06qxayUxgrBsiG2eigLe54VLm3ycbFBbWi31lhfsCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.24",
|
"version": "3.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
|
"expo-media-library": "^18.2.1",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-video-thumbnails": "^10.0.8",
|
"expo-video-thumbnails": "^10.0.8",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ export interface AlbumPhoto {
|
||||||
thumb_url: string;
|
thumb_url: string;
|
||||||
concept_name?: string;
|
concept_name?: string;
|
||||||
members?: { id: number; name: string }[];
|
members?: { id: number; name: string }[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumTeaser {
|
export interface AlbumTeaser {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { ChevronLeft } from 'lucide-react-native';
|
||||||
import { colors } from '../../constants/colors';
|
import { colors } from '../../constants/colors';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
// 페이지 제목 (없으면 "fromis_9" 표시)
|
// 페이지 제목 (없으면 "fromis_9" 표시)
|
||||||
title?: string;
|
title?: string;
|
||||||
|
// 오른쪽 영역에 표시할 요소
|
||||||
|
rightElement?: React.ReactNode;
|
||||||
|
// 뒤로가기 버튼 표시 여부
|
||||||
|
showBack?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모바일 웹 MobileHeader와 동일한 공통 헤더 컴포넌트
|
// 모바일 웹 MobileHeader와 동일한 공통 헤더 컴포넌트
|
||||||
export default function Header({ title }: HeaderProps) {
|
export default function Header({ title, rightElement, showBack }: HeaderProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.header, { paddingTop: insets.top }]}>
|
<View style={[styles.header, { paddingTop: insets.top }]}>
|
||||||
<View style={styles.headerContent}>
|
<View style={styles.headerContent}>
|
||||||
|
{/* 왼쪽 영역 */}
|
||||||
|
<View style={styles.leftSection}>
|
||||||
|
{showBack && (
|
||||||
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
|
||||||
|
<ChevronLeft size={24} color={colors.textPrimary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 중앙 타이틀 */}
|
||||||
<Text style={styles.headerTitle}>
|
<Text style={styles.headerTitle}>
|
||||||
{title || 'fromis_9'}
|
{title || 'fromis_9'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* 오른쪽 영역 */}
|
||||||
|
<View style={styles.rightSection}>
|
||||||
|
{rightElement}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -34,13 +56,26 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
headerContent: {
|
headerContent: {
|
||||||
height: 56,
|
height: 56,
|
||||||
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
leftSection: {
|
||||||
|
width: 40,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 4,
|
||||||
|
marginLeft: -4,
|
||||||
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
|
rightSection: {
|
||||||
|
width: 40,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -7,39 +7,45 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Modal,
|
Modal,
|
||||||
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { ChevronLeft, X, Download } from 'lucide-react-native';
|
||||||
import PagerView from 'react-native-pager-view';
|
import PagerView from 'react-native-pager-view';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import * as MediaLibrary from 'expo-media-library';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
||||||
import { getAlbumByName, Album, AlbumPhoto } from '../api/albums';
|
import { getAlbumByName, Album, AlbumPhoto } from '../api/albums';
|
||||||
import { colors } from '../constants/colors';
|
import { colors } from '../constants/colors';
|
||||||
|
import Header from '../components/common/Header';
|
||||||
import type { AlbumStackParamList } from '../navigation/AppNavigator';
|
import type { AlbumStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<AlbumStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<AlbumStackParamList>;
|
||||||
type RouteType = RouteProp<AlbumStackParamList, 'AlbumGallery'>;
|
type RouteType = RouteProp<AlbumStackParamList, 'AlbumGallery'>;
|
||||||
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
|
||||||
export default function AlbumGalleryScreen() {
|
export default function AlbumGalleryScreen() {
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const route = useRoute<RouteType>();
|
const route = useRoute<RouteType>();
|
||||||
const { name } = route.params;
|
const { name } = route.params;
|
||||||
|
|
||||||
const [photos, setPhotos] = useState<AlbumPhoto[]>([]);
|
const [album, setAlbum] = useState<Album | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// 라이트박스 상태
|
// 라이트박스 상태
|
||||||
const [lightboxVisible, setLightboxVisible] = useState(false);
|
const [lightboxVisible, setLightboxVisible] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const pagerRef = useRef<PagerView>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAlbum = async () => {
|
const fetchAlbum = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getAlbumByName(name);
|
const data = await getAlbumByName(name);
|
||||||
setPhotos(data.photos || []);
|
setAlbum(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('앨범 로드 오류:', error);
|
console.error('앨범 로드 오류:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -49,10 +55,42 @@ export default function AlbumGalleryScreen() {
|
||||||
fetchAlbum();
|
fetchAlbum();
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
const openLightbox = (index: number) => {
|
// 모든 컨셉 포토를 하나의 배열로 (웹과 동일)
|
||||||
|
const allPhotos = useMemo(() => {
|
||||||
|
if (!album) return [];
|
||||||
|
return album.conceptPhotos
|
||||||
|
? Object.values(album.conceptPhotos).flat() as AlbumPhoto[]
|
||||||
|
: (album.photos || []);
|
||||||
|
}, [album]);
|
||||||
|
|
||||||
|
const openLightbox = useCallback((index: number) => {
|
||||||
setSelectedIndex(index);
|
setSelectedIndex(index);
|
||||||
setLightboxVisible(true);
|
setLightboxVisible(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const closeLightbox = useCallback(() => {
|
||||||
|
setLightboxVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 이미지 다운로드
|
||||||
|
const downloadImage = useCallback(async () => {
|
||||||
|
const photo = allPhotos[selectedIndex];
|
||||||
|
if (!photo) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
|
if (status !== 'granted') return;
|
||||||
|
|
||||||
|
const filename = photo.original_url.split('/').pop() || 'photo.jpg';
|
||||||
|
const fileUri = FileSystem.documentDirectory + filename;
|
||||||
|
|
||||||
|
await FileSystem.downloadAsync(photo.original_url, fileUri);
|
||||||
|
await MediaLibrary.saveToLibraryAsync(fileUri);
|
||||||
|
await FileSystem.deleteAsync(fileUri);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('다운로드 오류:', error);
|
||||||
|
}
|
||||||
|
}, [allPhotos, selectedIndex]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,123 +102,164 @@ export default function AlbumGalleryScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2열로 분배
|
// 2열 지그재그로 분배 (웹 동일)
|
||||||
const leftColumn = photos.filter((_, i) => i % 2 === 0);
|
const leftColumn = allPhotos.filter((_, i) => i % 2 === 0).map((photo, idx) => ({ ...photo, originalIndex: idx * 2 }));
|
||||||
const rightColumn = photos.filter((_, i) => i % 2 === 1);
|
const rightColumn = allPhotos.filter((_, i) => i % 2 === 1).map((photo, idx) => ({ ...photo, originalIndex: idx * 2 + 1 }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
{/* 헤더 */}
|
<Header title="앨범" />
|
||||||
<View style={{
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 16 }}>
|
||||||
|
{/* 앨범 헤더 카드 - 웹: mx-4 mt-4 mb-4 p-4 bg-gradient-to-r from-primary/5 to-primary/10 rounded-2xl */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
style={{
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 16,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
gap: 16,
|
||||||
paddingVertical: 12,
|
backgroundColor: 'rgba(34, 197, 94, 0.08)',
|
||||||
borderBottomWidth: 1,
|
}}
|
||||||
borderBottomColor: colors.borderLight,
|
>
|
||||||
}}>
|
{/* 앨범 커버 */}
|
||||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
{album?.cover_thumb_url && (
|
||||||
<Ionicons name="arrow-back" size={24} color={colors.textPrimary} />
|
<Image
|
||||||
</TouchableOpacity>
|
source={{ uri: album.cover_thumb_url }}
|
||||||
<Text style={{
|
style={{
|
||||||
flex: 1,
|
width: 56,
|
||||||
fontSize: 18,
|
height: 56,
|
||||||
fontWeight: '600',
|
borderRadius: 12,
|
||||||
color: colors.textPrimary,
|
// shadow-sm
|
||||||
textAlign: 'center',
|
shadowColor: '#000',
|
||||||
}}>
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 앨범 정보 */}
|
||||||
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{/* 웹: text-xs text-primary font-medium mb-0.5 */}
|
||||||
|
<Text style={{ fontSize: 12, color: colors.primary, fontWeight: '500', marginBottom: 2 }}>
|
||||||
컨셉 포토
|
컨셉 포토
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 13, color: colors.textSecondary }}>
|
{/* 웹: font-bold truncate */}
|
||||||
{photos.length}장
|
<Text style={{ fontWeight: '700', color: colors.textPrimary }} numberOfLines={1}>
|
||||||
|
{album?.title}
|
||||||
|
</Text>
|
||||||
|
{/* 웹: text-xs text-gray-500 */}
|
||||||
|
<Text style={{ fontSize: 12, color: '#6B7280' }}>
|
||||||
|
{allPhotos.length}장의 사진
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 2열 그리드 */}
|
{/* 뒤로가기 화살표 - 웹: rotate-180 */}
|
||||||
<View style={{ flex: 1, flexDirection: 'row', padding: 8 }}>
|
<ChevronLeft size={20} color="#9CA3AF" />
|
||||||
{/* 왼쪽 열 */}
|
</TouchableOpacity>
|
||||||
<View style={{ flex: 1, paddingRight: 4 }}>
|
|
||||||
{leftColumn.map((photo, idx) => (
|
{/* 2열 그리드 - 웹: px-3 flex gap-2 */}
|
||||||
|
<View style={{ paddingHorizontal: 12, flexDirection: 'row', gap: 8 }}>
|
||||||
|
{/* 왼쪽 열 - 웹: flex-1 flex flex-col gap-2 */}
|
||||||
|
<View style={{ flex: 1, gap: 8 }}>
|
||||||
|
{leftColumn.map((photo) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={photo.id}
|
key={photo.id}
|
||||||
onPress={() => openLightbox(idx * 2)}
|
onPress={() => openLightbox(photo.originalIndex)}
|
||||||
style={{ marginBottom: 8 }}
|
activeOpacity={0.8}
|
||||||
|
style={{
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: photo.medium_url || photo.thumb_url }}
|
source={{ uri: photo.thumb_url || photo.medium_url }}
|
||||||
style={{
|
style={{ width: '100%', aspectRatio: photo.width && photo.height ? photo.width / photo.height : 0.75 }}
|
||||||
width: '100%',
|
|
||||||
aspectRatio: 0.75,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 오른쪽 열 */}
|
{/* 오른쪽 열 */}
|
||||||
<View style={{ flex: 1, paddingLeft: 4 }}>
|
<View style={{ flex: 1, gap: 8 }}>
|
||||||
{rightColumn.map((photo, idx) => (
|
{rightColumn.map((photo) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={photo.id}
|
key={photo.id}
|
||||||
onPress={() => openLightbox(idx * 2 + 1)}
|
onPress={() => openLightbox(photo.originalIndex)}
|
||||||
style={{ marginBottom: 8 }}
|
activeOpacity={0.8}
|
||||||
|
style={{
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: photo.medium_url || photo.thumb_url }}
|
source={{ uri: photo.thumb_url || photo.medium_url }}
|
||||||
style={{
|
style={{ width: '100%', aspectRatio: photo.width && photo.height ? photo.width / photo.height : 0.75 }}
|
||||||
width: '100%',
|
|
||||||
aspectRatio: 0.75,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
{/* 라이트박스 모달 */}
|
{/* 풀스크린 라이트박스 - 웹: fixed inset-0 bg-black z-[60] */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={lightboxVisible}
|
visible={lightboxVisible}
|
||||||
transparent
|
transparent
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
onRequestClose={() => setLightboxVisible(false)}
|
onRequestClose={closeLightbox}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||||
{/* 헤더 */}
|
{/* 상단 헤더 - 3등분 - 웹: absolute top-0 left-0 right-0 flex items-center px-4 py-3 z-20 */}
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
<View style={{
|
<View style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
}}>
|
}}>
|
||||||
<TouchableOpacity onPress={() => setLightboxVisible(false)}>
|
{/* 왼쪽: X 버튼 */}
|
||||||
<Ionicons name="close" size={28} color="white" />
|
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||||
|
<TouchableOpacity onPress={closeLightbox} style={{ padding: 4 }}>
|
||||||
|
<X size={24} color="rgba(255,255,255,0.8)" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={{ color: 'white', fontSize: 14 }}>
|
</View>
|
||||||
{selectedIndex + 1} / {photos.length}
|
|
||||||
|
{/* 중앙: 페이지 표시 - 웹: text-white/70 text-sm tabular-nums */}
|
||||||
|
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, fontVariant: ['tabular-nums'] }}>
|
||||||
|
{selectedIndex + 1} / {allPhotos.length}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 28 }} />
|
|
||||||
|
{/* 오른쪽: 다운로드 버튼 */}
|
||||||
|
<View style={{ flex: 1, alignItems: 'flex-end' }}>
|
||||||
|
<TouchableOpacity onPress={downloadImage} style={{ padding: 4 }}>
|
||||||
|
<Download size={22} color="rgba(255,255,255,0.8)" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
||||||
{/* PagerView */}
|
{/* PagerView - 웹: Swiper */}
|
||||||
<PagerView
|
<PagerView
|
||||||
|
ref={pagerRef}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
initialPage={selectedIndex}
|
initialPage={selectedIndex}
|
||||||
onPageSelected={(e) => setSelectedIndex(e.nativeEvent.position)}
|
onPageSelected={(e) => setSelectedIndex(e.nativeEvent.position)}
|
||||||
>
|
>
|
||||||
{photos.map((photo, index) => (
|
{allPhotos.map((photo) => (
|
||||||
<View key={photo.id} style={{ flex: 1, justifyContent: 'center' }}>
|
<View key={photo.id} style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: photo.original_url || photo.medium_url }}
|
source={{ uri: photo.medium_url || photo.original_url }}
|
||||||
style={{
|
style={{
|
||||||
width: SCREEN_WIDTH,
|
width: SCREEN_WIDTH,
|
||||||
height: SCREEN_WIDTH * 1.33,
|
height: SCREEN_HEIGHT * 0.8,
|
||||||
}}
|
}}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|
@ -189,6 +268,6 @@ export default function AlbumGalleryScreen() {
|
||||||
</PagerView>
|
</PagerView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue