Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/analyses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type AnalysisResponse = {
errorCode?: string;
errorStage?: number;
errorMessage?: string;
contentAnalysisError?: string;
};

export type VerdictStatisticsResponse = Record<AnalysisVerdict, number>;
Expand Down
26 changes: 25 additions & 1 deletion app/(tabs)/(home)/scan-result-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getAnalysisDisplayUrl,
getAnalysisReasonText,
getAnalysisResultPath,
getContentAnalysisErrorText,
getRouteParam,
} from '@/utils/analysis-result-display';
import { useGuardedPress } from '@/utils/press-guard';
Expand All @@ -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;
Expand Down Expand Up @@ -107,7 +109,20 @@ export default function ScanResultBlockScreen() {
{isLoading && <Text style={styles.statusText}>분석 결과를 불러오는 중입니다.</Text>}
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}

<ScanResultReason reason={reason} style={[styles.reasonCard, isCompactResult && styles.reasonCardCompact]} />
<ScanResultReason
reason={reason}
style={[
styles.reasonCard,
isCompactResult && styles.reasonCardCompact,
Boolean(contentAnalysisErrorText) && styles.reasonCardWithNotice,
]}
/>

<ScanResultReason
label="페이지 접근 안내"
reason={contentAnalysisErrorText}
style={[styles.noticeCard, isCompactResult && styles.noticeCardCompact]}
/>

{/* 검사 대상 카드 */}
<View style={[styles.card, isCompactResult && styles.cardCompact]}>
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 25 additions & 1 deletion app/(tabs)/(home)/scan-result-caution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getAnalysisFinalUrl,
getAnalysisReasonText,
getAnalysisResultPath,
getContentAnalysisErrorText,
getRouteParam,
} from '@/utils/analysis-result-display';
import { showAlert } from '@/utils/guarded-alert';
Expand All @@ -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);
Expand Down Expand Up @@ -167,7 +169,20 @@ export default function ScanResultCautionScreen() {
{isLoading && <Text style={styles.statusText}>분석 결과를 불러오는 중입니다.</Text>}
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}

<ScanResultReason reason={reason} style={[styles.reasonCard, isCompactResult && styles.reasonCardCompact]} />
<ScanResultReason
reason={reason}
style={[
styles.reasonCard,
isCompactResult && styles.reasonCardCompact,
Boolean(contentAnalysisErrorText) && styles.reasonCardWithNotice,
]}
/>

<ScanResultReason
label="페이지 접근 안내"
reason={contentAnalysisErrorText}
style={[styles.noticeCard, isCompactResult && styles.noticeCardCompact]}
/>

{/* 검사 대상 카드 */}
<View style={[styles.card, isCompactResult && styles.cardCompact]}>
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 25 additions & 1 deletion app/(tabs)/(home)/scan-result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getAnalysisFinalUrl,
getAnalysisReasonText,
getAnalysisResultPath,
getContentAnalysisErrorText,
getRouteParam,
} from '@/utils/analysis-result-display';
import { showAlert } from '@/utils/guarded-alert';
Expand All @@ -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);
Expand Down Expand Up @@ -167,7 +169,20 @@ export default function ScanResultScreen() {
{isLoading && <Text style={styles.statusText}>분석 결과를 불러오는 중입니다.</Text>}
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}

<ScanResultReason reason={reason} style={[styles.reasonCard, isCompactResult && styles.reasonCardCompact]} />
<ScanResultReason
reason={reason}
style={[
styles.reasonCard,
isCompactResult && styles.reasonCardCompact,
Boolean(contentAnalysisErrorText) && styles.reasonCardWithNotice,
]}
/>

<ScanResultReason
label="페이지 접근 안내"
reason={contentAnalysisErrorText}
style={[styles.noticeCard, isCompactResult && styles.noticeCardCompact]}
/>

{/* 검사 대상 카드 */}
<View style={[styles.card, isCompactResult && styles.cardCompact]}>
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 31 additions & 1 deletion app/(tabs)/(home)/scanning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 : '';
}
Expand Down
5 changes: 3 additions & 2 deletions components/ui/scan-result-reason.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import { Colors, Typography } from '@/constants/theme';

interface ScanResultReasonProps {
reason: string;
label?: string;
style?: StyleProp<ViewStyle>;
}

export function ScanResultReason({ reason, style }: ScanResultReasonProps) {
export function ScanResultReason({ reason, label = '판정 이유', style }: ScanResultReasonProps) {
const normalizedReason = reason.trim();

if (!normalizedReason) return null;

return (
<View style={[styles.container, style]}>
<Text style={styles.label}>판정 이유</Text>
<Text style={styles.label}>{label}</Text>
<Text style={styles.reason} numberOfLines={4}>
{normalizedReason}
</Text>
Expand Down
21 changes: 21 additions & 0 deletions utils/analysis-result-display.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -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':
Expand All @@ -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);
}
Loading