diff --git a/backend/.gitignore b/backend/.gitignore index b017271..1ade2aa 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -47,6 +47,7 @@ bin/ # 문서 폴더 /docs/ +/scripts/ # 로컬 전용 분석 워크플로 API 명세 /analysis-workflow-api-spec.md diff --git a/backend/src/main/java/com/secondzip/backend/report/domain/ReportFraudType.java b/backend/src/main/java/com/secondzip/backend/report/domain/ReportFraudType.java index 62538d9..39be120 100644 --- a/backend/src/main/java/com/secondzip/backend/report/domain/ReportFraudType.java +++ b/backend/src/main/java/com/secondzip/backend/report/domain/ReportFraudType.java @@ -23,6 +23,6 @@ public class ReportFraudType { private Long analysisReportId; private FraudType fraudType; - /** 세부 3개 중 최악값. */ + /** 세부 판정들을 개수 기반으로 집계한 유형 대표값. */ private RiskLevel riskLevel; } diff --git a/backend/src/main/java/com/secondzip/backend/report/dto/FraudTypeResultDTO.java b/backend/src/main/java/com/secondzip/backend/report/dto/FraudTypeResultDTO.java index 3e9bd35..ca0c163 100644 --- a/backend/src/main/java/com/secondzip/backend/report/dto/FraudTypeResultDTO.java +++ b/backend/src/main/java/com/secondzip/backend/report/dto/FraudTypeResultDTO.java @@ -12,6 +12,6 @@ @AllArgsConstructor public class FraudTypeResultDTO { private FraudType fraudType; - private RiskLevel riskLevel; // 세부 3개 중 최악값 + private RiskLevel riskLevel; // 세부 판정들을 집계한 유형 대표값 private List details; // 세부 3개 -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/secondzip/backend/report/dto/RiskAggregation.java b/backend/src/main/java/com/secondzip/backend/report/dto/RiskAggregation.java index 087d79f..9ae83f0 100644 --- a/backend/src/main/java/com/secondzip/backend/report/dto/RiskAggregation.java +++ b/backend/src/main/java/com/secondzip/backend/report/dto/RiskAggregation.java @@ -9,6 +9,7 @@ public final class RiskAggregation { private static final int CHECK_DANGER_THRESHOLD = 3; + private static final int OVERALL_DANGER_THRESHOLD = 3; private RiskAggregation() { } @@ -17,6 +18,21 @@ public static RiskLevel aggregateChecks(List judgements) { return aggregate(judgements, CHECK_DANGER_THRESHOLD); } + /** + * 필수점검 대표값 1개와 사기 유형 대표값 3개를 최종 집계한다. + * DANGER가 하나라도 있거나 CAUTION이 3개 이상이면 DANGER다. + */ + public static RiskLevel aggregateOverall(List levels) { + if (levels == null) { + return RiskLevel.SAFE; + } + + return RiskLevel.aggregateByCount( + levels.stream().filter(Objects::nonNull).toList(), + OVERALL_DANGER_THRESHOLD + ); + } + public static RiskLevel aggregateDetails(List judgements) { List values = validValues(judgements); @@ -65,4 +81,4 @@ private static List validValues( .filter(Objects::nonNull) .toList(); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/secondzip/backend/report/dto/RiskEvaluationResultDTO.java b/backend/src/main/java/com/secondzip/backend/report/dto/RiskEvaluationResultDTO.java index 860d046..27fceb5 100644 --- a/backend/src/main/java/com/secondzip/backend/report/dto/RiskEvaluationResultDTO.java +++ b/backend/src/main/java/com/secondzip/backend/report/dto/RiskEvaluationResultDTO.java @@ -8,7 +8,7 @@ // 위험 분석 결과 @Getter public class RiskEvaluationResultDTO { - private final RiskLevel overallRiskLevel; // 필수 5 + 유형 3 중 최악값 + private final RiskLevel overallRiskLevel; // 필수점검 대표값 1개 + 유형 대표값 3개의 개수 기반 집계 private final List checkResultDTOS; // 필수 5개 private final List fraudTypeResultDTOS; // 유형 3개 (각각 세부 3개 포함) @@ -49,4 +49,4 @@ public RiskEvaluationResultDTO( ) { this(overallRiskLevel, checkResultDTOS, fraudTypeResultDTOS, null, null, null); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/secondzip/backend/report/service/RiskEvaluationService.java b/backend/src/main/java/com/secondzip/backend/report/service/RiskEvaluationService.java index 3b4bd49..ad89048 100644 --- a/backend/src/main/java/com/secondzip/backend/report/service/RiskEvaluationService.java +++ b/backend/src/main/java/com/secondzip/backend/report/service/RiskEvaluationService.java @@ -20,7 +20,7 @@ /** 위험도 판단 순서 1. 필수점검 5개 판정 → 개수기반 집계로 대표값 1개 2. 유형별 세부 9개 판정 (3개씩) → 개수기반 집계로 유형 대표값 3개 - 3. [필수점검대표값, 유형1, 유형2, 유형3] 4개 중 최악값 = 전체 결과 + 3. 대표값 4개 중 DANGER 1개 또는 CAUTION 3개 이상이면 전체 DANGER **/ @Service @@ -125,11 +125,11 @@ public RiskEvaluationResultDTO evaluate(RegistryData registry, BuildingData buil fraudTypeResultDTOS.add(buildRightsConcealment(registry, building)); fraudTypeResultDTOS.add(buildTrustPropertyFraud(registry)); - // ===== 3. 전체 결과 = [필수점검최종, 유형1, 유형2, 유형3] 4개 중 최악값 ===== + // ===== 3. 전체 결과 = 대표값 4개 중 DANGER 1개 또는 CAUTION 3개 이상이면 DANGER ===== List topLevels = new ArrayList<>(); topLevels.add(checkOverall); fraudTypeResultDTOS.forEach(f -> topLevels.add(f.getRiskLevel())); - RiskLevel overall = RiskLevel.worstOf(topLevels); + RiskLevel overall = RiskAggregation.aggregateOverall(topLevels); return new RiskEvaluationResultDTO( overall, diff --git a/backend/src/test/java/com/secondzip/backend/report/service/RiskEvaluationServiceTest.java b/backend/src/test/java/com/secondzip/backend/report/service/RiskEvaluationServiceTest.java index 91e58ad..2d1bdec 100644 --- a/backend/src/test/java/com/secondzip/backend/report/service/RiskEvaluationServiceTest.java +++ b/backend/src/test/java/com/secondzip/backend/report/service/RiskEvaluationServiceTest.java @@ -273,6 +273,49 @@ void cleanApartmentIsSafeOverall() { ); } + @Test + @DisplayName("필수점검과 유형 대표값 중 CAUTION이 3개면 전체 결과가 DANGER다") + void threeCautionRepresentativesEscalateOverallToDanger() { + RegistryData registry = cleanRegistry(); + registry.setMortgageAmount(40_000_000L); + + BuildingData building = cleanApartment(); + building.setBuildingUse("공동주택(아파트)"); + building.setBuildingLevelNonResidentialUses("근린생활시설"); + + RiskEvaluationResultDTO evaluation = service.evaluate( + registry, + building, + price(450_000_000L), + 315_000_000L, + "서울특별시 성동구 왕십리로 410" + ); + + RiskLevel checkOverall = RiskAggregation.aggregateChecks( + evaluation.getCheckResultDTOS().stream() + .map(result -> new JudgementDTO( + result.getRiskLevel(), result.getDataStatus())) + .toList() + ); + List representatives = List.of( + checkOverall, + evaluation.getFraudTypeResultDTOS().get(0).getRiskLevel(), + evaluation.getFraudTypeResultDTOS().get(1).getRiskLevel(), + evaluation.getFraudTypeResultDTOS().get(2).getRiskLevel() + ); + + assertEquals( + List.of( + RiskLevel.CAUTION, + RiskLevel.CAUTION, + RiskLevel.CAUTION, + RiskLevel.SAFE + ), + representatives + ); + assertEquals(RiskLevel.DANGER, evaluation.getOverallRiskLevel()); + } + @Test @DisplayName("1층이 근린생활시설인 정상 다세대는 위험이 아니라 주의로 남긴다") void residentialUnitInMixedUseBuildingIsCautionNotDanger() {