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 @@ -6,6 +6,8 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import jakarta.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionHandler {

Expand All @@ -19,7 +21,8 @@ public ResponseEntity<ErrorResponse> handleApiException(ApiException exception)

@ExceptionHandler({
MissingServletRequestParameterException.class,
MethodArgumentTypeMismatchException.class
MethodArgumentTypeMismatchException.class,
ConstraintViolationException.class
})
public ResponseEntity<ErrorResponse> handleBadRequest(Exception exception) {
return ResponseEntity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ssafy.salmanhae.controller.price;

import com.ssafy.salmanhae.common.response.ApiResponse;
import com.ssafy.salmanhae.model.dto.property.PriceAnalysisResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.TransactionType;
import com.ssafy.salmanhae.service.property.PropertyService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/price-analysis")
@Validated
public class PriceAnalysisController {

private final PropertyService propertyService;

public PriceAnalysisController(PropertyService propertyService) {
this.propertyService = propertyService;
}

@GetMapping
public ApiResponse<PriceAnalysisResponse> getPriceAnalysis(
@RequestParam @NotBlank @Pattern(regexp = "\\d{10}") String legalDongCode,
@RequestParam @NotNull PropertyType propertyType,
@RequestParam @NotNull TransactionType transactionType
) {
return ApiResponse.ok(propertyService.getPriceAnalysis(legalDongCode, propertyType, transactionType));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@
import com.ssafy.salmanhae.common.response.ApiResponse;
import com.ssafy.salmanhae.common.response.ListResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyDetailResponse;
import com.ssafy.salmanhae.model.dto.property.PropertySafetySummaryResponse;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.PropertySummaryResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyTransactionResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.TransactionType;
import com.ssafy.salmanhae.service.property.PropertyService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import org.springframework.validation.annotation.Validated;

@RestController
@RequestMapping("/api/v1/properties")
@Validated
public class PropertyController {

private final PropertyService propertyService;
Expand Down Expand Up @@ -59,7 +67,23 @@ public ApiResponse<ListResponse<PropertySummaryResponse>> searchProperties(
}

@GetMapping("/{propertyId}")
public ApiResponse<PropertyDetailResponse> getProperty(@PathVariable Long propertyId) {
public ApiResponse<PropertyDetailResponse> getProperty(@PathVariable @NotNull @Positive Long propertyId) {
return ApiResponse.ok(propertyService.getProperty(propertyId));
}

@GetMapping("/{propertyId}/transactions")
public ApiResponse<ListResponse<PropertyTransactionResponse>> getPropertyTransactions(
@PathVariable @NotNull @Positive Long propertyId,
@RequestParam(required = false) @Min(1) @Max(10) Integer years
) {
return ApiResponse.ok(ListResponse.from(propertyService.getTransactions(propertyId, years)));
}

@GetMapping("/{propertyId}/safety-summary")
public ApiResponse<PropertySafetySummaryResponse> getPropertySafetySummary(
@PathVariable @NotNull @Positive Long propertyId,
@RequestParam(required = false) @Min(300) @Max(500) Integer radius
) {
return ApiResponse.ok(propertyService.getSafetySummary(propertyId, radius));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.sql.ResultSet;
import java.sql.SQLException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -11,9 +12,13 @@
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

import com.ssafy.salmanhae.model.dto.property.BuildingPriceStatResponse;
import com.ssafy.salmanhae.model.dto.property.PropertySafetySummaryResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyRow;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.PropertyTransactionResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.RegionPriceStatResponse;
import com.ssafy.salmanhae.model.dto.property.TransactionType;

@Repository
Expand Down Expand Up @@ -90,10 +95,166 @@ public Optional<PropertyRow> findActiveById(Long propertyId) {
return rows.stream().findFirst();
}

@Override
public List<PropertyTransactionResponse> findComparableTransactions(PropertyRow property, String minContractYearMonth) {
String sql = """
SELECT transaction_type, contract_year_month, deposit, monthly_rent, price, area_m2, floor,
CASE WHEN building_key = :buildingKey THEN 0 ELSE 1 END AS match_rank
FROM transaction_history
WHERE contract_year_month >= :minContractYearMonth
AND transaction_type = :transactionType
AND property_type = :propertyType
AND (
building_key = :buildingKey
OR (
legal_dong_code = :legalDongCode
AND area_m2 BETWEEN :minAreaM2 AND :maxAreaM2
)
)
ORDER BY match_rank ASC, contract_year_month DESC, contract_day DESC NULLS LAST, id DESC
LIMIT 20
""";
BigDecimal areaM2 = property.areaM2();
Map<String, Object> params = Map.of(
"buildingKey", property.buildingKey(),
"minContractYearMonth", minContractYearMonth,
"transactionType", property.transactionType().name(),
"propertyType", property.propertyType().name(),
"legalDongCode", property.legalDongCode(),
"minAreaM2", areaM2.subtract(BigDecimal.TEN),
"maxAreaM2", areaM2.add(BigDecimal.TEN)
);
return jdbcTemplate.query(sql, params, propertyTransactionMapper());
}

@Override
public Optional<PropertySafetySummaryResponse> findSafetySummary(Long propertyId, Integer radius) {
String sql = """
SELECT property_id, safety_score, price_score, cctv_count_300m, bell_count_300m,
light_count_300m, police_count_500m
FROM property_score_stat
WHERE property_id = :propertyId
""";
List<PropertySafetySummaryResponse> rows = jdbcTemplate.query(
sql,
Map.of("propertyId", propertyId),
(rs, rowNum) -> new PropertySafetySummaryResponse(
rs.getLong("property_id"),
radius,
nullableInteger(rs, "safety_score"),
nullableInteger(rs, "price_score"),
nullableInteger(rs, "cctv_count_300m"),
nullableInteger(rs, "bell_count_300m"),
nullableInteger(rs, "light_count_300m"),
nullableInteger(rs, "police_count_500m")
)
);
return rows.stream().findFirst();
}

@Override
public List<RegionPriceStatResponse> findRegionPriceStats(
String legalDongCode,
PropertyType propertyType,
TransactionType transactionType
) {
String sql = """
SELECT region_level, region_code, sido, sigungu, dong, avg_deposit, median_deposit,
avg_monthly_rent, median_monthly_rent, avg_price, median_price,
transaction_count, sample_from_ym, sample_to_ym
FROM region_price_stat
WHERE region_code = :legalDongCode
AND property_type = :propertyType
AND transaction_type = :transactionType
ORDER BY region_level, region_code
""";
Map<String, Object> params = Map.of(
"legalDongCode", legalDongCode,
"propertyType", propertyType.name(),
"transactionType", transactionType.name()
);
return jdbcTemplate.query(sql, params, regionPriceStatMapper());
}

@Override
public List<BuildingPriceStatResponse> findBuildingPriceStats(
String legalDongCode,
PropertyType propertyType,
TransactionType transactionType
) {
String sql = """
SELECT building_key, building_name, sido, sigungu, dong, avg_deposit, median_deposit,
avg_monthly_rent, median_monthly_rent, avg_price, median_price,
transaction_count, sample_from_ym, sample_to_ym
FROM building_price_stat
WHERE legal_dong_code = :legalDongCode
AND property_type = :propertyType
AND transaction_type = :transactionType
ORDER BY transaction_count DESC, building_key ASC
LIMIT 20
""";
Map<String, Object> params = Map.of(
"legalDongCode", legalDongCode,
"propertyType", propertyType.name(),
"transactionType", transactionType.name()
);
return jdbcTemplate.query(sql, params, buildingPriceStatMapper());
}

private RowMapper<PropertyRow> propertyRowMapper() {
return (rs, rowNum) -> mapPropertyRow(rs);
}

private RowMapper<PropertyTransactionResponse> propertyTransactionMapper() {
return (rs, rowNum) -> new PropertyTransactionResponse(
TransactionType.valueOf(rs.getString("transaction_type")),
formatYearMonth(rs.getString("contract_year_month")),
nullableLong(rs, "deposit"),
nullableLong(rs, "monthly_rent"),
nullableLong(rs, "price"),
rs.getBigDecimal("area_m2"),
nullableInteger(rs, "floor")
);
}

private RowMapper<RegionPriceStatResponse> regionPriceStatMapper() {
return (rs, rowNum) -> new RegionPriceStatResponse(
rs.getString("region_level"),
rs.getString("region_code"),
rs.getString("sido"),
rs.getString("sigungu"),
rs.getString("dong"),
nullableLong(rs, "avg_deposit"),
nullableLong(rs, "median_deposit"),
nullableLong(rs, "avg_monthly_rent"),
nullableLong(rs, "median_monthly_rent"),
nullableLong(rs, "avg_price"),
nullableLong(rs, "median_price"),
nullableInteger(rs, "transaction_count"),
formatYearMonth(rs.getString("sample_from_ym")),
formatYearMonth(rs.getString("sample_to_ym"))
);
}

private RowMapper<BuildingPriceStatResponse> buildingPriceStatMapper() {
return (rs, rowNum) -> new BuildingPriceStatResponse(
rs.getString("building_key"),
rs.getString("building_name"),
rs.getString("sido"),
rs.getString("sigungu"),
rs.getString("dong"),
nullableLong(rs, "avg_deposit"),
nullableLong(rs, "median_deposit"),
nullableLong(rs, "avg_monthly_rent"),
nullableLong(rs, "median_monthly_rent"),
nullableLong(rs, "avg_price"),
nullableLong(rs, "median_price"),
nullableInteger(rs, "transaction_count"),
formatYearMonth(rs.getString("sample_from_ym")),
formatYearMonth(rs.getString("sample_to_ym"))
);
}

private PropertyRow mapPropertyRow(ResultSet rs) throws SQLException {
return new PropertyRow(
rs.getLong("id"),
Expand Down Expand Up @@ -127,4 +288,11 @@ private Integer nullableInteger(ResultSet rs, String column) throws SQLException
int value = rs.getInt(column);
return rs.wasNull() ? null : value;
}

private String formatYearMonth(String yearMonth) {
if (yearMonth == null || yearMonth.length() != 6) {
return yearMonth;
}
return yearMonth.substring(0, 4) + "-" + yearMonth.substring(4);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@
import java.util.List;
import java.util.Optional;

import com.ssafy.salmanhae.model.dto.property.BuildingPriceStatResponse;
import com.ssafy.salmanhae.model.dto.property.PropertySafetySummaryResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyTransactionResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.RegionPriceStatResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyRow;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.TransactionType;

public interface PropertyDao {

List<PropertyRow> findInBounds(PropertySearchCriteria criteria);

Optional<PropertyRow> findActiveById(Long propertyId);

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

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

List<RegionPriceStatResponse> findRegionPriceStats(
String legalDongCode,
PropertyType propertyType,
TransactionType transactionType
);

List<BuildingPriceStatResponse> findBuildingPriceStats(
String legalDongCode,
PropertyType propertyType,
TransactionType transactionType
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ssafy.salmanhae.model.dto.property;

public record BuildingPriceStatResponse(
String buildingKey,
String buildingName,
String sido,
String sigungu,
String dong,
Long avgDeposit,
Long medianDeposit,
Long avgMonthlyRent,
Long medianMonthlyRent,
Long avgPrice,
Long medianPrice,
Integer transactionCount,
String sampleFromYm,
String sampleToYm
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ssafy.salmanhae.model.dto.property;

import java.util.List;

public record PriceAnalysisResponse(
String legalDongCode,
PropertyType propertyType,
TransactionType transactionType,
List<RegionPriceStatResponse> regionStats,
List<BuildingPriceStatResponse> buildingStats
) {
public PriceAnalysisResponse {
regionStats = regionStats == null ? List.of() : List.copyOf(regionStats);
buildingStats = buildingStats == null ? List.of() : List.copyOf(buildingStats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ssafy.salmanhae.model.dto.property;

public record PropertySafetySummaryResponse(
Long propertyId,
Integer radius,
Integer safetyScore,
Integer priceScore,
Integer cctvCount300m,
Integer bellCount300m,
Integer lightCount300m,
Integer policeCount500m
) {
}
Loading