Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -28,7 +28,20 @@ public record ComplexFilterResponse(
@Builder
@Schema(name = "지역 필터", description = "단지가 속한 지역(구) 목록")
public record DistrictFilter(
@Schema(description = "고유한 지역(구) 목록", example = "[\"성남시 분당구\", \"서울시 강남구\", \"부산시 해운대구\"]")
@Schema(description = "고유한 지역 목록")
List<District> districts
) {}

/**
* 지역 정보 (city별로 그룹화된 districts)
*/
@Builder
@Schema(name = "지역 정보", description = "city별로 그룹화된 district 목록")
public record District(
@Schema(description = "시/도", example = "경기")
String city,

@Schema(description = "구/시 목록", example = "[\"동두천시\", \"양주시\"]")
List<String> districts
) {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.pinHouse.server.platform.housing.notice.application.service;

import com.pinHouse.server.platform.Location;
import com.pinHouse.server.platform.housing.complex.domain.entity.ComplexDocument;
import com.pinHouse.server.platform.housing.complex.domain.entity.Deposit;
import com.pinHouse.server.platform.housing.complex.domain.entity.UnitType;
import com.pinHouse.server.platform.housing.facility.application.dto.NoticeFacilityListResponse;
import com.pinHouse.server.platform.housing.facility.domain.entity.FacilityType;
import com.pinHouse.server.platform.housing.notice.application.dto.ComplexFilterResponse;
import com.pinHouse.server.platform.housing.notice.application.dto.NoticeDetailFilterRequest;
import com.pinHouse.server.platform.pinPoint.domain.entity.PinPoint;
import com.pinHouse.server.platform.pinPoint.domain.repository.PinPointMongoRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -26,8 +23,6 @@
@RequiredArgsConstructor
public class ComplexFilterService {

private final PinPointMongoRepository pinPointRepository;

/**
* 필터 조건에 따라 단지를 filtered와 nonFiltered로 분리
*/
Expand Down Expand Up @@ -186,81 +181,198 @@ private boolean passesUnitTypeFilters(UnitType unit, NoticeDetailFilterRequest r
}

/**
* PinPoint ID로부터 사용자 위치 정보 조회
* 단지 목록으로부터 필터 정보 계산
*/
public ComplexFilterResponse buildFilterResponse(List<ComplexDocument> complexes) {
ComplexFilterResponse.DistrictFilter districtFilter = calculateDistrictFilter(complexes);
ComplexFilterResponse.CostFilter costFilter = calculateCostFilter(complexes);
ComplexFilterResponse.AreaFilter areaFilter = calculateAreaFilter(complexes);

return ComplexFilterResponse.builder()
.districtFilter(districtFilter)
.costFilter(costFilter)
.areaFilter(areaFilter)
.build();
}

/**
* 지역 필터 계산
*/
public ComplexFilterResponse.DistrictFilter calculateDistrictFilter(List<ComplexDocument> complexes) {
// 1. 각 complex에서 city와 district 추출
List<TempDistrictInfo> tempDistricts = complexes.stream()
.map(this::parseAddress)
.filter(Objects::nonNull)
.toList();

// 2. city별로 그룹화하여 district 목록 생성
Map<String, List<String>> cityToDistricts = tempDistricts.stream()
.collect(Collectors.groupingBy(
TempDistrictInfo::city,
Collectors.mapping(
TempDistrictInfo::district,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream().distinct().sorted().toList()
)
)
));

// 3. 최종 District 리스트 생성
List<ComplexFilterResponse.District> groupedDistricts = cityToDistricts.entrySet().stream()
.map(entry -> ComplexFilterResponse.District.builder()
.city(entry.getKey())
.districts(entry.getValue())
.build())
.sorted(Comparator.comparing(ComplexFilterResponse.District::city))
.toList();

return ComplexFilterResponse.DistrictFilter.builder()
.districts(groupedDistricts)
.build();
}

/**
* 임시 District 정보 (그룹화 전)
*/
private Location getUserLocation(String pinPointId) {
if (pinPointId == null || pinPointId.isBlank()) {
private record TempDistrictInfo(String city, String district) {}

/**
* ComplexDocument의 주소 정보를 파싱하여 TempDistrictInfo로 변환
*
* 변환 규칙:
* 1. 광역시/특별시: "부산시 해운대구" → city: "부산", district: "해운대구"
* 2. 일반시 (구 있음): city: "경기도", county: "청주시 서원구" → city: "경기", district: "청주시 서원구"
* 3. 일반시 (구 없음): city: "경기도", county: "동두천시" → city: "경기", district: "동두천시"
*/
private TempDistrictInfo parseAddress(ComplexDocument complex) {
String county = complex.getCounty();
String city = complex.getCity();

if (county == null || county.isBlank()) {
return null;
}

try {
PinPoint pinPoint = pinPointRepository.findById(pinPointId)
.orElse(null);
// 광역시 및 특별시 목록
final Set<String> METRO_CITIES = Set.of(
"서울", "부산", "대구", "인천", "광주", "대전", "울산", "세종"
);

if (pinPoint != null) {
return pinPoint.getLocation();
// county를 공백으로 분리
String[] countyParts = county.trim().split("\\s+");

String finalCity;
String finalDistrict;

// 광역시/특별시 여부 확인
boolean isMetroCity = false;
String metroCityName = null;

if (countyParts.length >= 1) {
String cityName = countyParts[0];
// "광주광역시", "부산광역시", "서울특별시" 등에서 도시 이름 추출
String cityBase = extractMetroCityName(cityName);
if (cityBase != null && METRO_CITIES.contains(cityBase)) {
isMetroCity = true;
metroCityName = cityBase;
}
} catch (Exception e) {
log.error("Failed to fetch PinPoint: {}", pinPointId, e);
}

return null;
if (isMetroCity) {
// 광역시/특별시인 경우
finalCity = metroCityName;

if (countyParts.length >= 2) {
// "부산시 해운대구" 또는 "광주광역시 서구" → city: "부산", district: "해운대구"
finalDistrict = countyParts[1];
} else {
// "대구광역시"만 있는 경우 → city: "대구", district: "대구광역시" (원본 유지)
finalDistrict = county;
}
} else {
// 일반시인 경우
// city 필드가 광역시일 수도 있으므로 확인
String cityBase = extractMetroCityName(city);
if (cityBase != null && METRO_CITIES.contains(cityBase)) {
// city 필드가 "대구광역시"인 경우 → city: "대구"
finalCity = cityBase;
} else {
// city 필드가 도인 경우 → "경기도" → "경기"
finalCity = shortenProvinceName(city);
}
finalDistrict = county;
}

return new TempDistrictInfo(finalCity, finalDistrict);
}
Comment on lines +248 to 307
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find test files related to ComplexFilterService
fd -e java -path "*test*" | xargs rg -l "ComplexFilterService" | head -20

Repository: PinHouse/PinHouse_BE

Length of output: 450


🏁 Script executed:

# Check for parseAddress method and its test cases
rg -nA 20 "void test.*[Pp]arse[Aa]ddress|@Test.*\n.*parseAddress" --type=java

Repository: PinHouse/PinHouse_BE

Length of output: 255


🏁 Script executed:

# Find the helper methods: extractMetroCityName and shortenProvinceName
rg -nA 10 "extractMetroCityName|shortenProvinceName" --type=java src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java

Repository: PinHouse/PinHouse_BE

Length of output: 1870


🏁 Script executed:

# Look for sample address data or test data patterns
rg -nP "county|city" --type=java src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | grep -E "(county|city)\s*=" | head -30

Repository: PinHouse/PinHouse_BE

Length of output: 211


🏁 Script executed:

# Check how ComplexDocument is populated and what data it receives
fd -e java -type f | xargs rg -l "ComplexDocument" | head -10

Repository: PinHouse/PinHouse_BE

Length of output: 1244


🏁 Script executed:

# Search for test files more thoroughly
find . -type f -name "*Test.java" -o -name "*test*.java" 2>/dev/null | xargs rg -l "ComplexFilterService\|parseAddress" 2>/dev/null

Repository: PinHouse/PinHouse_BE

Length of output: 48


🏁 Script executed:

# Get the complete implementation of helper methods
cat -n src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | sed -n '309,380p'

Repository: PinHouse/PinHouse_BE

Length of output: 2606


🏁 Script executed:

# Check where ComplexDocument data comes from
rg -n "setCounty|setCity|county|city" --type java src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | head -20

Repository: PinHouse/PinHouse_BE

Length of output: 1296


🏁 Script executed:

# Look for test resources or sample data files
find . -type f \( -name "*.json" -o -name "*.xml" -o -name "*.properties" \) 2>/dev/null | xargs rg -l "county\|city\|광역시\|특별시" 2>/dev/null | head -10

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Check the ComplexDocument class definition to understand county/city fields
rg -A 10 "class ComplexDocument|private.*county|private.*city" --type java src/main/java/com/pinHouse/server/platform/housing/notice/domain/model/ComplexDocument.java 2>/dev/null || find . -name "ComplexDocument.java" -type f | xargs cat -n | head -80

Repository: PinHouse/PinHouse_BE

Length of output: 2646


🏁 Script executed:

# Search for test cases related to parseAddress
find . -type f -name "*.java" | xargs grep -l "parseAddress" 2>/dev/null

Repository: PinHouse/PinHouse_BE

Length of output: 169


🏁 Script executed:

# Look for ComplexFilterService tests specifically
find . -type f -name "*ComplexFilterServiceTest*" -o -name "*Test.java" | xargs grep -l "parseAddress\|ComplexFilterService" 2>/dev/null | head -5

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Check how city field can be null or what values it gets
rg -B5 -A5 "getCity\(\)|getCounty\(\)" src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java

Repository: PinHouse/PinHouse_BE

Length of output: 1111


🏁 Script executed:

# Check for 제주 (Jeju) handling - special autonomous province
rg -n "제주\|세종\|특별자치" src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Look at the test directories to understand project structure
find . -type d -name "test" -o -type d -name "tests" 2>/dev/null | head -5

Repository: PinHouse/PinHouse_BE

Length of output: 74


🏁 Script executed:

# Look for test files in the test directory
find ./src/test -type f -name "*.java" | xargs grep -l "ComplexFilterService\|parseAddress\|TempDistrictInfo" 2>/dev/null

Repository: PinHouse/PinHouse_BE

Length of output: 48


🏁 Script executed:

# Check the entire test directory structure
find ./src/test -type f -name "*.java" | head -20

Repository: PinHouse/PinHouse_BE

Length of output: 345


🏁 Script executed:

# Look for any integration or unit tests that might test address parsing
find ./src/test -type f -name "*.java" | xargs grep -l "address\|county\|city" 2>/dev/null | head -10

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Check the full parseAddress method one more time and review the logic for edge cases
cat -n src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | sed -n '248,307p'

Repository: PinHouse/PinHouse_BE

Length of output: 2457


🏁 Script executed:

# Check if 제주 (Jeju) is handled in the helper methods
cat -n src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | sed -n '316,370p'

Repository: PinHouse/PinHouse_BE

Length of output: 1982


🏁 Script executed:

# Check if there's any documentation about expected city/county formats
find . -type f \( -name "README*" -o -name "*.md" \) | xargs grep -i "city\|county\|address" 2>/dev/null | head -20

Repository: PinHouse/PinHouse_BE

Length of output: 98


🏁 Script executed:

# Check the Address class to understand the data structure better
find . -type f -name "Address.java" | xargs cat -n 2>/dev/null | head -50

Repository: PinHouse/PinHouse_BE

Length of output: 773


🏁 Script executed:

# Verify how parseAddress is called and what happens with null return
rg -B3 -A5 "parseAddress\(" src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java

Repository: PinHouse/PinHouse_BE

Length of output: 470


🏁 Script executed:

# Check Korean administrative divisions to verify if 제주특별자치도 is handled
cat -n src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | sed -n '350,370p'

Repository: PinHouse/PinHouse_BE

Length of output: 921


🏁 Script executed:

# Check if there are any data files or queries that show what city/county values actually look like
find . -type f \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) | xargs grep -i "특별자치\|제주\|세종" 2>/dev/null | head -20

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Verify the exact behavior when shortenProvinceName receives "제주특별자치도"
cat > /tmp/test_jeju.java << 'EOF'
public class TestJeju {
    public static void main(String[] args) {
        String jeju = "제주특별자치도";
        if (jeju.endsWith("도")) {
            String base = jeju.substring(0, jeju.length() - 1);
            System.out.println("Base after removing '도': [" + base + "]");
            System.out.println("Expected: [제주]");
            System.out.println("Actual result: [" + base + "]");
        }
    }
}
EOF
javac /tmp/test_jeju.java 2>&1 || echo "Java not available"

Repository: PinHouse/PinHouse_BE

Length of output: 127


🏁 Script executed:

# Check if there's any special handling needed for Jeju in the codebase
rg -n "제주\|Jeju\|jeju" src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java

Repository: PinHouse/PinHouse_BE

Length of output: 46


🏁 Script executed:

# Verify what happens with null city field in the parseAddress method more carefully
cat -n src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java | sed -n '292,304p'

Repository: PinHouse/PinHouse_BE

Length of output: 639


shortenProvinceName 메서드에서 제주특별자치도 처리 누락

shortenProvinceName은 "도" 문자만 제거하기 때문에, city = "제주특별자치도"인 경우 "제주특별자치"를 반환하게 됩니다 (올바른 값: "제주"). extractMetroCityName에서 "특별자치시"를 처리하는 것과 유사하게, shortenProvinceName도 "특별자치도" 접미사를 처리해야 합니다.

🤖 Prompt for AI Agents
In
src/main/java/com/pinHouse/server/platform/housing/notice/application/service/ComplexFilterService.java
around lines 248 to 307, the shortenProvinceName method currently only strips a
trailing "도" so "제주특별자치도" becomes "제주특별자치" — update shortenProvinceName to
detect and strip the full "특별자치도" suffix (returning "제주") as well as existing
cases (e.g., remove "도", "도시" variants if needed), preserving behavior for other
inputs; implement the check/order so longer suffixes are removed first (e.g., if
endsWith("특별자치도") remove that, else if endsWith("도") remove that) and ensure
null/blank safety.


/**
* Haversine formula를 사용한 두 지점 간 거리 계산 (km)
* 광역시/특별시 이름 추출
* "광주광역시" → "광주"
* "부산광역시" → "부산"
* "서울특별시" → "서울"
* "부산시" → "부산"
*/
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
final int EARTH_RADIUS_KM = 6371;
private String extractMetroCityName(String cityName) {
if (cityName == null || cityName.isBlank()) {
return null;
}

// "광역시" 제거
if (cityName.endsWith("광역시")) {
return cityName.substring(0, cityName.length() - 3);
}

double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
// "특별시" 제거
if (cityName.endsWith("특별시")) {
return cityName.substring(0, cityName.length() - 3);
}

double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
// "특별자치시" 제거 (세종)
if (cityName.endsWith("특별자치시")) {
return cityName.substring(0, cityName.length() - 5);
}

double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// "시" 제거
if (cityName.endsWith("시")) {
return cityName.substring(0, cityName.length() - 1);
}

return EARTH_RADIUS_KM * c;
return cityName;
}

/**
* 단지 목록으로부터 필터 정보 계산
* 도 이름 축약
* "경기도" → "경기"
* "충청북도" → "충북"
* "경상남도" → "경남"
*/
public ComplexFilterResponse buildFilterResponse(List<ComplexDocument> complexes) {
ComplexFilterResponse.DistrictFilter districtFilter = calculateDistrictFilter(complexes);
ComplexFilterResponse.CostFilter costFilter = calculateCostFilter(complexes);
ComplexFilterResponse.AreaFilter areaFilter = calculateAreaFilter(complexes);
private String shortenProvinceName(String province) {
if (province == null || province.isBlank()) {
return "";
}

return ComplexFilterResponse.builder()
.districtFilter(districtFilter)
.costFilter(costFilter)
.areaFilter(areaFilter)
.build();
}
// "도" 제거
if (province.endsWith("도")) {
String base = province.substring(0, province.length() - 1);

/**
* 지역 필터 계산
*/
private ComplexFilterResponse.DistrictFilter calculateDistrictFilter(List<ComplexDocument> complexes) {
List<String> uniqueDistricts = complexes.stream()
.map(ComplexDocument::getCounty)
.filter(Objects::nonNull)
.filter(county -> !county.isBlank())
.distinct()
.sorted()
.toList();
// "충청북", "충청남", "경상북", "경상남", "전라북", "전라남" → 첫글자 + 마지막글자
if (base.startsWith("충청") || base.startsWith("경상") || base.startsWith("전라")) {
// "충청북" → "충북", "경상남" → "경남"
return base.charAt(0) + String.valueOf(base.charAt(base.length() - 1));
}

return ComplexFilterResponse.DistrictFilter.builder()
.districts(uniqueDistricts)
.build();
// 그 외 도는 "도"만 제거 ("경기도" → "경기", "강원도" → "강원")
return base;
}

return province;
}

/**
* 가격 필터 계산
*/
private ComplexFilterResponse.CostFilter calculateCostFilter(List<ComplexDocument> complexes) {
public ComplexFilterResponse.CostFilter calculateCostFilter(List<ComplexDocument> complexes) {
// 모든 unitType의 보증금(deposit.total) 수집
List<Long> allPrices = complexes.stream()
.flatMap(complex -> complex.getUnitTypes().stream())
Expand Down Expand Up @@ -348,7 +460,7 @@ private List<ComplexFilterResponse.PriceDistribution> calculatePriceDistribution
/**
* 면적(타입코드) 필터 계산
*/
private ComplexFilterResponse.AreaFilter calculateAreaFilter(List<ComplexDocument> complexes) {
public ComplexFilterResponse.AreaFilter calculateAreaFilter(List<ComplexDocument> complexes) {
List<String> uniqueTypeCodes = complexes.stream()
.flatMap(complex -> complex.getUnitTypes().stream())
.map(UnitType::getTypeCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,51 @@ public ComplexFilterResponse getComplexFilters(String noticeId) {
return complexFilterService.buildFilterResponse(complexes);
}

/// 공고의 단지 지역 필터 정보 조회
@Override
@Transactional(readOnly = true)
public ComplexFilterResponse.DistrictFilter getDistrictFilter(String noticeId) {

/// 공고 존재 확인
loadNotice(noticeId);

/// 단지 목록 조회
List<ComplexDocument> complexes = complexService.loadComplexes(noticeId);

/// 지역 필터 계산
return complexFilterService.calculateDistrictFilter(complexes);
}

/// 공고의 단지 비용 필터 정보 조회
@Override
@Transactional(readOnly = true)
public ComplexFilterResponse.CostFilter getCostFilter(String noticeId) {

/// 공고 존재 확인
loadNotice(noticeId);

/// 단지 목록 조회
List<ComplexDocument> complexes = complexService.loadComplexes(noticeId);

/// 비용 필터 계산
return complexFilterService.calculateCostFilter(complexes);
}

/// 공고의 단지 방타입 필터 정보 조회
@Override
@Transactional(readOnly = true)
public ComplexFilterResponse.AreaFilter getAreaFilter(String noticeId) {

/// 공고 존재 확인
loadNotice(noticeId);

/// 단지 목록 조회
List<ComplexDocument> complexes = complexService.loadComplexes(noticeId);

/// 방타입 필터 계산
return complexFilterService.calculateAreaFilter(complexes);
}

/// 공고의 필터 조건에 맞는 단지 개수 조회
@Override
@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public interface NoticeUseCase {
/// 공고의 단지 필터링 정보 조회 (지역, 가격, 면적)
ComplexFilterResponse getComplexFilters(String noticeId);

/// 공고의 단지 지역 필터 정보 조회
ComplexFilterResponse.DistrictFilter getDistrictFilter(String noticeId);

/// 공고의 단지 비용 필터 정보 조회
ComplexFilterResponse.CostFilter getCostFilter(String noticeId);

/// 공고의 단지 방타입 필터 정보 조회
ComplexFilterResponse.AreaFilter getAreaFilter(String noticeId);

/// 공고의 필터 조건에 맞는 단지 개수 조회
int countFilteredComplexes(String noticeId, NoticeDetailFilterRequest request);

Expand Down
Loading
Loading