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 @@ -46,6 +46,7 @@ public static class AreaBasedList2 {
private String cat1;
private String cat2;
private Integer contentTypeId;
private String keyword; // 검색어 (장소명 필터링)

@Builder.Default private Integer pageNo = 1;
@Builder.Default private Integer numOfRows = 20;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,25 @@ public Flux<TatsCnctrResponse.TatsCnctrResponseDto> locationRecommendPlaces(
summary = "지역 기반 한적한 장소 추천",
description = """
각 장소의 한적함 점수(cnctrRate)를 포함하여 반환합니다.
- `cnctrRate`는 관광객 혼잡도를 나타내는 지수로, -1은 데이터가 없음을 의미합니다.
- 정렬(arrange) 기본값: O=제목순, Q=수정일순, R=등록일순, S=거리순
- `cat1`, `cat2`를 통해 장소 분류 코드로 필터링할 수 있습니다.

**검색 기능:**
- `keyword`: 장소명으로 검색 (부분 일치, 대소문자 무관)
- 검색어가 없으면 전체 결과 반환

**정렬 옵션:**
- `arrange=A`: AI 추천순 (기본값) - 종합적인 추천 알고리즘 사용
- `arrange=C`: 한적함순 - cnctrRate 높은 순으로 정렬 (AI 정렬 건너뜀)
- `arrange=Q`: 수정일순
- `arrange=R`: 등록일순

**기타 필터:**
- `cnctrRate`: 관광객 혼잡도 지수 (-1은 데이터 없음)
- `cat1`, `cat2`: 장소 분류 코드로 필터링

**예시:**
- `/api/places?areaCode=1&keyword=바다` - "바다" 포함 장소 검색
- `/api/places?areaCode=1&arrange=C` - 한적함순 정렬
- `/api/places?areaCode=1&keyword=카페&arrange=C` - "카페" 검색 + 한적함순
""",
responses = {
@ApiResponse(responseCode = "200", description = "성공",
Expand Down
135 changes: 101 additions & 34 deletions src/main/java/com/comma/soomteum/domain/tour/service/TourService.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,40 +79,73 @@ public Flux<TatsCnctrResponse.TatsCnctrResponseDto> locationPlaces(TourApiReques
public Flux<TatsCnctrResponse.TatsCnctrResponseDto> AreaPlaces(TourApiRequestDto.AreaBasedList2 request) {
return korAreaService.areaBasedList(request)
.flatMapMany(response -> Flux.fromIterable(response.getBody().getItems().getItem()))
// 검색 필터링
.filter(place -> {
if (request.getKeyword() == null || request.getKeyword().isBlank()) {
return true; // 검색어 없으면 모두 통과
}
return place.getTitle().toLowerCase()
.contains(request.getKeyword().toLowerCase());
})
.flatMap(this::addCnctrRateToPlace)
.collectList()
.flatMap(candidateList -> Mono.fromCallable(() -> {
.flatMapMany(candidateList -> {
// 한적함순 정렬
if ("C".equals(request.getArrange())) {
candidateList.sort((a, b) -> {
double rateA = parseCnctrRate(a.getCnctrRate());
double rateB = parseCnctrRate(b.getCnctrRate());
return Double.compare(rateB, rateA); // 내림차순 (높은 값 = 한적함)
});

List<AiRecommendationRequest> aiRequestItems = candidateList.stream()
.map(dto -> new AiRecommendationRequest(
dto.getTitle(),
dto.getContentid(),
dto.getCat1(),
dto.getCat2(),
dto.getFirstimage(),
dto.getDist(),
dto.getCnctrRate()
))
.collect(Collectors.toList());
// AI 정렬 건너뛰고 바로 반환
return Flux.fromIterable(candidateList)
.map(dto -> {
// quietnessLevel 계산 (AI 정렬을 안 거치므로 직접 계산)
double rate = parseCnctrRate(dto.getCnctrRate());
int quietnessLevel = calculateQuietnessLevel(rate);
dto.setQuietnessLevel(quietnessLevel);

return aiServiceAdapter.createRankedRecommendations(aiRequestItems);
}))
.flatMapMany(Flux::fromIterable)
.map(aiResponse -> {
TatsCnctrResponse.TatsCnctrResponseDto finalDto = new TatsCnctrResponse.TatsCnctrResponseDto();
finalDto.setTitle(aiResponse.getTitle());
finalDto.setContentid(aiResponse.getContentid());
finalDto.setCat1(aiResponse.getCat1());
finalDto.setCat2(aiResponse.getCat2());
finalDto.setFirstimage(aiResponse.getFirstimage());
finalDto.setDist(aiResponse.getDist());
finalDto.setCnctrRate(aiResponse.getCnctrRate());
finalDto.setQuietnessLevel(aiResponse.getQuietnessLevel());

// 새로운 필드들 설정
setCatNameAndAreaInfo(finalDto, request.getAreaCode(), request.getSigunguCode());

return finalDto;
// 새로운 필드들 설정
setCatNameAndAreaInfo(dto, request.getAreaCode(), request.getSigunguCode());

return dto;
});
}

// 기존 AI 정렬 로직
return Mono.fromCallable(() -> {
List<AiRecommendationRequest> aiRequestItems = candidateList.stream()
.map(dto -> new AiRecommendationRequest(
dto.getTitle(),
dto.getContentid(),
dto.getCat1(),
dto.getCat2(),
dto.getFirstimage(),
dto.getDist(),
dto.getCnctrRate()
))
.collect(Collectors.toList());

return aiServiceAdapter.createRankedRecommendations(aiRequestItems);
})
.flatMapMany(Flux::fromIterable)
.map(aiResponse -> {
TatsCnctrResponse.TatsCnctrResponseDto finalDto = new TatsCnctrResponse.TatsCnctrResponseDto();
finalDto.setTitle(aiResponse.getTitle());
finalDto.setContentid(aiResponse.getContentid());
finalDto.setCat1(aiResponse.getCat1());
finalDto.setCat2(aiResponse.getCat2());
finalDto.setFirstimage(aiResponse.getFirstimage());
finalDto.setDist(aiResponse.getDist());
finalDto.setCnctrRate(aiResponse.getCnctrRate());
finalDto.setQuietnessLevel(aiResponse.getQuietnessLevel());

// 새로운 필드들 설정
setCatNameAndAreaInfo(finalDto, request.getAreaCode(), request.getSigunguCode());

return finalDto;
});
});
}

Expand Down Expand Up @@ -156,23 +189,57 @@ private void setCatNameAndAreaInfo(TatsCnctrResponse.TatsCnctrResponseDto dto, I
themeRepository.findByCat1AndCat2(dto.getCat1(), dto.getCat2())
.ifPresent(theme -> dto.setCatName(theme.getName()));
}

// likeCount 설정
if (dto.getContentid() != null) {
placeService.findByContentId(dto.getContentid())
.ifPresent(place -> dto.setLikeCount(place.getLikeCount()));
}

// areaCode, sigunguCode, areaName 설정
if (areaCode != null && sigunguCode != null) {
dto.setAreaCode(areaCode);
dto.setSigunguCode(sigunguCode);

// areaName 설정
regionRepository.findByKorAreaCodeAndKorSigunguCode(
String.valueOf(areaCode),
String.valueOf(areaCode),
String.valueOf(sigunguCode)
).ifPresent(region -> dto.setAreaName(region.getName()));
}
}

/**
* cnctrRate 문자열을 double로 변환
* @param cnctrRate 혼잡도 문자열
* @return 파싱된 혼잡도 값. 실패 시 -1.0 반환
*/
private double parseCnctrRate(String cnctrRate) {
try {
return Double.parseDouble(cnctrRate);
} catch (NumberFormatException | NullPointerException e) {
return -1.0; // 데이터 없는 경우 최하위
}
}

/**
* cnctrRate 값을 기반으로 한적함 등급 계산
* @param rate 혼잡도 값
* @return 한적함 등급 (1-5, -1은 데이터 없음)
*/
private int calculateQuietnessLevel(double rate) {
if (rate < 0) {
return -1; // 데이터 없음
} else if (rate <= 20.0) {
return 1; // 혼잡
} else if (rate <= 40.0) {
return 2; // 약간 혼잡
} else if (rate <= 60.0) {
return 3; // 보통
} else if (rate <= 80.0) {
return 4; // 한적함
} else {
return 5; // 매우 한적함
}
}
}