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',