diff --git a/app.config.js b/app.config.js index 37b8242..374b754 100644 --- a/app.config.js +++ b/app.config.js @@ -25,7 +25,7 @@ export default { favicon: './assets/images/logoIcon.png', }, extra: { - BASE_URL: 'http://192.168.0.115:8081', // 본인 pc IPv4 주소로 수정하세용 + BASE_URL: 'http://192.168.0.181:8081', // 본인 pc IPv4 주소로 수정하세용 }, }, }; diff --git a/components/cards/Dot.js b/components/cards/Dot.js new file mode 100644 index 0000000..ede8791 --- /dev/null +++ b/components/cards/Dot.js @@ -0,0 +1,36 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet } from 'react-native'; +import { styles } from './styles/Dot.styles'; +import { colors } from '../../constants/colors'; + +// dot의 기본 너비와 활성화(선택) 상태일 때의 너비 +const DOT_WIDTH = 10; +const DOT_ACTIVE_WIDTH = 16; + +const Dot = ({ active }) => { + // dot width를 위한 Animated.Value 배열 + const animatedWidth = useRef(new Animated.Value(active ? DOT_ACTIVE_WIDTH : DOT_WIDTH)).current; + + // pageIndex가 바뀔 때 dot width 애니메이션 + useEffect(() => { + Animated.timing(animatedWidth, { + toValue: active ? DOT_ACTIVE_WIDTH : DOT_WIDTH, + duration: 300, + useNativeDriver: false, + }).start(); + }, [active, animatedWidth]); + + return ( + + ); +}; + +export default Dot; diff --git a/components/cards/QrCard.js b/components/cards/QrCard.js new file mode 100644 index 0000000..3ee8a62 --- /dev/null +++ b/components/cards/QrCard.js @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { View, Text, Image } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import QRCode from 'react-native-qrcode-svg'; + +import NormalButton from '../../components/buttons/NormalButton'; +import { styles } from './styles/QrCard.styles'; +import { colors } from '../../constants/colors'; +import { hospitalName } from '../../mocks/hospitalData'; + +// hasAccessAuthority: 출입 권한 여부, userVC : VC에 담을 사용자 정보, qrData : QR에 담을 JSON 문자열 +const QrCard = ({ hasAccessAuthority, did, userName, hospitalName }) => { + // 해당 QR의 상세 페이지로 이동 (아직 미구현) + //const navigation = useNavigation(); + // const navigateToAccessListDeatail = () => { + // navigation.navigate('AccessListDetailPage'); + // }; + + // 임시: QR에 담을 JSON 문자열 + const qrData = JSON.stringify({ did, userName, hospitalName }); + return ( + + + + {hasAccessAuthority ? ( + <> + 임시 출입 QR + + {userName} + {hospitalName} + + ) : ( + <> + 등록된 출입 권한이 존재하지 않습니다. + 방문 신청 버튼을 눌러 출입 권한을 신청해주세요. + + )} + + + ); +}; + +export default QrCard; diff --git a/components/cards/QrCards.js b/components/cards/QrCards.js new file mode 100644 index 0000000..5dfc9b1 --- /dev/null +++ b/components/cards/QrCards.js @@ -0,0 +1,83 @@ +import React, { useRef, useState } from 'react'; +import { FlatList, View, Dimensions, TouchableOpacity } from 'react-native'; +import QrCard from './QrCard'; +import styles from './styles/QrCards.styles'; +import Dot from './Dot'; + +// 화면의 가로 길이 가져오기 +const { width } = Dimensions.get('window'); +const CARD_WIDTH = width; +const SIDE_PADDING = (width - CARD_WIDTH) / 2; // 좌우 패딩 계산 +const OVERLAP = 80; // 카드가 겹치는 정도 + +const QrCards = ({ userVC, hasAccessAuthority }) => { + const [pageIndex, setPageIndex] = useState(0); + const flatListRef = useRef(null); + + // 권한 없거나 카드 데이터 없으면 안내 메시지 카드만 + if (!hasAccessAuthority || !userVC || userVC.length === 0) { + return ( + + + + ); + } + + // 스크롤이 끝났을 때 현재 페이지 인덱스 계산 + const onMomentumScrollEnd = (event) => { + const offsetX = event.nativeEvent.contentOffset.x; + const newIndex = Math.round(offsetX / (CARD_WIDTH - OVERLAP)); + setPageIndex(newIndex); + }; + + // dot(인디케이터) 클릭 시 해당 카드로 이동 + const handleDotPress = (index) => { + flatListRef.current?.scrollToIndex({ index, animated: true }); + }; + + // 카드 리스트 + return ( + + item.did} + horizontal //가로 스크롤 + showsHorizontalScrollIndicator={false} //하단 기본 스크롤바 숨김 + pagingEnabled={false} // snapToInterval을 사용하므로 false + snapToInterval={CARD_WIDTH - OVERLAP} // 카드 단위로 스냅 + decelerationRate="fast" // 빠른 스냅 효과 + contentContainerStyle={{ + paddingHorizontal: SIDE_PADDING, // 양쪽에 패딩 추가로 옆 카드 살짝 보이게 + }} + renderItem={({ item, index }) => ( + + + + )} + onMomentumScrollEnd={onMomentumScrollEnd} + /> + + {userVC.map((_, i) => ( + handleDotPress(i)}> + + + ))} + + + ); +}; + +export default QrCards; diff --git a/components/cards/styles/Dot.styles.js b/components/cards/styles/Dot.styles.js new file mode 100644 index 0000000..5d09691 --- /dev/null +++ b/components/cards/styles/Dot.styles.js @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; +import { colors } from '../../../constants/colors'; + +export const styles = StyleSheet.create({ + dot: { + height: 10, + borderRadius: 16, + marginHorizontal: 4, + }, +}); diff --git a/components/cards/styles/QrCard.styles.js b/components/cards/styles/QrCard.styles.js new file mode 100644 index 0000000..7f13ae1 --- /dev/null +++ b/components/cards/styles/QrCard.styles.js @@ -0,0 +1,65 @@ +import { StyleSheet } from 'react-native'; +import { colors } from '../../../constants/colors'; + +export const styles = StyleSheet.create({ + shadowWrapper: { + // 그림자 설정 + shadowColor: 'black', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 5, + elevation: 7, // Android용 + borderRadius: 15, + width: '70%', + height: '80%', + marginVertical: '10%', + }, + cardContainer: { + flex: 1, // 부모(shadowWrapper)의 크기를 꽉 채워 카드와 그림자 모양을 일치시킴 + backgroundColor: colors.white, + borderRadius: 15, + padding: '10%', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', // 배경 이미지가 카드 안에 잘리게 + position: 'relative', // 자식의 absolute 포지션 기준점 + }, + backgroundImage: { + ...StyleSheet.absoluteFillObject, // 전체 카드 덮게 만듦 + width: '125%', + zIndex: 0, // 맨 뒤로 + opacity: 0.5, + }, + cardText: { + fontSize: 20, + fontWeight: 600, + color: colors.black, + textAlign: 'center', + marginBottom: '5%', + }, + cardSubText: { + fontSize: 16, + fontWeight: 500, + color: colors.darkGray, + textAlign: 'center', + }, + qrTitle: { + fontSize: 26, + fontWeight: 700, + color: colors.black, + marginBottom: '10%', + zIndex: 1, // QR과 텍스트를 배경 위에 + }, + userName: { + fontSize: 24, + fontWeight: 600, + color: colors.black, + marginVertical: '8%', + }, + hospital: { + fontSize: 16, + fontWeight: 500, + color: colors.darkGray, + marginTop: '2%', + }, +}); diff --git a/components/cards/styles/QrCards.styles.js b/components/cards/styles/QrCards.styles.js new file mode 100644 index 0000000..0502f92 --- /dev/null +++ b/components/cards/styles/QrCards.styles.js @@ -0,0 +1,11 @@ +import { StyleSheet } from 'react-native'; + +const styles = StyleSheet.create({ + dotContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginBottom: '5%', + }, +}); + +export default styles; diff --git a/pages/MainPage.js b/pages/MainPage.js index bcc2fbb..967dbe3 100644 --- a/pages/MainPage.js +++ b/pages/MainPage.js @@ -5,22 +5,35 @@ import NormalButton from '../components/buttons/NormalButton'; import { styles } from './styles/MainPage.styles'; import QRCode from 'react-native-qrcode-svg'; import { colors } from '../constants/colors'; +import QrCards from '../components/cards/QrCards'; const MainPage = () => { // 임시: 상태변수로 출입 권한 제어 const [hasAccessAuthority, setHasAccessAuthority] = useState(true); // 임시: VC에 담을 사용자 정보 - const userVC = { - did: 'did:example:123456789abcdefghi', - userName: '김짱구', - hospital1: '짱구병원', - hospital2: '흰둥이병원', - hospital3: '오수병원', - issuedAt: Date.now(), - }; + const userVC = [ + { + did: 'did:example:123456789abcdefghi', + userName: '김짱구', + hospitalName: '짱구병원', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefdhi', + userName: '김짱구', + hospitalName: '흰둥이병원', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdeffhi', + userName: '김짱구', + hospitalName: '오수병원', + issuedAt: Date.now(), + }, + ]; // 임시: QR에 담을 JSON 문자열 - const qrData = JSON.stringify(userVC); + //const qrData = JSON.stringify(userVC); const navigation = useNavigation(); @@ -39,37 +52,7 @@ const MainPage = () => { source={require('../assets/images/logoGreen.png')} resizeMode="contain" // 이미지 비율 유지 /> - - - - {hasAccessAuthority ? ( - <> - 임시 출입 QR - - {userVC.userName} - {userVC.hospital1} - {userVC.hospital2} - {userVC.hospital3} - - ) : ( - <> - 등록된 출입 권한이 존재하지 않습니다. - - 방문 신청 버튼을 눌러 출입 권한을 신청해주세요. - - - )} - - + diff --git a/pages/styles/MainPage.styles.js b/pages/styles/MainPage.styles.js index 9321614..091dddd 100644 --- a/pages/styles/MainPage.styles.js +++ b/pages/styles/MainPage.styles.js @@ -4,7 +4,6 @@ import { colors } from '../../constants/colors'; export const styles = StyleSheet.create({ container: { flex: 1, - paddingHorizontal: '10%', paddingTop: '5%', }, logoImage: { @@ -13,70 +12,10 @@ export const styles = StyleSheet.create({ marginTop: '15%', alignSelf: 'center', }, - shadowWrapper: { - // 그림자 설정 - shadowColor: 'black', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 5, - elevation: 7, // Android용 - borderRadius: 15, - width: '100%', - height: '55%', - marginVertical: '10%', - }, - cardContainer: { - flex: 1, // 부모(shadowWrapper)의 크기를 꽉 채워 카드와 그림자 모양을 일치시킴 - backgroundColor: colors.white, - borderRadius: 15, - padding: '10%', - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', // 배경 이미지가 카드 안에 잘리게 - position: 'relative', // 자식의 absolute 포지션 기준점 - }, - backgroundImage: { - ...StyleSheet.absoluteFillObject, // 전체 카드 덮게 만듦 - width: '125%', - zIndex: 0, // 맨 뒤로 - opacity: 0.5, - }, - cardText: { - fontSize: 20, - fontWeight: 600, - color: colors.black, - textAlign: 'center', - marginBottom: '5%', - }, - cardSubText: { - fontSize: 16, - fontWeight: 500, - color: colors.darkGray, - textAlign: 'center', - }, - qrTitle: { - fontSize: 26, - fontWeight: 700, - color: colors.black, - marginBottom: '10%', - zIndex: 1, // QR과 텍스트를 배경 위에 - }, - userName: { - fontSize: 24, - fontWeight: 600, - color: colors.black, - marginVertical: '8%', - }, - hospital: { - fontSize: 16, - fontWeight: 500, - color: colors.darkGray, - marginTop: '2%', - }, buttonContainer: { flexDirection: 'row', justifyContent: 'center', marginTop: '3%', - gap: '10%', + gap: '5%', }, });