Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
@@ -0,0 +1,38 @@
package com.ssafy.salmanhae.batch;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.ssafy.salmanhae.model.dto.safety.PropertySafetyScoreResult;
import com.ssafy.salmanhae.service.safety.PropertySafetyScoreService;

@Component
@ConditionalOnProperty(
prefix = "safety.score.scheduler",
name = "enabled",
havingValue = "true"
)
public class PropertySafetyScoreScheduler {

private static final Logger log = LoggerFactory.getLogger(PropertySafetyScoreScheduler.class);

private final PropertySafetyScoreService propertySafetyScoreService;

public PropertySafetyScoreScheduler(PropertySafetyScoreService propertySafetyScoreService) {
this.propertySafetyScoreService = propertySafetyScoreService;
}

@Scheduled(
cron = "${safety.score.scheduler.cron:0 30 3 1 * *}",
zone = "${safety.score.scheduler.zone:Asia/Seoul}"
)
public void runMonthlyRecalculation() {
List<PropertySafetyScoreResult> results = propertySafetyScoreService.recalculateAll();
log.info("Monthly property safety score recalculation completed: properties={}", results.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.ssafy.salmanhae.model.dto.map.MapViewportItemType;
import com.ssafy.salmanhae.model.dto.map.PropertyClusterViewportItem;
Expand All @@ -23,6 +25,7 @@
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.RegionPriceStatResponse;
import com.ssafy.salmanhae.model.dto.property.TransactionType;
import com.ssafy.salmanhae.model.dto.safety.PropertySafetyScoreResult;

@Repository
public class JdbcPropertyDao implements PropertyDao {
Expand All @@ -35,6 +38,7 @@ public class JdbcPropertyDao implements PropertyDao {
""";

private final NamedParameterJdbcTemplate jdbcTemplate;
private volatile Boolean onConflictSupported;

public JdbcPropertyDao(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
Expand Down Expand Up @@ -311,6 +315,19 @@ public Optional<PropertyRow> findActiveById(Long propertyId) {
return rows.stream().findFirst();
}

@Override
public List<PropertyRow> findActivePropertiesForSafetyScoring() {
String sql = """
SELECT %s
FROM properties
WHERE is_active = true
AND latitude IS NOT NULL
AND longitude IS NOT NULL
ORDER BY id ASC
""".formatted(PROPERTY_COLUMNS);
return jdbcTemplate.query(sql, Map.of(), propertyRowMapper());
}

@Override
public List<PropertyTransactionResponse> findComparableTransactions(PropertyRow property, String minContractYearMonth) {
String sql = """
Expand Down Expand Up @@ -368,6 +385,96 @@ public Optional<PropertySafetySummaryResponse> findSafetySummary(Long propertyId
return rows.stream().findFirst();
}

@Override
@Transactional
public int upsertSafetyScoreStats(List<PropertySafetyScoreResult> results) {
if (results == null || results.isEmpty()) {
return 0;
}
return results.stream()
.mapToInt(this::upsertSafetyScoreStat)
.sum();
}

private int upsertSafetyScoreStat(PropertySafetyScoreResult result) {
Map<String, Object> params = new HashMap<>();
params.put("propertyId", result.propertyId());
params.put("safetyScore", result.safetyScore());
params.put("cctvCount300m", result.cctvCount300m());
params.put("bellCount300m", result.bellCount300m());
params.put("lightCount300m", result.lightCount300m());
params.put("policeCount500m", result.policeCount500m());
if (supportsOnConflict()) {
return upsertSafetyScoreStatWithOnConflict(params);
}
return upsertSafetyScoreStatWithUpdateInsert(params);
}

private int upsertSafetyScoreStatWithOnConflict(Map<String, Object> params) {
return jdbcTemplate.update("""
INSERT INTO property_score_stat (
property_id, safety_score, cctv_count_300m, bell_count_300m,
light_count_300m, police_count_500m, created_at, updated_at
) VALUES (
:propertyId, :safetyScore, :cctvCount300m, :bellCount300m,
:lightCount300m, :policeCount500m, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (property_id) DO UPDATE
SET safety_score = EXCLUDED.safety_score,
cctv_count_300m = EXCLUDED.cctv_count_300m,
bell_count_300m = EXCLUDED.bell_count_300m,
light_count_300m = EXCLUDED.light_count_300m,
police_count_500m = EXCLUDED.police_count_500m,
updated_at = CURRENT_TIMESTAMP
""", params);
}

private int upsertSafetyScoreStatWithUpdateInsert(Map<String, Object> params) {
int updated = jdbcTemplate.update("""
UPDATE property_score_stat
SET safety_score = :safetyScore,
cctv_count_300m = :cctvCount300m,
bell_count_300m = :bellCount300m,
light_count_300m = :lightCount300m,
police_count_500m = :policeCount500m,
updated_at = CURRENT_TIMESTAMP
WHERE property_id = :propertyId
""", params);
if (updated > 0) {
return updated;
}
return jdbcTemplate.update("""
INSERT INTO property_score_stat (
property_id, safety_score, cctv_count_300m, bell_count_300m,
light_count_300m, police_count_500m, created_at, updated_at
) VALUES (
:propertyId, :safetyScore, :cctvCount300m, :bellCount300m,
:lightCount300m, :policeCount500m, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
""", params);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private boolean supportsOnConflict() {
Boolean cached = onConflictSupported;
if (cached != null) {
return cached;
}
onConflictSupported = detectOnConflictSupport();
return onConflictSupported;
}

private boolean detectOnConflictSupport() {
if (jdbcTemplate.getJdbcTemplate().getDataSource() == null) {
return true;
}
try (var connection = jdbcTemplate.getJdbcTemplate().getDataSource().getConnection()) {
String productName = connection.getMetaData().getDatabaseProductName();
return productName == null || !productName.toLowerCase(Locale.ROOT).contains("h2");
} catch (SQLException exception) {
return true;
}
}

@Override
public List<RegionPriceStatResponse> findRegionPriceStats(
String legalDongCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.ssafy.salmanhae.model.dto.property.PropertyRow;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.TransactionType;
import com.ssafy.salmanhae.model.dto.safety.PropertySafetyScoreResult;

public interface PropertyDao {

Expand All @@ -35,10 +36,14 @@ List<PropertyClusterViewportItem> findPropertyClusters(

Optional<PropertyRow> findActiveById(Long propertyId);

List<PropertyRow> findActivePropertiesForSafetyScoring();

List<PropertyTransactionResponse> findComparableTransactions(PropertyRow property, String minContractYearMonth);

Optional<PropertySafetySummaryResponse> findSafetySummary(Long propertyId, Integer radius);

int upsertSafetyScoreStats(List<PropertySafetyScoreResult> results);

List<RegionPriceStatResponse> findRegionPriceStats(
String legalDongCode,
PropertyType propertyType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ssafy.salmanhae.model.dto.safety;

public record PropertySafetyScoreInput(
Long propertyId,
int cctvCount300m,
int bellCount300m,
int lightCount300m,
int policeCount500m
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ssafy.salmanhae.model.dto.safety;

public record PropertySafetyScoreResult(
Long propertyId,
int safetyScore,
int cctvCount300m,
int bellCount300m,
int lightCount300m,
int policeCount500m
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ssafy.salmanhae.service.safety;

import java.util.List;

import com.ssafy.salmanhae.model.dto.safety.PropertySafetyScoreInput;
import com.ssafy.salmanhae.model.dto.safety.PropertySafetyScoreResult;

public interface PropertySafetyScoreService {

List<PropertySafetyScoreResult> recalculateAll();

PropertySafetyScoreResult calculateScore(PropertySafetyScoreInput input);
}
Loading