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
6 changes: 3 additions & 3 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"type": "command",
"command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/codex_hook_bridge.py\" pre-bash",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$root = git rev-parse --show-toplevel; python (Join-Path $root '.codex\\hooks\\codex_hook_bridge.py') pre-bash\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"`$root = git rev-parse --show-toplevel; python (Join-Path `$root '.codex\\hooks\\codex_hook_bridge.py') pre-bash\"",
"statusMessage": "Checking command safety"
}
]
Expand All @@ -20,7 +20,7 @@
{
"type": "command",
"command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/codex_hook_bridge.py\" post-edit",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$root = git rev-parse --show-toplevel; python (Join-Path $root '.codex\\hooks\\codex_hook_bridge.py') post-edit\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"`$root = git rev-parse --show-toplevel; python (Join-Path `$root '.codex\\hooks\\codex_hook_bridge.py') post-edit\"",
"statusMessage": "Checking test coverage"
}
]
Expand All @@ -31,7 +31,7 @@
{
"type": "command",
"command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/codex_hook_bridge.py\" post-bash",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$root = git rev-parse --show-toplevel; python (Join-Path $root '.codex\\hooks\\codex_hook_bridge.py') post-bash\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"`$root = git rev-parse --show-toplevel; python (Join-Path `$root '.codex\\hooks\\codex_hook_bridge.py') post-bash\"",
"statusMessage": "Checking repeated failures"
}
]
Expand Down
22 changes: 22 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,34 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ssafy.salmanhae.common.exception;

