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
13 changes: 13 additions & 0 deletions app/infrastructure/rabbitmq/result_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AnalysisRequestedEvent,
AnalysisResultEvent,
AnalysisResultPayload,
EvidenceCardDetail,
InstitutionMatchDetail,
RawScores,
RuleAnalysisDetail,
Expand Down Expand Up @@ -108,11 +109,23 @@ def build_payload(
),
urlAnalysis=(None if is_failed or not url else _url_detail(url)),
ruleAnalysis=(None if is_failed or not rules else _rule_detail(rules)),
evidenceCards=_evidence_cards(result.evidence),
failedTracks=list(failed_tracks),
failureCode=("PIPELINE_FAILED" if is_failed else None),
)


def _evidence_cards(evidence) -> list[EvidenceCardDetail]:
return [
EvidenceCardDetail(
category=item.category.value,
title=item.title,
description=item.description,
)
for item in (evidence or [])
]


def _text_detail(
text: dict,
failed_tracks: tuple[str, ...],
Expand Down
14 changes: 14 additions & 0 deletions app/infrastructure/rabbitmq/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ class RuleAnalysisDetail(BaseModel):
institutionMatch: InstitutionMatchDetail | None = None


class EvidenceCardDetail(BaseModel):
"""사용자 언어로 정리한 위험 근거 카드 (issue #64)"""

model_config = ConfigDict(extra="forbid")

category: str
title: str
description: str


class AnalysisResultPayload(BaseModel):
"""분석 결과 이벤트에 포함되는 통합 분석 페이로드 스키마"""

Expand All @@ -198,6 +208,10 @@ class AnalysisResultPayload(BaseModel):
urlAnalysis: UrlAnalysisDetail | None = None
ruleAnalysis: RuleAnalysisDetail | None = None

# 규칙/URL/텍스트(AI) 트랙을 모두 아우르는 결과라 특정 트랙 상세가 아닌 최상위에 둔다
# (institutionMatch와 달리 ruleAnalysis 하위가 아님 - Spring PR #102와 합의된 위치).
evidenceCards: list[EvidenceCardDetail] = Field(default_factory=list)

failedTracks: list[str] = Field(default_factory=list)
failureCode: str | None = None

Expand Down
33 changes: 33 additions & 0 deletions tests/infrastructure/rabbitmq/test_result_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
)
from app.analysis.schemas import (
ContributionBreakdown,
EvidenceCategory,
EvidenceItem,
RiskGrade,
SmishingAnalysisResponse,
)
Expand Down Expand Up @@ -108,6 +110,17 @@ def _result(
if status == "SUCCESS"
else None
),
evidence=(
[
EvidenceItem(
category=EvidenceCategory.INSTITUTION_IMPERSONATION,
title="기관 사칭",
description="국민은행을 언급했지만 공식 도메인이 아닙니다.",
)
]
if status == "SUCCESS"
else []
),
)


Expand Down Expand Up @@ -176,6 +189,26 @@ def test_factory_maps_successful_execution(
event.payload.ruleAnalysis.institutionMatch.textDomain
== "kb-bank-security.xyz"
)
assert len(event.payload.evidenceCards) == 1
assert event.payload.evidenceCards[0].category == "INSTITUTION_IMPERSONATION"
assert event.payload.evidenceCards[0].title == "기관 사칭"
assert event.payload.evidenceCards[0].description == (
"국민은행을 언급했지만 공식 도메인이 아닙니다."
)


def test_factory_maps_empty_evidence_cards_on_failure() -> None:
request = _request()
event = AnalysisResultEventFactory().create(
request=request,
execution=AnalysisExecution(
status=AnalysisExecutionStatus.FAILED,
result=_result(status="ERROR"),
failed_tracks=("PIPELINE",),
),
)

assert event.payload.evidenceCards == []


def test_factory_maps_institution_match_not_checked() -> None:
Expand Down
Loading