From f1061d2ee2d48257c2a7b7fc32e3dfecd276df69 Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Mon, 1 Jun 2026 14:08:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80=EC=9A=A9=20=EB=B6=84=EC=84=9D=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/analyses.ts | 1 + app/(tabs)/(home)/scan-result-block.tsx | 26 +++++++++++++++++- app/(tabs)/(home)/scan-result-caution.tsx | 26 +++++++++++++++++- app/(tabs)/(home)/scan-result.tsx | 26 +++++++++++++++++- app/(tabs)/(home)/scanning.tsx | 32 ++++++++++++++++++++++- components/ui/scan-result-reason.tsx | 5 ++-- utils/analysis-result-display.ts | 21 +++++++++++++++ 7 files changed, 131 insertions(+), 6 deletions(-) diff --git a/api/analyses.ts b/api/analyses.ts index 3f5bf9b..fed3f84 100644 --- a/api/analyses.ts +++ b/api/analyses.ts @@ -29,6 +29,7 @@ export type AnalysisResponse = { errorCode?: string; errorStage?: number; errorMessage?: string; + contentAnalysisError?: string; }; export type VerdictStatisticsResponse = Record; diff --git a/app/(tabs)/(home)/scan-result-block.tsx b/app/(tabs)/(home)/scan-result-block.tsx index b777a92..ed40092 100644 --- a/app/(tabs)/(home)/scan-result-block.tsx +++ b/app/(tabs)/(home)/scan-result-block.tsx @@ -11,6 +11,7 @@ import { getAnalysisDisplayUrl, getAnalysisReasonText, getAnalysisResultPath, + getContentAnalysisErrorText, getRouteParam, } from '@/utils/analysis-result-display'; import { useGuardedPress } from '@/utils/press-guard'; @@ -30,6 +31,7 @@ export default function ScanResultBlockScreen() { const { analysis, isLoading, errorMessage } = useAnalysisResult(analysisId); const displayUrl = getAnalysisDisplayUrl(analysis, url); const reason = getAnalysisReasonText(analysis, getMockScanResultReason('danger')); + const contentAnalysisErrorText = getContentAnalysisErrorText(analysis); const shouldRedirectToVerdict = Boolean(analysis?.verdict && analysis.verdict !== 'danger'); const isVerifyingAnalysis = Boolean(analysisId) && !errorMessage && (!analysis?.verdict || isLoading); const isCompactResult = windowHeight <= COMPACT_RESULT_HEIGHT; @@ -107,7 +109,20 @@ export default function ScanResultBlockScreen() { {isLoading && 분석 결과를 불러오는 중입니다.} {errorMessage ? {errorMessage} : null} - + + + {/* 검사 대상 카드 */} @@ -169,9 +184,18 @@ const styles = StyleSheet.create({ reasonCard: { marginBottom: 24, }, + reasonCardWithNotice: { + marginBottom: 12, + }, reasonCardCompact: { marginBottom: 16, }, + noticeCard: { + marginBottom: 24, + }, + noticeCardCompact: { + marginBottom: 16, + }, statusText: { ...Typography.caption, color: Colors.brand.textSecondary, diff --git a/app/(tabs)/(home)/scan-result-caution.tsx b/app/(tabs)/(home)/scan-result-caution.tsx index cf42365..4dff279 100644 --- a/app/(tabs)/(home)/scan-result-caution.tsx +++ b/app/(tabs)/(home)/scan-result-caution.tsx @@ -14,6 +14,7 @@ import { getAnalysisFinalUrl, getAnalysisReasonText, getAnalysisResultPath, + getContentAnalysisErrorText, getRouteParam, } from '@/utils/analysis-result-display'; import { showAlert } from '@/utils/guarded-alert'; @@ -36,6 +37,7 @@ export default function ScanResultCautionScreen() { const displayUrl = getAnalysisDisplayUrl(analysis, url); const finalUrl = getAnalysisFinalUrl(analysis, displayUrl); const reason = getAnalysisReasonText(analysis, getMockScanResultReason('caution')); + const contentAnalysisErrorText = getContentAnalysisErrorText(analysis); const [saveModalVisible, setSaveModalVisible] = useState(false); const [isSaving, setIsSaving] = useState(false); const isSavingRef = useRef(false); @@ -167,7 +169,20 @@ export default function ScanResultCautionScreen() { {isLoading && 분석 결과를 불러오는 중입니다.} {errorMessage ? {errorMessage} : null} - + + + {/* 검사 대상 카드 */} @@ -254,9 +269,18 @@ const styles = StyleSheet.create({ reasonCard: { marginBottom: 24, }, + reasonCardWithNotice: { + marginBottom: 12, + }, reasonCardCompact: { marginBottom: 16, }, + noticeCard: { + marginBottom: 24, + }, + noticeCardCompact: { + marginBottom: 16, + }, statusText: { ...Typography.caption, color: Colors.brand.textSecondary, diff --git a/app/(tabs)/(home)/scan-result.tsx b/app/(tabs)/(home)/scan-result.tsx index fd56e4c..c37d8cd 100644 --- a/app/(tabs)/(home)/scan-result.tsx +++ b/app/(tabs)/(home)/scan-result.tsx @@ -14,6 +14,7 @@ import { getAnalysisFinalUrl, getAnalysisReasonText, getAnalysisResultPath, + getContentAnalysisErrorText, getRouteParam, } from '@/utils/analysis-result-display'; import { showAlert } from '@/utils/guarded-alert'; @@ -36,6 +37,7 @@ export default function ScanResultScreen() { const displayUrl = getAnalysisDisplayUrl(analysis, url); const finalUrl = getAnalysisFinalUrl(analysis, displayUrl); const reason = getAnalysisReasonText(analysis, getMockScanResultReason('safe')); + const contentAnalysisErrorText = getContentAnalysisErrorText(analysis); const [saveModalVisible, setSaveModalVisible] = useState(false); const [isSaving, setIsSaving] = useState(false); const isSavingRef = useRef(false); @@ -167,7 +169,20 @@ export default function ScanResultScreen() { {isLoading && 분석 결과를 불러오는 중입니다.} {errorMessage ? {errorMessage} : null} - + + + {/* 검사 대상 카드 */} @@ -256,9 +271,18 @@ const styles = StyleSheet.create({ reasonCard: { marginBottom: 24, }, + reasonCardWithNotice: { + marginBottom: 12, + }, reasonCardCompact: { marginBottom: 16, }, + noticeCard: { + marginBottom: 24, + }, + noticeCardCompact: { + marginBottom: 16, + }, statusText: { ...Typography.caption, color: Colors.brand.textSecondary, diff --git a/app/(tabs)/(home)/scanning.tsx b/app/(tabs)/(home)/scanning.tsx index abeafe0..b41864c 100644 --- a/app/(tabs)/(home)/scanning.tsx +++ b/app/(tabs)/(home)/scanning.tsx @@ -19,6 +19,10 @@ const VERY_SHORT_SCREEN_HEIGHT = 700; const DEFAULT_ANIMATION_SIZE = 280; const SHORT_ANIMATION_SIZE = 216; const VERY_SHORT_ANIMATION_SIZE = 188; +const PAGE_UNAVAILABLE_ERROR_CODE = 'PAGE_UNAVAILABLE'; +const PAGE_UNAVAILABLE_DEFAULT_MESSAGE = '페이지에 연결할 수 없습니다.'; +const PAGE_UNAVAILABLE_HELP_MESSAGE = + '사이트 접속이 제한되었거나 일시적으로 응답하지 않을 수 있습니다.'; export default function ScanningScreen() { const { getToken, isLoaded, isSignedIn } = useAuth(); @@ -316,7 +320,7 @@ function handleAnalysisResult( } if (analysis.status === 'failed') { - throw new Error(analysis.errorMessage || 'ANALYSIS_FAILED'); + throw new Error(getFailedAnalysisErrorMessage(analysis)); } if (!analysis.verdict) { @@ -410,6 +414,32 @@ function getAnalysisErrorMessage(error: unknown) { return '링크 검사에 실패했습니다. 잠시 후 다시 시도해주세요.'; } +function getFailedAnalysisErrorMessage(analysis: AnalysisResponse) { + if (analysis.errorCode === PAGE_UNAVAILABLE_ERROR_CODE) { + return getPageUnavailableErrorMessage(analysis.errorMessage); + } + + return analysis.errorMessage?.trim() || 'ANALYSIS_FAILED'; +} + +function getPageUnavailableErrorMessage(errorMessage?: string) { + const normalizedMessage = errorMessage?.trim(); + + if (!normalizedMessage || isTechnicalErrorCode(normalizedMessage)) { + return `${PAGE_UNAVAILABLE_DEFAULT_MESSAGE}\n${PAGE_UNAVAILABLE_HELP_MESSAGE}`; + } + + if (normalizedMessage.includes(PAGE_UNAVAILABLE_HELP_MESSAGE)) { + return normalizedMessage; + } + + return `${normalizedMessage}\n${PAGE_UNAVAILABLE_HELP_MESSAGE}`; +} + +function isTechnicalErrorCode(value: string) { + return /^[A-Z0-9_]+$/.test(value) || /^[a-z0-9_]+$/.test(value); +} + function getUrlParam(value: string | string[] | undefined) { return typeof value === 'string' ? value : ''; } diff --git a/components/ui/scan-result-reason.tsx b/components/ui/scan-result-reason.tsx index 704d773..29febe3 100644 --- a/components/ui/scan-result-reason.tsx +++ b/components/ui/scan-result-reason.tsx @@ -3,17 +3,18 @@ import { Colors, Typography } from '@/constants/theme'; interface ScanResultReasonProps { reason: string; + label?: string; style?: StyleProp; } -export function ScanResultReason({ reason, style }: ScanResultReasonProps) { +export function ScanResultReason({ reason, label = '판정 이유', style }: ScanResultReasonProps) { const normalizedReason = reason.trim(); if (!normalizedReason) return null; return ( - 판정 이유 + {label} {normalizedReason} diff --git a/utils/analysis-result-display.ts b/utils/analysis-result-display.ts index 4fa0dd6..88e83a0 100644 --- a/utils/analysis-result-display.ts +++ b/utils/analysis-result-display.ts @@ -1,5 +1,8 @@ import type { AnalysisResponse, AnalysisVerdict } from '@/api/analyses'; +const DEFAULT_CONTENT_ANALYSIS_ERROR_MESSAGE = + '페이지 내용을 가져오지 못했어요. 사이트 접속이 제한되었거나 일시적으로 응답하지 않을 수 있습니다.'; + export function getRouteParam(value: string | string[] | undefined) { return typeof value === 'string' ? value : undefined; } @@ -34,6 +37,20 @@ export function getAnalysisReasonText( return fallbackReason; } +export function getContentAnalysisErrorText(analysis: AnalysisResponse | null) { + const contentAnalysisError = analysis?.contentAnalysisError?.trim(); + + if (!contentAnalysisError) { + return ''; + } + + if (isTechnicalErrorCode(contentAnalysisError)) { + return DEFAULT_CONTENT_ANALYSIS_ERROR_MESSAGE; + } + + return `페이지 내용을 가져오지 못했어요. ${contentAnalysisError}`; +} + export function getAnalysisResultPath(verdict: AnalysisVerdict) { switch (verdict) { case 'safe': @@ -52,3 +69,7 @@ export function getSiteName(url: string) { return '정보 없음'; } } + +function isTechnicalErrorCode(value: string) { + return /^[A-Z0-9_]+$/.test(value) || /^[a-z0-9_]+$/.test(value); +}