diff --git a/src/pages/Report/ReportPage.tsx b/src/pages/Report/ReportPage.tsx index 9c21897..b669a32 100644 --- a/src/pages/Report/ReportPage.tsx +++ b/src/pages/Report/ReportPage.tsx @@ -7,7 +7,6 @@ import AppHeader from '@/shared/ui/app-header'; import { resolveRiskDetectionContent } from './constants/riskDetectionCatalog'; import { useReportPage } from './hooks/useReportPage'; -import { resolveAnalysisFailureNotice } from './lib/analysisFailureNotice'; import * as styles from './styles/reportPage.css'; import type { ReportStatusTone } from './types/reportPage.types'; @@ -96,7 +95,6 @@ export default function ReportPage() { ...resolveRiskDetectionContent(riskType, reportPageData.riskLevel), key: `${riskType}-${index}`, })); - const analysisFailureNotice = resolveAnalysisFailureNotice(reportPageData.detectedRiskTypes); const toneKey = reportPageData.riskLevel; @@ -157,36 +155,6 @@ export default function ReportPage() { /> - {analysisFailureNotice.hasFailureAlert ? ( -
-
- - ! - -
-

- {analysisFailureNotice.isOnlyAnalysisFailures - ? '분석 결과가 제한되었습니다' - : '일부 분석이 제한되었습니다'} -

-

- {analysisFailureNotice.isOnlyAnalysisFailures - ? '여러 검사 모듈이 정상적으로 완료되지 않아, 이 결과를 안전하다는 의미로 해석하면 안 됩니다.' - : '일부 검사 모듈이 정상적으로 완료되지 않아, 아래 결과에는 확인하지 못한 영역이 남아 있습니다.'} -

-
-
- -

- 실패한 검사: {analysisFailureNotice.labels.join(', ')} -

-

- 공식 사이트인지 직접 확인하기 전에는 로그인, 결제, 개인정보 입력, 파일 다운로드를 - 진행하지 마세요. -

-
- ) : null} -
diff --git a/src/pages/Report/constants/riskDetectionCatalog.test.ts b/src/pages/Report/constants/riskDetectionCatalog.test.ts index 9aaab84..285d528 100644 --- a/src/pages/Report/constants/riskDetectionCatalog.test.ts +++ b/src/pages/Report/constants/riskDetectionCatalog.test.ts @@ -49,6 +49,9 @@ const backendThreatCodes = [ 'XGB_FAILED', 'ML_FAILED', 'SCORING_FAILED', + 'XGB_SUSPICIOUS_URL_FEATURES', + 'XGB_HIGH_RISK_URL', + 'CHARCNN_SUSPICIOUS_URL_PATTERN', ] as const; describe('resolveRiskDetectionContent', () => { @@ -115,6 +118,21 @@ describe('resolveRiskDetectionContent', () => { }); }); + it('maps newly added ML model risk signals to user-facing descriptions', () => { + expect(resolveRiskDetectionContent('XGB_SUSPICIOUS_URL_FEATURES', 'warning')).toMatchObject({ + englishLabel: 'XGB SUSPICIOUS URL FEATURES', + title: 'XGB URL 특징 의심 신호 감지', + }); + expect(resolveRiskDetectionContent('XGB_HIGH_RISK_URL', 'critical')).toMatchObject({ + englishLabel: 'XGB HIGH RISK URL', + title: 'XGB 고위험 URL 감지', + }); + expect(resolveRiskDetectionContent('CHARCNN_SUSPICIOUS_URL_PATTERN', 'warning')).toMatchObject({ + englishLabel: 'CHARCNN SUSPICIOUS URL PATTERN', + title: 'CharCNN URL 패턴 의심 신호 감지', + }); + }); + it('uses user-facing copy for unknown threat fallbacks', () => { const content = resolveRiskDetectionContent('NEW_BACKEND_SIGNAL', 'critical'); diff --git a/src/pages/Report/constants/threatText.ts b/src/pages/Report/constants/threatText.ts index db2e100..15f7d1d 100644 --- a/src/pages/Report/constants/threatText.ts +++ b/src/pages/Report/constants/threatText.ts @@ -524,6 +524,38 @@ export const threatTextCatalog: RiskDetectionCatalogItem[] = [ risk: '최종 위험도 산정이 제한되어 실제 위험 수준이 과소 또는 과대 표시될 수 있습니다.', title: '위험 점수 산정 실패 감지', }, + { + description: + 'XGBoost 모델이 URL 길이, 특수문자 사용, 숫자와 영문 조합, 경로 구성 같은 특징에서 의심스러운 패턴을 확인했습니다.', + englishLabel: 'XGB SUSPICIOUS URL FEATURES', + names: [ + 'XGB_SUSPICIOUS_URL_FEATURES', + 'xgb_suspicious_url_features', + 'xgboost_suspicious_url_features', + ], + risk: '정상 사이트에서도 일부 특징이 겹칠 수 있지만, 피싱이나 악성 URL은 주소를 복잡하게 만들거나 사용자를 속이는 문자열 조합을 자주 사용합니다. 공식 주소인지 다시 확인해야 합니다.', + title: 'XGB URL 특징 의심 신호 감지', + }, + { + description: + 'XGBoost 모델이 URL의 여러 특징을 종합했을 때 고위험 주소일 가능성이 높다고 판단했습니다.', + englishLabel: 'XGB HIGH RISK URL', + names: ['XGB_HIGH_RISK_URL', 'xgb_high_risk_url', 'xgboost_high_risk_url'], + risk: '주소 구조와 패턴이 위험 사이트와 유사하게 나타난 상태입니다. 로그인, 결제, 개인정보 입력, 파일 다운로드를 진행하지 말고 공식 경로로 다시 접속하는 것이 안전합니다.', + title: 'XGB 고위험 URL 감지', + }, + { + description: + 'CharCNN 모델이 URL 문자열 자체의 글자 배열에서 피싱이나 악성 주소에서 자주 보이는 의심 패턴을 확인했습니다.', + englishLabel: 'CHARCNN SUSPICIOUS URL PATTERN', + names: [ + 'CHARCNN_SUSPICIOUS_URL_PATTERN', + 'charcnn_suspicious_url_pattern', + 'char_cnn_suspicious_url_pattern', + ], + risk: '사람이 보기에는 정상 주소처럼 보여도 글자 단위 패턴이 위장 도메인, 난독화 주소, 유사 도메인과 닮아 있을 수 있습니다. 민감한 정보를 입력하기 전에 반드시 공식 주소와 비교해야 합니다.', + title: 'CharCNN URL 패턴 의심 신호 감지', + }, { description: 'URL에서 접속 대상 호스트명을 확인하지 못했거나 DNS 해석에 실패한 신호입니다.', englishLabel: 'HOSTNAME MISSING', diff --git a/src/pages/Report/lib/analysisFailureNotice.test.ts b/src/pages/Report/lib/analysisFailureNotice.test.ts deleted file mode 100644 index 67d8c1b..0000000 --- a/src/pages/Report/lib/analysisFailureNotice.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { resolveAnalysisFailureNotice } from './analysisFailureNotice'; - -describe('resolveAnalysisFailureNotice', () => { - it('resolves failure labels with provider or module prefixes', () => { - const notice = resolveAnalysisFailureNotice([ - 'MODEL:ML:CHARCNN_FAILED', - 'OTX:OTX_FAILED', - 'PHISHING', - ]); - - expect(notice).toEqual({ - hasFailureAlert: true, - isOnlyAnalysisFailures: false, - labels: ['문자 패턴 AI 분석', 'OTX 위협 정보 조회'], - }); - }); - - it('detects an all-failure result even when labels are deduplicated', () => { - const notice = resolveAnalysisFailureNotice(['REDIRECT_FAILED', 'REDIRECT:REDIRECT_FAILED']); - - expect(notice).toEqual({ - hasFailureAlert: true, - isOnlyAnalysisFailures: true, - labels: ['리다이렉트 분석'], - }); - }); -}); diff --git a/src/pages/Report/lib/analysisFailureNotice.ts b/src/pages/Report/lib/analysisFailureNotice.ts deleted file mode 100644 index d2f2395..0000000 --- a/src/pages/Report/lib/analysisFailureNotice.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { resolveRiskTypeLookupKeys } from '../constants/riskDetectionCatalog'; - -const analysisFailureTypeLabels: Record = { - certificatefailed: '인증서 분석', - charcnnfailed: '문자 패턴 AI 분석', - gsbfailed: 'Google Safe Browsing 조회', - mlfailed: '머신러닝 종합 분석', - otxfailed: 'OTX 위협 정보 조회', - redirectclienterror: '리다이렉트 추적', - redirectfailed: '리다이렉트 분석', - redirectinvalidlocation: '리다이렉트 위치 확인', - redirectloopdetected: '리다이렉트 루프 확인', - redirectrequestfailed: '리다이렉트 요청', - redirecttoomanyredirects: '과도한 리다이렉트 확인', - scoringfailed: '최종 위험 점수 산정', - serverinfofailed: '서버 정보 조회', - whoisfailed: 'WHOIS 도메인 조회', - xgbfailed: 'XGB AI 분석', -}; - -export type AnalysisFailureNotice = { - hasFailureAlert: boolean; - isOnlyAnalysisFailures: boolean; - labels: string[]; -}; - -function resolveAnalysisFailureLabel(riskType: string): string | null { - const failureKey = resolveRiskTypeLookupKeys(riskType).find((lookupKey) => - Boolean(analysisFailureTypeLabels[lookupKey]), - ); - - return failureKey ? analysisFailureTypeLabels[failureKey] : null; -} - -export function resolveAnalysisFailureNotice(detectedRiskTypes: string[]): AnalysisFailureNotice { - const resolvedLabels = detectedRiskTypes.map((riskType) => resolveAnalysisFailureLabel(riskType)); - const labels = Array.from( - new Set(resolvedLabels.filter((label): label is string => label !== null)), - ); - const failureCount = resolvedLabels.filter((label) => label !== null).length; - - return { - hasFailureAlert: labels.length > 0, - isOnlyAnalysisFailures: - detectedRiskTypes.length > 0 && failureCount === detectedRiskTypes.length, - labels, - }; -} diff --git a/src/pages/Report/styles/reportPage.css.ts b/src/pages/Report/styles/reportPage.css.ts index 9502055..57e46a7 100644 --- a/src/pages/Report/styles/reportPage.css.ts +++ b/src/pages/Report/styles/reportPage.css.ts @@ -223,72 +223,6 @@ export const metricsGrid = style({ }, }); -export const analysisFailureNotice = style({ - borderRadius: vars.radius.lg, - border: '1px solid #F4C56E', - backgroundColor: '#FFF8E8', - padding: vars.spacing.lg, - display: 'grid', - gap: vars.spacing.sm, - boxShadow: '0 8px 18px rgba(138, 89, 0, 0.08)', - '@media': { - print: { - breakInside: 'avoid', - boxShadow: 'none', - }, - }, -}); - -export const analysisFailureNoticeHeader = style({ - display: 'flex', - alignItems: 'flex-start', - gap: vars.spacing.sm, -}); - -export const analysisFailureNoticeIcon = style({ - width: '30px', - height: '30px', - borderRadius: '999px', - backgroundColor: '#D97706', - color: vars.colors.white, - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - flexShrink: 0, - fontSize: vars.font.size.md, - fontWeight: vars.font.weight.bold, - lineHeight: 1, -}); - -export const analysisFailureNoticeTitleBlock = style({ - display: 'grid', - gap: '4px', -}); - -export const analysisFailureNoticeTitle = style({ - margin: 0, - color: '#7C3F00', - fontSize: vars.font.size.xl, - fontWeight: vars.font.weight.bold, - lineHeight: 1.35, -}); - -export const analysisFailureNoticeSummary = style({ - margin: 0, - color: '#8A5A12', - fontSize: vars.font.size.sm, - fontWeight: vars.font.weight.semibold, - lineHeight: 1.6, -}); - -export const analysisFailureNoticeText = style({ - margin: 0, - color: '#5F4824', - fontSize: vars.font.size.sm, - fontWeight: vars.font.weight.medium, - lineHeight: 1.65, -}); - export const sectionCard = style({ borderRadius: vars.radius.lg, border: '1px solid #F0E4BE',