55import java .math .BigDecimal ;
66import java .util .HashMap ;
77import java .util .List ;
8+ import java .util .Locale ;
89import java .util .Map ;
910import java .util .Optional ;
1011
1112import org .springframework .jdbc .core .RowMapper ;
1213import org .springframework .jdbc .core .namedparam .NamedParameterJdbcTemplate ;
1314import org .springframework .stereotype .Repository ;
15+ import org .springframework .transaction .annotation .Transactional ;
1416
1517import com .ssafy .salmanhae .model .dto .map .MapViewportItemType ;
1618import com .ssafy .salmanhae .model .dto .map .PropertyClusterViewportItem ;
2325import com .ssafy .salmanhae .model .dto .property .PropertyType ;
2426import com .ssafy .salmanhae .model .dto .property .RegionPriceStatResponse ;
2527import com .ssafy .salmanhae .model .dto .property .TransactionType ;
28+ import com .ssafy .salmanhae .model .dto .safety .PropertySafetyScoreResult ;
2629
2730@ Repository
2831public class JdbcPropertyDao implements PropertyDao {
@@ -35,6 +38,7 @@ public class JdbcPropertyDao implements PropertyDao {
3538 """ ;
3639
3740 private final NamedParameterJdbcTemplate jdbcTemplate ;
41+ private volatile Boolean onConflictSupported ;
3842
3943 public JdbcPropertyDao (NamedParameterJdbcTemplate jdbcTemplate ) {
4044 this .jdbcTemplate = jdbcTemplate ;
@@ -311,6 +315,19 @@ public Optional<PropertyRow> findActiveById(Long propertyId) {
311315 return rows .stream ().findFirst ();
312316 }
313317
318+ @ Override
319+ public List <PropertyRow > findActivePropertiesForSafetyScoring () {
320+ String sql = """
321+ SELECT %s
322+ FROM properties
323+ WHERE is_active = true
324+ AND latitude IS NOT NULL
325+ AND longitude IS NOT NULL
326+ ORDER BY id ASC
327+ """ .formatted (PROPERTY_COLUMNS );
328+ return jdbcTemplate .query (sql , Map .of (), propertyRowMapper ());
329+ }
330+
314331 @ Override
315332 public List <PropertyTransactionResponse > findComparableTransactions (PropertyRow property , String minContractYearMonth ) {
316333 String sql = """
@@ -368,6 +385,96 @@ public Optional<PropertySafetySummaryResponse> findSafetySummary(Long propertyId
368385 return rows .stream ().findFirst ();
369386 }
370387
388+ @ Override
389+ @ Transactional
390+ public int upsertSafetyScoreStats (List <PropertySafetyScoreResult > results ) {
391+ if (results == null || results .isEmpty ()) {
392+ return 0 ;
393+ }
394+ return results .stream ()
395+ .mapToInt (this ::upsertSafetyScoreStat )
396+ .sum ();
397+ }
398+
399+ private int upsertSafetyScoreStat (PropertySafetyScoreResult result ) {
400+ Map <String , Object > params = new HashMap <>();
401+ params .put ("propertyId" , result .propertyId ());
402+ params .put ("safetyScore" , result .safetyScore ());
403+ params .put ("cctvCount300m" , result .cctvCount300m ());
404+ params .put ("bellCount300m" , result .bellCount300m ());
405+ params .put ("lightCount300m" , result .lightCount300m ());
406+ params .put ("policeCount500m" , result .policeCount500m ());
407+ if (supportsOnConflict ()) {
408+ return upsertSafetyScoreStatWithOnConflict (params );
409+ }
410+ return upsertSafetyScoreStatWithUpdateInsert (params );
411+ }
412+
413+ private int upsertSafetyScoreStatWithOnConflict (Map <String , Object > params ) {
414+ return jdbcTemplate .update ("""
415+ INSERT INTO property_score_stat (
416+ property_id, safety_score, cctv_count_300m, bell_count_300m,
417+ light_count_300m, police_count_500m, created_at, updated_at
418+ ) VALUES (
419+ :propertyId, :safetyScore, :cctvCount300m, :bellCount300m,
420+ :lightCount300m, :policeCount500m, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
421+ )
422+ ON CONFLICT (property_id) DO UPDATE
423+ SET safety_score = EXCLUDED.safety_score,
424+ cctv_count_300m = EXCLUDED.cctv_count_300m,
425+ bell_count_300m = EXCLUDED.bell_count_300m,
426+ light_count_300m = EXCLUDED.light_count_300m,
427+ police_count_500m = EXCLUDED.police_count_500m,
428+ updated_at = CURRENT_TIMESTAMP
429+ """ , params );
430+ }
431+
432+ private int upsertSafetyScoreStatWithUpdateInsert (Map <String , Object > params ) {
433+ int updated = jdbcTemplate .update ("""
434+ UPDATE property_score_stat
435+ SET safety_score = :safetyScore,
436+ cctv_count_300m = :cctvCount300m,
437+ bell_count_300m = :bellCount300m,
438+ light_count_300m = :lightCount300m,
439+ police_count_500m = :policeCount500m,
440+ updated_at = CURRENT_TIMESTAMP
441+ WHERE property_id = :propertyId
442+ """ , params );
443+ if (updated > 0 ) {
444+ return updated ;
445+ }
446+ return jdbcTemplate .update ("""
447+ INSERT INTO property_score_stat (
448+ property_id, safety_score, cctv_count_300m, bell_count_300m,
449+ light_count_300m, police_count_500m, created_at, updated_at
450+ ) VALUES (
451+ :propertyId, :safetyScore, :cctvCount300m, :bellCount300m,
452+ :lightCount300m, :policeCount500m, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
453+ )
454+ """ , params );
455+ }
456+
457+ private boolean supportsOnConflict () {
458+ Boolean cached = onConflictSupported ;
459+ if (cached != null ) {
460+ return cached ;
461+ }
462+ onConflictSupported = detectOnConflictSupport ();
463+ return onConflictSupported ;
464+ }
465+
466+ private boolean detectOnConflictSupport () {
467+ if (jdbcTemplate .getJdbcTemplate ().getDataSource () == null ) {
468+ return true ;
469+ }
470+ try (var connection = jdbcTemplate .getJdbcTemplate ().getDataSource ().getConnection ()) {
471+ String productName = connection .getMetaData ().getDatabaseProductName ();
472+ return productName == null || !productName .toLowerCase (Locale .ROOT ).contains ("h2" );
473+ } catch (SQLException exception ) {
474+ return true ;
475+ }
476+ }
477+
371478 @ Override
372479 public List <RegionPriceStatResponse > findRegionPriceStats (
373480 String legalDongCode ,
0 commit comments