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 @@ -5,11 +5,13 @@
import com.zipte.platform.core.response.pageable.PageResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstateDetailResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstateListResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstatePriceListResponse;
import com.zipte.platform.server.adapter.in.web.swagger.EstateApiSpec;
import com.zipte.platform.server.application.in.estate.EstatePriceUseCase;
import com.zipte.platform.server.application.in.estate.GetEstateUseCase;
import com.zipte.platform.server.application.in.external.OpenAiUseCase;
import com.zipte.platform.server.domain.estate.Estate;
import jakarta.persistence.EntityNotFoundException;
import com.zipte.platform.server.domain.estate.EstatePrice;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -25,6 +27,9 @@ public class EstateApi implements EstateApiSpec {

private final GetEstateUseCase getService;

/// 가격 의존성
private final EstatePriceUseCase priceService;

/// AI 의존성
private final OpenAiUseCase openAiService;

Expand All @@ -37,13 +42,11 @@ public ApiResponse<EstateDetailResponse> getEstate(
Estate estate;

if (code != null) {
estate = getService.loadEstateByCode(code)
.orElseThrow(() -> new EntityNotFoundException("해당하는 아파트가 존재하지 않습니다."));
estate = getService.loadEstateByCode(code);
} else if (name != null) {
estate = getService.loadEstateByName(name)
.orElseThrow(() -> new EntityNotFoundException("해당하는 아파트가 존재하지 않습니다."));
estate = getService.loadEstateByName(name);
} else {
throw new IllegalArgumentException("최소 하나 이상의 요청 파라미터가 필요합니다.");
throw new IllegalArgumentException();
}

return ApiResponse.created(EstateDetailResponse.from(estate));
Expand Down Expand Up @@ -74,9 +77,8 @@ public ApiResponse<List<EstateListResponse>> getEstateByLocation(
@RequestParam(value = "longitude") double longitude,
@RequestParam(value = "latitude") double latitude,
@RequestParam(value = "radius") double radius) {
List<Estate> list = getService.loadEstatesNearBy(longitude, latitude, radius);

return ApiResponse.ok(EstateListResponse.from(list));
return ApiResponse.ok(getService.loadEstatesNearBy(longitude, latitude, radius));

}

Expand All @@ -93,6 +95,16 @@ public ApiResponse<List<EstateListResponse>> getEstateByLocationByKaptCode(

}

@GetMapping("/compare")
public ApiResponse<List<EstateDetailResponse>> getEstateByCompare(
@RequestParam(value = "first") String first,
@RequestParam(value = "second") String second
) {
List<Estate> estates = getService.loadEstatesByCompare(List.of(first, second));

return ApiResponse.ok(EstateDetailResponse.from(estates));
}
Comment on lines +98 to +106
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

아파트 비교 기능 구현

비교 기능이 적절히 구현되었습니다. 입력 파라미터 검증을 추가하는 것을 고려해보세요.

@GetMapping("/compare")
public ApiResponse<List<EstateDetailResponse>> getEstateByCompare(
        @RequestParam(value = "first") String first,
        @RequestParam(value = "second") String second
) {
+    if (first == null || first.trim().isEmpty()) {
+        throw new IllegalArgumentException("첫 번째 아파트 코드는 필수입니다.");
+    }
+    if (second == null || second.trim().isEmpty()) {
+        throw new IllegalArgumentException("두 번째 아파트 코드는 필수입니다.");
+    }
+    if (first.equals(second)) {
+        throw new IllegalArgumentException("비교할 아파트는 서로 달라야 합니다.");
+    }
+    
    List<Estate> estates = getService.loadEstatesByCompare(List.of(first, second));

    return ApiResponse.ok(EstateDetailResponse.from(estates));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping("/compare")
public ApiResponse<List<EstateDetailResponse>> getEstateByCompare(
@RequestParam(value = "first") String first,
@RequestParam(value = "second") String second
) {
List<Estate> estates = getService.loadEstatesByCompare(List.of(first, second));
return ApiResponse.ok(EstateDetailResponse.from(estates));
}
@GetMapping("/compare")
public ApiResponse<List<EstateDetailResponse>> getEstateByCompare(
@RequestParam(value = "first") String first,
@RequestParam(value = "second") String second
) {
if (first == null || first.trim().isEmpty()) {
throw new IllegalArgumentException("첫 번째 아파트 코드는 필수입니다.");
}
if (second == null || second.trim().isEmpty()) {
throw new IllegalArgumentException("두 번째 아파트 코드는 필수입니다.");
}
if (first.equals(second)) {
throw new IllegalArgumentException("비교할 아파트는 서로 달라야 합니다.");
}
List<Estate> estates = getService.loadEstatesByCompare(List.of(first, second));
return ApiResponse.ok(EstateDetailResponse.from(estates));
}
🤖 Prompt for AI Agents
In src/main/java/com/zipte/platform/server/adapter/in/web/EstateApi.java around
lines 98 to 106, the getEstateByCompare method lacks validation for the input
parameters 'first' and 'second'. Add checks to ensure these parameters are not
null or empty before proceeding, and handle invalid inputs appropriately, such
as returning a bad request response or throwing a validation exception.



/// AI 기반 특징 요약
@GetMapping("/ai/{kaptCode}")
Expand All @@ -101,4 +113,25 @@ public ApiResponse<String> getEstateDetail(@PathVariable String kaptCode) {

return ApiResponse.ok(result);
}


/// 가격 조회
@GetMapping("/price")
public ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
@RequestParam String kaptCode,
@RequestParam double area) {

List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode, area);

return ApiResponse.ok(EstatePriceListResponse.from(list));
}
Comment on lines +119 to +127
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파라미터 검증 로직을 추가하는 것을 고려하세요.

새로운 가격 조회 엔드포인트가 추가되었습니다. 그러나 입력 파라미터에 대한 검증이 부족합니다.

다음과 같은 개선사항을 적용하는 것을 권장합니다:

@GetMapping("/price")
public ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
        @RequestParam String kaptCode,
        @RequestParam double area) {

+    if (kaptCode == null || kaptCode.trim().isEmpty()) {
+        throw new IllegalArgumentException("kaptCode는 필수 파라미터입니다.");
+    }
+    if (area <= 0) {
+        throw new IllegalArgumentException("area는 0보다 큰 값이어야 합니다.");
+    }

    List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode, area);

    return ApiResponse.ok(EstatePriceListResponse.from(list));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping("/price")
public ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
@RequestParam String kaptCode,
@RequestParam double area) {
List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode, area);
return ApiResponse.ok(EstatePriceListResponse.from(list));
}
@GetMapping("/price")
public ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
@RequestParam String kaptCode,
@RequestParam double area) {
if (kaptCode == null || kaptCode.trim().isEmpty()) {
throw new IllegalArgumentException("kaptCode는 필수 파라미터입니다.");
}
if (area <= 0) {
throw new IllegalArgumentException("area는 0보다 큰 값이어야 합니다.");
}
List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode, area);
return ApiResponse.ok(EstatePriceListResponse.from(list));
}
🤖 Prompt for AI Agents
In src/main/java/com/zipte/platform/server/adapter/in/web/EstateApi.java around
lines 112 to 120, the getPriceByCodeAndArea method lacks validation for its
input parameters kaptCode and area. Add validation logic to check that kaptCode
is not null or empty and that area is a positive number. If validation fails,
return an appropriate error response or throw a validation exception to prevent
processing invalid inputs.



@GetMapping("/price/{kaptCode}")
public ApiResponse<List<EstatePriceListResponse>> getPrice(@PathVariable String kaptCode) {

List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode);

return ApiResponse.ok(EstatePriceListResponse.from(list));
}
Comment on lines +130 to +136
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

코드 중복을 제거하고 예외 처리를 개선하세요.

두 번째 가격 조회 엔드포인트에서도 파라미터 검증과 예외 처리가 필요합니다. 또한 두 메서드 간에 유사한 로직이 반복됩니다.

다음과 같이 개선할 수 있습니다:

@GetMapping("/price/{kaptCode}")
public ApiResponse<List<EstatePriceListResponse>> getPrice(@PathVariable String kaptCode) {

+    if (kaptCode == null || kaptCode.trim().isEmpty()) {
+        throw new IllegalArgumentException("kaptCode는 필수 파라미터입니다.");
+    }

-    List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode);
-
-    return ApiResponse.ok(EstatePriceListResponse.from(list));
+    try {
+        List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode);
+        return ApiResponse.ok(EstatePriceListResponse.from(list));
+    } catch (Exception e) {
+        throw new EntityNotFoundException("해당 아파트의 가격 정보를 찾을 수 없습니다.");
+    }
}

