diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 94a693a..8b85faa 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/src/components/cards/QrCard.js b/src/components/cards/QrCard.js index cec4443..148aedd 100644 --- a/src/components/cards/QrCard.js +++ b/src/components/cards/QrCard.js @@ -85,7 +85,7 @@ const QrCard = ({ style={{ width: '100%' }} showsVerticalScrollIndicator={false} > - 임시 출입 QR + 출입 QR { 방문자: {data.visitorType} 시작일: {data.startDate} 만료일: {data.expireDate} - 승인 여부: {data.approval} + 승인 여부: {data.approval.replace(/\n/g, '')} 환자 번호: {data.patientNumber} diff --git a/src/components/modals/PasswordConfirmModal.js b/src/components/modals/PasswordConfirmModal.js index 843338c..81d6f28 100644 --- a/src/components/modals/PasswordConfirmModal.js +++ b/src/components/modals/PasswordConfirmModal.js @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { View, Text, Modal } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { verifyPassword } from '../../apis/PasswordApi'; +import { useAuthStore } from '../../stores/authStore'; import { useModalStore } from '../../stores/modalStore'; import { styles } from './styles/PasswordConfirmModal.styles'; import NormalInput from '../textinputs/NormalInput'; @@ -9,9 +10,11 @@ import NormalButton from '../buttons/NormalButton'; import WaveHeader from '../headers/WaveHeader'; const PasswordConfirmModal = ({ navigationRef }) => { - const { isPasswordModalVisible, pendingTab, prevTab, hidePasswordModal } = useModalStore(); + const { isPasswordModalVisible, pendingTab, prevTab, isFromAppState, hidePasswordModal } = + useModalStore(); const [password, setPassword] = useState(''); const [errorText, setErrorText] = useState(''); // mediumText ErrorText + const setLastAuthTime = useAuthStore((state) => state.setLastAuthTime); useEffect(() => { if (isPasswordModalVisible) { @@ -43,7 +46,8 @@ const PasswordConfirmModal = ({ navigationRef }) => { try { await verifyPassword(password); - + // 인증 성공 시 인증 시각 저장 + setLastAuthTime(Date.now()); navigationRef.current?.navigate(pendingTab); hidePasswordModal(); } catch (error) { @@ -53,7 +57,11 @@ const PasswordConfirmModal = ({ navigationRef }) => { const onClosePasswordModal = () => { hidePasswordModal(); - navigationRef.current?.navigate(prevTab); + if (isFromAppState) { + navigationRef.current?.navigate('MainPage'); // 홈으로 강제 이동 + } else { + navigationRef.current?.navigate(prevTab); // 이전 탭으로 이동 + } }; return ( diff --git a/src/navigations/AppNavigator.js b/src/navigations/AppNavigator.js index 2f6383c..c473f98 100644 --- a/src/navigations/AppNavigator.js +++ b/src/navigations/AppNavigator.js @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { StatusBar } from 'react-native'; +import { StatusBar, AppState } from 'react-native'; import { NavigationContainer, DefaultTheme } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import Ionicons from 'react-native-vector-icons/Ionicons'; @@ -28,6 +28,7 @@ import AccessRequestRolePage from '../pages/AccessRequestRolePage'; const Stack = createStackNavigator(); const Tab = createBottomTabNavigator(); +const PASSWORD_AUTH_VALID_MS = 5 * 60 * 1000; // 비밀번호 재인증 시간 (5분) // StatusBar 스타일 설정 const WHITE_TAB_SCREENS = ['MainPage', 'WelcomePage']; @@ -106,11 +107,13 @@ export default function AppNavigator() { clearAccessToken, _hasHydrated, // hydration flag } = useAuthStore(); + const lastAuthTime = useAuthStore((state) => state.lastAuthTime); + const appState = useRef(AppState.currentState); const showPasswordModal = useModalStore((state) => state.showPasswordModal); // 현재 라우트 이름을 저장하는 state - const [currentRouteName, setCurrentRouteName] = useState('WelcomePage'); + const [currentRouteName, setCurrentRouteName] = useState(isLoggedIn ? 'MainPage' : 'WelcomePage'); // 알림 읽음 여부 관련 const { hasUnread, markAllAsRead } = useNoticeBadge(); @@ -143,10 +146,33 @@ export default function AppNavigator() { // 탭 클릭 시 비밀번호 모달 호출 const handleTabPress = (e, tabName) => { - e.preventDefault(); - showPasswordModal(tabName, currentRouteName || 'MainPage'); + if (!lastAuthTime || Date.now() - lastAuthTime > PASSWORD_AUTH_VALID_MS) { + e.preventDefault(); + showPasswordModal(tabName, currentRouteName || 'MainPage'); + } }; + // 비밀번호 모달 백그라운드 -> 포그라운드 변경시 적용 + useEffect(() => { + const subscription = AppState.addEventListener('change', (nextAppState) => { + // 포그라운드로 돌아올 때 + if (appState.current.match(/inactive|background/) && nextAppState === 'active') { + // 현재 라우트가 마이페이지(혹은 마이페이지 stack 내부)라면 + if (currentRouteName === 'MyPage' || currentRouteName === 'MyPageStack') { + // 인증 만료됐으면 모달 띄우기 + if (!lastAuthTime || Date.now() - lastAuthTime > PASSWORD_AUTH_VALID_MS) { + showPasswordModal('MyPageStack', currentRouteName, true); + } + } + } + appState.current = nextAppState; + }); + + return () => { + subscription.remove(); + }; + }, [currentRouteName, lastAuthTime]); + const navTheme = { ...DefaultTheme, colors: { diff --git a/src/stores/authStore.js b/src/stores/authStore.js index 0b108a6..2054f35 100644 --- a/src/stores/authStore.js +++ b/src/stores/authStore.js @@ -39,6 +39,10 @@ export const useAuthStore = create( // hydration(스토리지 복원) 상태 _hasHydrated: false, setHasHydrated: (state) => set({ _hasHydrated: state }), + + //인증 시각 관리 + lastAuthTime: 0, + setLastAuthTime: (time) => set({ lastAuthTime: time }), }), { name: 'auth-storage', @@ -47,6 +51,7 @@ export const useAuthStore = create( accessToken: state.accessToken, isLoggedIn: state.isLoggedIn, userInfo: state.userInfo, + lastAuthTime: state.lastAuthTime, }), // 복원 완료 시 _hasHydrated를 true로 변경 onRehydrateStorage: () => (state, error) => { diff --git a/src/stores/modalStore.js b/src/stores/modalStore.js index ced7b53..8736957 100644 --- a/src/stores/modalStore.js +++ b/src/stores/modalStore.js @@ -4,15 +4,18 @@ export const useModalStore = create((set) => ({ isPasswordModalVisible: false, pendingTab: null, // 이동 시도한 탭 이름 prevTab: '', // 이전 탭 이름 - showPasswordModal: (tabName, prevTab) => + isFromAppState: false, // 모달 출현 이유 + showPasswordModal: (tabName, prevTab, isFromAppState = false) => set({ isPasswordModalVisible: true, pendingTab: tabName, prevTab, + isFromAppState, }), hidePasswordModal: () => set({ isPasswordModalVisible: false, pendingTab: null, + isFromAppState: false, // 닫을 때 초기화 }), }));