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 backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ bin/

# 문서 폴더
/docs/
/scripts/
# 로컬 전용 분석 워크플로 API 명세
/analysis-workflow-api-spec.md

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public class ReportFraudType {
private Long analysisReportId;
private FraudType fraudType;

/** 세부 3개 중 최악값. */
/** 세부 판정들을 개수 기반으로 집계한 유형 대표값. */
private RiskLevel riskLevel;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
@AllArgsConstructor
public class FraudTypeResultDTO {
private FraudType fraudType;
private RiskLevel riskLevel; // 세부 3개 중 최악값
private RiskLevel riskLevel; // 세부 판정들을 집계한 유형 대표값
private List<DetailResultDTO> details; // 세부 3개
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
Expand All @@ -17,6 +18,21 @@ public static RiskLevel aggregateChecks(List<JudgementDTO> judgements) {
return aggregate(judgements, CHECK_DANGER_THRESHOLD);
}

/**
* 필수점검 대표값 1개와 사기 유형 대표값 3개를 최종 집계한다.
* DANGER가 하나라도 있거나 CAUTION이 3개 이상이면 DANGER다.
*/
public static RiskLevel aggregateOverall(List<RiskLevel> levels) {
if (levels == null) {
return RiskLevel.SAFE;
}

return RiskLevel.aggregateByCount(
levels.stream().filter(Objects::nonNull).toList(),
OVERALL_DANGER_THRESHOLD
);
}

public static RiskLevel aggregateDetails(List<JudgementDTO> judgements) {
List<JudgementDTO> values = validValues(judgements);

Expand Down Expand Up @@ -65,4 +81,4 @@ private static List<JudgementDTO> validValues(
.filter(Objects::nonNull)
.toList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// 위험 분석 결과
@Getter
public class RiskEvaluationResultDTO {
private final RiskLevel overallRiskLevel; // 필수 5 + 유형 3 중 최악값
private final RiskLevel overallRiskLevel; // 필수점검 대표값 1개 + 유형 대표값 3개의 개수 기반 집계
private final List<CheckResultDTO> checkResultDTOS; // 필수 5개
private final List<FraudTypeResultDTO> fraudTypeResultDTOS; // 유형 3개 (각각 세부 3개 포함)

Expand Down Expand Up @@ -49,4 +49,4 @@ public RiskEvaluationResultDTO(
) {
this(overallRiskLevel, checkResultDTOS, fraudTypeResultDTOS, null, null, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/** 위험도 판단 순서
1. 필수점검 5개 판정 → 개수기반 집계로 대표값 1개
2. 유형별 세부 9개 판정 (3개씩) → 개수기반 집계로 유형 대표값 3개
3. [필수점검대표값, 유형1, 유형2, 유형3] 4개 중 최악값 = 전체 결과
3. 대표값 4개 중 DANGER 1개 또는 CAUTION 3개 이상이면 전체 DANGER
**/

@Service
Expand Down Expand Up @@ -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<RiskLevel> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RiskLevel> 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() {
Expand Down