또한 두 메서드를 하나로 통합하여 코드 중복을 줄일 수 있습니다:

@GetMapping("/price")
public ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
        @RequestParam String kaptCode,
        @RequestParam(required = false) Double area) {
    
    if (kaptCode == null || kaptCode.trim().isEmpty()) {
        throw new IllegalArgumentException("kaptCode는 필수 파라미터입니다.");
    }
    
    List<EstatePrice> list;
    if (area != null) {
        if (area <= 0) {
            throw new IllegalArgumentException("area는 0보다 큰 값이어야 합니다.");
        }
        list = priceService.getEstatePriceByCode(kaptCode, area);
    } else {
        list = priceService.getEstatePriceByCode(kaptCode);
    }
    
    return ApiResponse.ok(EstatePriceListResponse.from(list));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping("/price/{kaptCode}")
public ApiResponse<List<EstatePriceListResponse>> getPrice(@PathVariable String kaptCode) {
List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode);
return ApiResponse.ok(EstatePriceListResponse.from(list));
}
@GetMapping("/price/{kaptCode}")
public ApiResponse<List<EstatePriceListResponse>> getPrice(@PathVariable String kaptCode) {
if (kaptCode == null || kaptCode.trim().isEmpty()) {
throw new IllegalArgumentException("kaptCode는 필수 파라미터입니다.");
}
try {
List<EstatePrice> list = priceService.getEstatePriceByCode(kaptCode);
return ApiResponse.ok(EstatePriceListResponse.from(list));
} catch (Exception e) {
throw new EntityNotFoundException("해당 아파트의 가격 정보를 찾을 수 없습니다.");
}
}
🤖 Prompt for AI Agents
In src/main/java/com/zipte/platform/server/adapter/in/web/EstateApi.java around
lines 123 to 129, the getPrice method lacks parameter validation and exception
handling, and duplicates logic present in another price retrieval method.
Refactor by adding validation for kaptCode and proper exception handling to
manage invalid inputs or service errors. Then, consolidate this method with the
other similar price retrieval method into a single unified method to eliminate
code duplication and centralize the logic.

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import lombok.Builder;
import lombok.Data;

import java.util.*;

