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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class RiskEvaluationService {

// 전세가율 기준
private static final double JEONSE_RATIO_CAUTION = 0.70;
private static final double MULTI_HOUSING_JEONSE_RATIO_CAUTION = 0.60;
private static final double JEONSE_RATIO_DANGER = 0.80;
// 선순위채권 부담 기준 (주택가격 90% × 60%)
private static final double PRIORITY_DEBT_LIMIT_RATIO = 0.54;
Expand All @@ -48,6 +49,8 @@ public class RiskEvaluationService {
Arrays.asList("SINGLE_FAMILY", "MULTI_FAMILY");
private static final List<String> COLLECTIVE_REGISTRY_TYPES =
Arrays.asList("APARTMENT", "MULTI_HOUSEHOLD", "OFFICETEL");
private static final Set<String> LOWER_JEONSE_RATIO_CAUTION_TYPES =
Set.of("MULTI_HOUSEHOLD");
private static final Set<String> KNOWN_OWNER_TYPES =
Set.of("INDIVIDUAL", "CORPORATION", "TRUST_COMPANY");

Expand Down Expand Up @@ -543,11 +546,14 @@ private FraudTypeResultDTO buildUnderwaterJeonse(
* 실제로 보는 것: 매매 기준가 대비 전세보증금 비율.
*
* [판정 로직]
* - 80% 이상 -> DANGER / 70~80% -> CAUTION / 70% 미만 -> SAFE
* - 연립·다세대: 80% 이상 -> DANGER / 60~80% -> CAUTION / 60% 미만 -> SAFE
* - 다가구: 다른 세입자의 선순위보증금을 알 수 없어 CAUTION / UNVERIFIED
* - 그 외 유형: 80% 이상 -> DANGER / 70~80% -> CAUTION / 70% 미만 -> SAFE
* - 가격 데이터 누락 -> CAUTION / UNVERIFIED
*/
private JudgementDTO judgeHighJeonseRatio(BuildingData building, PriceData price, Long deposit) {
// 다가구 실거래가는 건물 전체 가격이어서 한 세대 보증금과 직접 나눌 수 없다.
// 다가구는 분모가 건물 전체 가격이지만 분자는 특정 세대의 보증금이다.
// 다른 세입자의 선순위보증금을 모르는 상태에서 안전을 확정하지 않는다.
if (building != null && "MULTI_FAMILY".equals(building.getBuildingType())) {
return JudgementDTO.unverified();
}
Expand All @@ -557,8 +563,12 @@ private JudgementDTO judgeHighJeonseRatio(BuildingData building, PriceData price
}

double ratio = (double) deposit / basePrice;
String buildingType = building != null ? building.getBuildingType() : null;
double cautionThreshold = LOWER_JEONSE_RATIO_CAUTION_TYPES.contains(buildingType)
? MULTI_HOUSING_JEONSE_RATIO_CAUTION
: JEONSE_RATIO_CAUTION;
if (ratio >= JEONSE_RATIO_DANGER) return JudgementDTO.verified(RiskLevel.DANGER);
if (ratio >= JEONSE_RATIO_CAUTION) return JudgementDTO.verified(RiskLevel.CAUTION);
if (ratio >= cautionThreshold) return JudgementDTO.verified(RiskLevel.CAUTION);
return JudgementDTO.verified(RiskLevel.SAFE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,60 @@ static Stream<Arguments> jeonseRatioBoundaries() {
);
}

@ParameterizedTest(name = "deposit={0} -> {1}")
@MethodSource("multiHouseholdJeonseRatioBoundaries")
@DisplayName("연립·다세대는 전세가율 60%와 80% 경계값으로 판정한다")
void appliesMultiHouseholdJeonseRatioBoundaries(long deposit, RiskLevel expected) {
BuildingData building = baselineBuilding();
building.setBuildingType("MULTI_HOUSEHOLD");
building.setBuildingUse("다세대주택");

DetailResultDTO result = detail(
evaluate(
baselineRegistry(),
building,
price(1_000_000_000L),
deposit,
"서울"
),
DetailType.HIGH_JEONSE_RATIO
);

assertEquals(expected, result.getRiskLevel());
assertEquals(DataStatus.VERIFIED, result.getDataStatus());
}

static Stream<Arguments> multiHouseholdJeonseRatioBoundaries() {
return Stream.of(
Arguments.of(599_999_999L, RiskLevel.SAFE),
Arguments.of(600_000_000L, RiskLevel.CAUTION),
Arguments.of(799_999_999L, RiskLevel.CAUTION),
Arguments.of(800_000_000L, RiskLevel.DANGER)
);
}

@Test
@DisplayName("다가구는 건물 전체 가격과 한 세대 보증금을 직접 비교하지 않는다")
void multiFamilyJeonseRatioIsUnverified() {
BuildingData building = baselineBuilding();
building.setBuildingType("MULTI_FAMILY");
building.setBuildingUse("다가구주택");

DetailResultDTO result = detail(
evaluate(
baselineRegistry(),
building,
price(1_000_000_000L),
100_000_000L,
"서울"
),
DetailType.HIGH_JEONSE_RATIO
);

assertEquals(RiskLevel.CAUTION, result.getRiskLevel());
assertEquals(DataStatus.UNVERIFIED, result.getDataStatus());
}

@ParameterizedTest(name = "mortgage={0} -> {1}")
@CsvSource({
"0, SAFE",
Expand Down