From 0b512ea9147f998ef339438c460d5e52686202bf Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Wed, 27 May 2026 17:24:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A7=81=ED=81=AC=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=2020=EC=B4=88=20timeout=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/(home)/scanning.tsx | 134 ++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/app/(tabs)/(home)/scanning.tsx b/app/(tabs)/(home)/scanning.tsx index a258e41..6840b1a 100644 --- a/app/(tabs)/(home)/scanning.tsx +++ b/app/(tabs)/(home)/scanning.tsx @@ -1,5 +1,6 @@ import { useAuth } from '@clerk/expo'; import LottieView from 'lottie-react-native'; +import { useIsFocused } from '@react-navigation/native'; import { router, Stack, useLocalSearchParams } from 'expo-router'; import { useEffect, useRef, useState } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -9,15 +10,17 @@ import { ApiError } from '@/api/api-client'; import { Colors, Typography } from '@/constants/theme'; const POLLING_INTERVAL_MS = 2_000; -const MAX_POLLING_MS = 30_000; +const SCAN_SCREEN_TIMEOUT_MS = 20_000; export default function ScanningScreen() { const { getToken, isLoaded, isSignedIn } = useAuth(); + const isFocused = useIsFocused(); const { url: urlParam } = useLocalSearchParams<{ url?: string | string[] }>(); const url = getUrlParam(urlParam); const [errorMessage, setErrorMessage] = useState(''); const [retryKey, setRetryKey] = useState(0); const getTokenRef = useRef(getToken); + const currentAbortControllerRef = useRef(null); useEffect(() => { getTokenRef.current = getToken; @@ -25,44 +28,77 @@ export default function ScanningScreen() { useEffect(() => { const abortController = new AbortController(); - let timeoutId: ReturnType | null = null; - let timedOut = false; + let screenTimeoutId: ReturnType | null = null; + let isActive = true; + let didTimeout = false; + let hasNavigated = false; + + const clearScreenTimeout = () => { + if (screenTimeoutId) { + clearTimeout(screenTimeoutId); + screenTimeoutId = null; + } + }; + + const handleScreenTimeout = () => { + if (!isActive || didTimeout || hasNavigated) { + return; + } + + didTimeout = true; + abortController.abort(); + setErrorMessage(getAnalysisErrorMessage(new Error('TIMEOUT'))); + }; + + currentAbortControllerRef.current = abortController; if (!isLoaded) { - return () => abortController.abort(); + return () => { + isActive = false; + if (currentAbortControllerRef.current === abortController) { + currentAbortControllerRef.current = null; + } + abortController.abort(); + }; } if (!isSignedIn) { setErrorMessage('로그인 상태를 확인할 수 없습니다. 다시 로그인한 뒤 시도해주세요.'); - return () => abortController.abort(); + return () => { + isActive = false; + if (currentAbortControllerRef.current === abortController) { + currentAbortControllerRef.current = null; + } + abortController.abort(); + }; } if (!url) { setErrorMessage('검사할 URL을 찾을 수 없습니다. 링크를 다시 입력해주세요.'); - return () => abortController.abort(); + return () => { + isActive = false; + if (currentAbortControllerRef.current === abortController) { + currentAbortControllerRef.current = null; + } + abortController.abort(); + }; } setErrorMessage(''); - - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - timedOut = true; - abortController.abort(); - reject(new Error('TIMEOUT')); - }, MAX_POLLING_MS); - }); - - Promise.race([ - runAnalysisPolling({ - getToken: () => getTokenRef.current(), - url, - signal: abortController.signal, - }), - timeoutPromise, - ]) + screenTimeoutId = setTimeout(handleScreenTimeout, SCAN_SCREEN_TIMEOUT_MS); + + runAnalysisPolling({ + getToken: () => getTokenRef.current(), + url, + signal: abortController.signal, + canNavigate: () => isActive && !didTimeout && !hasNavigated, + onNavigate: () => { + hasNavigated = true; + clearScreenTimeout(); + }, + }) .catch((error) => { - if (timedOut) { - setErrorMessage(getAnalysisErrorMessage(new Error('TIMEOUT'))); + if (!isActive || hasNavigated || didTimeout) { return; } @@ -73,21 +109,34 @@ export default function ScanningScreen() { setErrorMessage(getAnalysisErrorMessage(error)); }) .finally(() => { - if (timeoutId) { - clearTimeout(timeoutId); - } + clearScreenTimeout(); }); return () => { - if (timeoutId) { - clearTimeout(timeoutId); + isActive = false; + clearScreenTimeout(); + if (currentAbortControllerRef.current === abortController) { + currentAbortControllerRef.current = null; } - abortController.abort(); }; }, [isLoaded, isSignedIn, retryKey, url]); const hasError = errorMessage.length > 0; + const isScanningAnimationVisible = isFocused && !hasError; + + useEffect(() => { + if (!isScanningAnimationVisible) { + return undefined; + } + + const animationTimeoutId = setTimeout(() => { + currentAbortControllerRef.current?.abort(); + setErrorMessage(getAnalysisErrorMessage(new Error('TIMEOUT'))); + }, SCAN_SCREEN_TIMEOUT_MS); + + return () => clearTimeout(animationTimeoutId); + }, [isScanningAnimationVisible, retryKey, url]); return ( <> @@ -107,8 +156,8 @@ export default function ScanningScreen() { @@ -161,16 +210,20 @@ async function runAnalysisPolling({ getToken, url, signal, + canNavigate, + onNavigate, }: { getToken: () => Promise; url: string; signal: AbortSignal; + canNavigate: () => boolean; + onNavigate: () => void; }) { - const deadline = Date.now() + MAX_POLLING_MS; + const deadline = Date.now() + SCAN_SCREEN_TIMEOUT_MS; let analysis = await requestAnalysis(getToken, url, { signal }); while (!signal.aborted) { - const handled = handleAnalysisResult(analysis, url, signal); + const handled = handleAnalysisResult(analysis, url, signal, canNavigate, onNavigate); if (handled) { return; @@ -187,7 +240,13 @@ async function runAnalysisPolling({ } } -function handleAnalysisResult(analysis: AnalysisResponse, fallbackUrl: string, signal: AbortSignal) { +function handleAnalysisResult( + analysis: AnalysisResponse, + fallbackUrl: string, + signal: AbortSignal, + canNavigate: () => boolean, + onNavigate: () => void, +) { if (analysis.status === 'queued') { return false; } @@ -200,7 +259,8 @@ function handleAnalysisResult(analysis: AnalysisResponse, fallbackUrl: string, s throw new Error('MISSING_VERDICT'); } - if (!signal.aborted) { + if (!signal.aborted && canNavigate()) { + onNavigate(); router.replace({ pathname: getResultPath(analysis.verdict), params: { From b69a172f27a914e213f3b94dcf62aa36d8a3bfb3 Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Fri, 29 May 2026 23:05:10 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EA=B2=80=EC=82=AC=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=B4=EB=8F=99=20=ED=9B=84=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/(home)/scanning.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/(tabs)/(home)/scanning.tsx b/app/(tabs)/(home)/scanning.tsx index 5ffa756..abeafe0 100644 --- a/app/(tabs)/(home)/scanning.tsx +++ b/app/(tabs)/(home)/scanning.tsx @@ -31,6 +31,7 @@ export default function ScanningScreen() { const [retryKey, setRetryKey] = useState(0); const getTokenRef = useRef(getToken); const currentAbortControllerRef = useRef(null); + const hasNavigatedRef = useRef(false); const isShortScreen = windowHeight <= SHORT_SCREEN_HEIGHT; const isVeryShortScreen = windowHeight <= VERY_SHORT_SCREEN_HEIGHT; const animationSize = isVeryShortScreen @@ -68,6 +69,7 @@ export default function ScanningScreen() { }; currentAbortControllerRef.current = abortController; + hasNavigatedRef.current = false; if (!isLoaded) { return () => { @@ -111,6 +113,7 @@ export default function ScanningScreen() { canNavigate: () => isActive && !didTimeout && !hasNavigated, onNavigate: () => { hasNavigated = true; + hasNavigatedRef.current = true; clearScreenTimeout(); }, }) @@ -151,6 +154,10 @@ export default function ScanningScreen() { } const animationTimeoutId = setTimeout(() => { + if (hasNavigatedRef.current) { + return; + } + currentAbortControllerRef.current?.abort(); setErrorMessage(getAnalysisErrorMessage(new Error('TIMEOUT'))); }, SCAN_SCREEN_TIMEOUT_MS);