public class ApiException extends RuntimeException {

private final ErrorCode errorCode;

public ApiException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.ssafy.salmanhae.common.exception;

import org.springframework.http.HttpStatus;

public enum ErrorCode {
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "요청 파라미터가 올바르지 않습니다."),
INVALID_BOUNDS(HttpStatus.BAD_REQUEST, "지도 범위 파라미터가 올바르지 않습니다."),
PROPERTY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 매물을 찾을 수 없습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.");

private final HttpStatus status;
private final String message;

ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

public HttpStatus getStatus() {
return status;
}

public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ssafy.salmanhae.common.exception;

public record ErrorResponse(
String code,
String message,
int status
) {

public static ErrorResponse from(ErrorCode errorCode) {
return new ErrorResponse(
errorCode.name(),
errorCode.getMessage(),
errorCode.getStatus().value()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ssafy.salmanhae.common.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ApiException.class)
public ResponseEntity<ErrorResponse> handleApiException(ApiException exception) {
ErrorCode errorCode = exception.getErrorCode();
return ResponseEntity
.status(errorCode.getStatus())
.body(ErrorResponse.from(errorCode));
}

@ExceptionHandler({
MissingServletRequestParameterException.class,
MethodArgumentTypeMismatchException.class
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
public ResponseEntity<ErrorResponse> handleBadRequest(Exception exception) {
return ResponseEntity
.status(ErrorCode.INVALID_REQUEST.getStatus())
.body(ErrorResponse.from(ErrorCode.INVALID_REQUEST));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ssafy.salmanhae.common.response;

public record ApiResponse<T>(
T data,
String message
) {

public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(data, "OK");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ssafy.salmanhae.common.response;

import java.util.List;

public record ListResponse<T>(
List<T> items,
int totalCount
) {

public static <T> ListResponse<T> from(List<T> items) {
List<T> snapshot = List.copyOf(items);
return new ListResponse<>(snapshot, snapshot.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.ssafy.salmanhae.controller.property;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.PropertySummaryResponse;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.TransactionType;
import com.ssafy.salmanhae.service.property.PropertyService;

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

private final PropertyService propertyService;

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

@GetMapping
public ApiResponse<ListResponse<PropertySummaryResponse>> searchProperties(
@RequestParam BigDecimal west,
@RequestParam BigDecimal east,
@RequestParam BigDecimal south,
@RequestParam BigDecimal north,
@RequestParam(required = false) TransactionType transactionType,
@RequestParam(required = false) PropertyType propertyType,
@RequestParam(required = false) Long minDeposit,
@RequestParam(required = false) Long maxDeposit,
@RequestParam(required = false) Long minPrice,
@RequestParam(required = false) Long maxPrice
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) {
PropertySearchCriteria criteria = new PropertySearchCriteria(
west,
east,
south,
north,
transactionType,
propertyType,
minDeposit,
maxDeposit,
minPrice,
maxPrice
);
criteria.validateBounds();
List<PropertySummaryResponse> properties = propertyService.searchProperties(criteria);
return ApiResponse.ok(ListResponse.from(properties));
}

@GetMapping("/{propertyId}")
public ApiResponse<PropertyDetailResponse> getProperty(@PathVariable Long propertyId) {
return ApiResponse.ok(propertyService.getProperty(propertyId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.ssafy.salmanhae.model.dao.property;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
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 com.ssafy.salmanhae.model.dto.property.PropertyRow;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;
import com.ssafy.salmanhae.model.dto.property.PropertyType;
import com.ssafy.salmanhae.model.dto.property.TransactionType;

@Repository
public class JdbcPropertyDao implements PropertyDao {

private static final String PROPERTY_COLUMNS = """
id, title, building_name, building_key, address, road_address,
legal_dong_code, property_type, transaction_type, deposit, monthly_rent,
price, maintenance_fee, area_m2, floor, total_floor, latitude, longitude,
description
""";

private final NamedParameterJdbcTemplate jdbcTemplate;

public JdbcPropertyDao(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@Override
public List<PropertyRow> findInBounds(PropertySearchCriteria criteria) {
Map<String, Object> params = new HashMap<>();
params.put("west", criteria.west());
params.put("east", criteria.east());
params.put("south", criteria.south());
params.put("north", criteria.north());

StringBuilder sql = new StringBuilder("""
SELECT %s
FROM properties
WHERE is_active = true
AND longitude BETWEEN :west AND :east
AND latitude BETWEEN :south AND :north
""".formatted(PROPERTY_COLUMNS));

if (criteria.transactionType() != null) {
sql.append(" AND transaction_type = :transactionType");
params.put("transactionType", criteria.transactionType().name());
}
if (criteria.propertyType() != null) {
sql.append(" AND property_type = :propertyType");
params.put("propertyType", criteria.propertyType().name());
}
if (criteria.minDeposit() != null) {
sql.append(" AND deposit >= :minDeposit");
params.put("minDeposit", criteria.minDeposit());
}
if (criteria.maxDeposit() != null) {
sql.append(" AND deposit <= :maxDeposit");
params.put("maxDeposit", criteria.maxDeposit());
}
if (criteria.minPrice() != null) {
sql.append(" AND price >= :minPrice");
params.put("minPrice", criteria.minPrice());
}
if (criteria.maxPrice() != null) {
sql.append(" AND price <= :maxPrice");
params.put("maxPrice", criteria.maxPrice());
}

sql.append(" ORDER BY id ASC");
return jdbcTemplate.query(sql.toString(), params, propertyRowMapper());
}

@Override
public Optional<PropertyRow> findActiveById(Long propertyId) {
String sql = """
SELECT %s
FROM properties
WHERE id = :id
AND is_active = true
""".formatted(PROPERTY_COLUMNS);
Map<String, Object> params = Map.of("id", propertyId);
List<PropertyRow> rows = jdbcTemplate.query(sql, params, propertyRowMapper());
return rows.stream().findFirst();
}

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

private PropertyRow mapPropertyRow(ResultSet rs) throws SQLException {
return new PropertyRow(
rs.getLong("id"),
rs.getString("title"),
rs.getString("building_name"),
rs.getString("building_key"),
rs.getString("address"),
rs.getString("road_address"),
rs.getString("legal_dong_code"),
PropertyType.valueOf(rs.getString("property_type")),
TransactionType.valueOf(rs.getString("transaction_type")),
nullableLong(rs, "deposit"),
nullableLong(rs, "monthly_rent"),
nullableLong(rs, "price"),
nullableLong(rs, "maintenance_fee"),
rs.getBigDecimal("area_m2"),
nullableInteger(rs, "floor"),
nullableInteger(rs, "total_floor"),
rs.getBigDecimal("latitude"),
rs.getBigDecimal("longitude"),
rs.getString("description")
);
}

private Long nullableLong(ResultSet rs, String column) throws SQLException {
long value = rs.getLong(column);
return rs.wasNull() ? null : value;
}

private Integer nullableInteger(ResultSet rs, String column) throws SQLException {
int value = rs.getInt(column);
return rs.wasNull() ? null : value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ssafy.salmanhae.model.dao.property;

import java.util.List;
import java.util.Optional;

import com.ssafy.salmanhae.model.dto.property.PropertyRow;
import com.ssafy.salmanhae.model.dto.property.PropertySearchCriteria;

public interface PropertyDao {

List<PropertyRow> findInBounds(PropertySearchCriteria criteria);

Optional<PropertyRow> findActiveById(Long propertyId);
}
Loading