@Data
@Builder
public class EstateDetailResponse {
Expand Down Expand Up @@ -34,4 +36,11 @@ public static EstateDetailResponse from(Estate estate) {
.facility(EstateFacilityResponse.from(estate))
.build();
}

public static List<EstateDetailResponse> from(List<Estate> estates) {
return estates.stream()
.map(EstateDetailResponse::from)
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

import com.fasterxml.jackson.annotation.JsonInclude;
import com.zipte.platform.server.domain.estate.Estate;
import com.zipte.platform.server.domain.estate.EstatePrice;
import lombok.Builder;
import lombok.Data;

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

@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL) // NULL 값인 필드는 JSON 응답에서
@JsonInclude(JsonInclude.Include.NON_NULL) // NULL 값인 필드는 JSON 응답에서 제외
public class EstateListResponse {

// 아파트 기본 정보
Expand All @@ -20,6 +22,9 @@ public class EstateListResponse {
// 매물 개수
private Integer propertyCount;

// 최근 거래 조회
private EstatePriceDetailResponse price;

// 좌표
private EstateLocationResponse location;

Expand All @@ -43,8 +48,21 @@ public static EstateListResponse from(Estate estate, int count) {
.build();
}

/// 아파트 정보와 가격을 동시에 보여주는 로직
public static EstateListResponse from(Estate estate, Optional<EstatePrice> price) {
return EstateListResponse.builder()
.complexCode(estate.getKaptCode())
.complexName(estate.getKaptName())
.address(estate.getKaptAddr())
.price(EstatePriceDetailResponse.from(price))
.location(EstateLocationResponse.from(estate))
.build();
}

public static List<EstateListResponse> from(List<Estate> estates) {
return estates.stream().map(EstateListResponse::from).toList();
return estates.stream()
.map(EstateListResponse::from)
.toList();
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.zipte.platform.server.adapter.in.web.dto.response;

import com.zipte.platform.server.domain.estate.EstatePrice;
import lombok.Builder;

import java.util.Optional;

@Builder
public record EstatePriceDetailResponse(
double exclusiveArea, String price, String transactionDate) {

public static EstatePriceDetailResponse from(Optional<EstatePrice> estatePrice) {
if (estatePrice.isEmpty()) {
return EstatePriceDetailResponse.builder()
.exclusiveArea(0.0)
.price("없음")
.transactionDate("없음")
.build();
}

EstatePrice price = estatePrice.get();
return EstatePriceDetailResponse.builder()
.exclusiveArea(price.getExclusiveArea() != 0.0 ? price.getExclusiveArea() : 0.0)
.price(price.getPrice() != null ? price.getPrice() : "없음")
.transactionDate(price.getTransactionDate() != null ? price.getTransactionDate() : "없음")
.build();
}
Comment on lines +21 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

중복된 null 체크 로직

line 23-25에서 각 필드마다 null 체크를 하고 있는데, 이미 Optional이 존재하는지 확인했으므로 일부 중복된 체크가 있을 수 있습니다. 또한 price.getExclusiveArea() != 0.0 ? price.getExclusiveArea() : 0.0 로직은 불필요합니다.

다음과 같이 간소화할 수 있습니다:

        EstatePrice price = estatePrice.get();
        return EstatePriceDetailResponse.builder()
-                .exclusiveArea(price.getExclusiveArea() != 0.0 ? price.getExclusiveArea() : 0.0)
+                .exclusiveArea(price.getExclusiveArea())
                .price(price.getPrice() != null ? price.getPrice() : "없음")
                .transactionDate(price.getTransactionDate() != null ? price.getTransactionDate() : "없음")
                .build();
🤖 Prompt for AI Agents
In
src/main/java/com/zipte/platform/server/adapter/in/web/dto/response/EstatePriceDetailResponse.java
lines 21 to 27, the code redundantly checks for null values on fields of the
EstatePrice object even though the Optional presence has already been confirmed.
Also, the check for exclusiveArea being not equal to 0.0 is unnecessary since it
returns the same value either way. Simplify the code by removing these redundant
null checks and directly returning the field values or default values where
appropriate without extra conditional checks.


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.zipte.platform.core.response.pageable.PageResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstateDetailResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstateListResponse;
import com.zipte.platform.server.adapter.in.web.dto.response.EstatePriceListResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -45,10 +46,10 @@ ApiResponse<PageResponse<EstateListResponse>> getEstateByRegion(
description = "지정한 경도, 위도, 반경 값으로부터 근처에 위치한 아파트 목록을 반환합니다."
)
ApiResponse<List<EstateListResponse>> getEstateByLocation(
@Parameter(description = "경도", required = true, example = "127.029704677041")
@Parameter(description = "경도", required = true, example = "127.118904")
@RequestParam(value = "longitude") double longitude,

@Parameter(description = "위도", required = true, example = "37.498266842599")
@Parameter(description = "위도", required = true, example = "37.382698")
@RequestParam(value = "latitude") double latitude,

@Parameter(description = "반경 (km 단위)", required = true, example = "1")
Expand All @@ -60,10 +61,10 @@ ApiResponse<List<EstateListResponse>> getEstateByLocation(
description = "지정한 좌표와 반경을 기준으로 근처의 아파트 및 해당 아파트에 등록된 매물 정보를 함께 조회합니다."
)
ApiResponse<List<EstateListResponse>> getEstateByLocationByKaptCode(
@Parameter(description = "경도", required = true, example = "127.029704677041")
@Parameter(description = "경도", required = true, example = "127.118904")
@RequestParam(value = "longitude") double longitude,

@Parameter(description = "위도", required = true, example = "37.498266842599")
@Parameter(description = "위도", required = true, example = "37.382698")
@RequestParam(value = "latitude") double latitude,

@Parameter(description = "반경 (km 단위)", required = true, example = "1.0")
Expand All @@ -77,4 +78,39 @@ ApiResponse<List<EstateListResponse>> getEstateByLocationByKaptCode(
ApiResponse<String> getEstateDetail(
@Parameter(description = "아파트 코드", required = true, example = "A46378823")
@PathVariable String kaptCode);



@Operation(
summary = "아파트 가격 정보 조회",
description = "아파트 코드를 통해 가격 정보를 조회합니다."
)
ApiResponse<List<EstatePriceListResponse>> getPrice(
@Parameter(description = "아파트 코드", required = true, example = "A46392821")
@PathVariable String kaptCode);



@Operation(
summary = "아파트 가격 정보 조회",
description = "아파트 코드와 평수를 통해 가격 정보를 조회합니다."
)
ApiResponse<List<EstatePriceListResponse>> getPriceByCodeAndArea(
@Parameter(description = "아파트 코드", required = true, example = "A46392821")
@RequestParam String kaptCode,

@Parameter(description = "면적", required = true, example = "83.73")
@RequestParam double area);


@Operation(
summary = "아파트 비교 조회",
description = "아파트 코드를 통해 2개의 아파트를 비교 조회합니다."
)
ApiResponse<List<EstateDetailResponse>> getEstateByCompare(
@Parameter(description = "비교할 아파트 코드 (1)", required = true, example = "A46392821")
@RequestParam(value = "first") String first,

@Parameter(description = "비교할 아파트 코드 (2)", required = true, example = "A46378823")
@RequestParam(value = "second") String second);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
import org.springframework.stereotype.Component;

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

@Component
@RequiredArgsConstructor
public class EstatePricePersistenceAdapter implements EstatePricePort {

private final EstatePriceMongoRepository repository;

@Override
public Optional<EstatePrice> loadRecentPriceByKaptCode(String kaptCode) {
return repository.findFirstByKaptCodeOrderByTransactionDateDesc(kaptCode)
.map(EstatePriceDocument::toDomain);
}

@Override
public List<EstatePrice> loadAllEstatePrices(String kaptCode) {
return repository.findAllByKaptCode(kaptCode).stream()
Expand All @@ -24,7 +31,7 @@ public List<EstatePrice> loadAllEstatePrices(String kaptCode) {

@Override
public List<EstatePrice> loadEstatePriceByCodeAndArea(String kaptCode, double exclusiveArea) {
return repository.findALlByKaptCodeAndExclusiveArea(kaptCode, exclusiveArea).stream()
return repository.findAllByKaptCodeAndExclusiveArea(kaptCode, exclusiveArea).stream()
.map(EstatePriceDocument::toDomain)
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ public interface EstateMongoRepository extends MongoRepository<EstateDocument, S
List<EstateDocument> findByLocation(double longitude, double latitude, double radiusInRadians);

boolean existsByKaptCode(String kaptCode);

/// 테스트용
void deleteByKaptCode(String kaptCode);
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

테스트 전용 메서드의 위치를 재검토해주세요.

프로덕션 레포지토리 인터페이스에 테스트 전용 메서드를 추가하는 것은 좋은 관행이 아닙니다. 다음 대안을 고려해보세요:

  1. 테스트 프로파일 전용 구성 클래스 생성
  2. 테스트 패키지에 별도의 테스트 전용 레포지토리 인터페이스 생성
  3. @TestRepository 애노테이션을 사용한 테스트 전용 빈 정의

테스트 전용 레포지토리 인터페이스 생성 예시:

// src/test/java/com/zipte/platform/server/adapter/out/mongo/estate/TestEstateMongoRepository.java
+@TestRepository
+public interface TestEstateMongoRepository extends EstateMongoRepository {
+    void deleteByKaptCode(String kaptCode);
+}

프로덕션 인터페이스에서 제거:

-    /// 테스트용
-    void deleteByKaptCode(String kaptCode);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// 테스트용
void deleteByKaptCode(String kaptCode);
🤖 Prompt for AI Agents
In
src/main/java/com/zipte/platform/server/adapter/out/mongo/estate/EstateMongoRepository.java
around lines 31 to 33, the deleteByKaptCode method is a test-only method
improperly placed in the production repository interface. Remove this method
from the production interface and instead create a separate test-only repository
interface in the test package or define a test-specific bean using
@TestRepository annotation to isolate test code from production code.

}
Loading
Loading