Skip to content

Commit bca9d7a

Browse files
authored
Merge pull request #88 from SWYP-mingling/fix/SW-78-fix-station-name
2 parents 5dfbc8e + 8674956 commit bca9d7a

9 files changed

Lines changed: 202 additions & 27 deletions

File tree

src/main/java/swyp/mingling/domain/meeting/dto/response/midpoint/GetMidPointResponse.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public class GetMidPointResponse {
2323
@Schema(description = "경도", example = "126.9236")
2424
private double longitude;
2525

26+
@Schema(description = "장소가 가장 많은지 여부")
27+
private boolean isHot;
28+
29+
@Schema(description = "주변 장소 개수")
30+
private Integer placeCount;
31+
2632
@Schema(description = "사용자 목록")
2733
List<UserRouteDto> userRoutes;
2834
}

src/main/java/swyp/mingling/domain/meeting/dto/response/midpoint/MidPointCandidate.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
import lombok.AllArgsConstructor;
44
import lombok.Getter;
5-
import java.util.List;
65
import swyp.mingling.domain.subway.dto.SubwayRouteInfo;
76

7+
import java.util.List;
8+
89
@Getter
910
@AllArgsConstructor
1011
public class MidPointCandidate {
1112

1213
private List<SubwayRouteInfo> routes; // 해당 번화가로 가는 모든 사람의 경로
1314
private int deviation; // 이동시간 편차
1415
private int avgTime; // 평균 이동시간 or 총합
16+
private boolean isHot; // 가장 장소가 많은 곳
17+
private int placeCount; // 주변 장소 개수
18+
19+
public void setHot(boolean hot) {
20+
isHot = hot;
21+
}
1522
}

src/main/java/swyp/mingling/domain/meeting/repository/MeetingRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import swyp.mingling.domain.meeting.entity.Meeting;
88

99
import java.util.List;
10+
import java.util.Optional;
1011
import java.util.UUID;
1112

1213
/**
@@ -21,4 +22,10 @@ public interface MeetingRepository extends JpaRepository<Meeting, UUID> {
2122
WHERE m.id = :meetingId and p.departure is not null
2223
""")
2324
List<DepartureListResponse> findDeparturesAndNicknameByMeetingId(@Param("meetingId") UUID meetingId);
25+
26+
@Query("SELECT mp.name FROM Meeting m " +
27+
"JOIN m.purposeMappings mpm " +
28+
"JOIN mpm.purpose mp " +
29+
"WHERE m.id = :meetingId")
30+
Optional<String> findPurposeNamesByMeetingId(@Param("meetingId") UUID meetingId);
2431
}

src/main/java/swyp/mingling/domain/meeting/service/MidPointAsyncUseCase.java

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import lombok.extern.slf4j.Slf4j;
66
import org.springframework.stereotype.Service;
77
import org.springframework.transaction.annotation.Transactional;
8-
import swyp.mingling.domain.meeting.dto.response.midpoint.*;
98
import swyp.mingling.domain.meeting.dto.StationCoordinate;
9+
import swyp.mingling.domain.meeting.dto.response.midpoint.*;
1010
import swyp.mingling.domain.meeting.repository.HotPlaceRepository;
1111
import swyp.mingling.domain.meeting.repository.MeetingRepository;
1212
import swyp.mingling.domain.subway.dto.SubwayRouteInfo;
1313
import swyp.mingling.domain.subway.service.SubwayRouteService;
14+
import swyp.mingling.external.dto.response.KakaoPlaceSearchResponse;
1415

1516
import java.util.*;
1617
import java.util.concurrent.CompletableFuture;
@@ -27,6 +28,7 @@ public class MidPointAsyncUseCase {
2728
private final FindStationCoordinateUseCase findStationCoordinateUseCase;
2829
private final HotPlaceRepository hotPlaceRepository;
2930
private final SubwayRouteService subwayRouteService;
31+
private final SearchPlaceCountUseCase searchPlaceCountUseCase;
3032

3133
public List<GetMidPointResponse> execute(UUID meetingId) {
3234
long startTime = System.currentTimeMillis();
@@ -71,6 +73,9 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
7173
.limit(5)
7274
.toList();
7375

76+
//카테고리 가져오기
77+
String category = meetingRepository.findPurposeNamesByMeetingId(meetingId).orElse("식당");
78+
7479
// ========================================
7580
// 편차가 작은 번화가 3개 추출 (비동기 병렬 처리)
7681
// ========================================
@@ -118,21 +123,35 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
118123
}))
119124
.toList();
120125

126+
//수정: 카카오 장소 추천 API를 비동기로 요청
127+
CompletableFuture<KakaoPlaceSearchResponse> kakaoPlaceSearchResponse = CompletableFuture.supplyAsync(() ->
128+
searchPlaceCountUseCase.execute(fivehotlist.getName(), category, 1, 15)
129+
);
130+
131+
// 비동기 작업을 하나의 리스트로 통합
132+
List<CompletableFuture<?>> allCombinedTasks = new ArrayList<>(routeFutures);
133+
allCombinedTasks.add(kakaoPlaceSearchResponse);
134+
121135
// [개선사항]
122136
// allOf()를 사용하여 모든 경로 조회를 한번에 대기
123137
// 기존: routeFutures.stream().map(CompletableFuture::join) == 순차 대기
124138
// 개선: allOf()로 묶어서 가장 느린 작업 하나만 기다림
125139
CompletableFuture<Void> allRoutes = CompletableFuture.allOf(
126-
routeFutures.toArray(new CompletableFuture[0])
140+
allCombinedTasks.toArray(new CompletableFuture[0])
127141
);
128142

143+
129144
// 모든 경로가 완료되면 MidPointCandidate 생성
130145
// thenApply()로 비블로킹 방식으로 후속 처리
131146
return allRoutes.thenApply(v -> {
132147
List<SubwayRouteInfo> routes = routeFutures.stream()
133148
.map(CompletableFuture::join)
134149
.toList();
135150

151+
KakaoPlaceSearchResponse place = kakaoPlaceSearchResponse.join();
152+
153+
int placeCount = place.getMeta().getTotalCount();
154+
136155
int min = Integer.MAX_VALUE;
137156
int max = Integer.MIN_VALUE;
138157

@@ -151,7 +170,7 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
151170

152171
int avgTime = sum / routes.size();
153172

154-
return new MidPointCandidate(routes, deviation, avgTime);
173+
return new MidPointCandidate(routes, deviation, avgTime, false, placeCount);
155174
});
156175
})
157176
.toList();
@@ -170,22 +189,46 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
170189
.map(CompletableFuture::join)
171190
.toList();
172191

173-
List<List<SubwayRouteInfo>> midlist =
192+
// 이동시간 + 편차 고려한 리스트 중 2개 추출
193+
List<MidPointCandidate> sortedByFairness =
174194
candidates.stream()
175195
.sorted(
176196
Comparator.comparing(MidPointCandidate::getDeviation)
177197
.thenComparing(MidPointCandidate::getAvgTime)
178198
)
179-
.limit(3)
180-
.map(MidPointCandidate::getRoutes)
199+
.limit(2)
181200
.toList();
182201

202+
// 장소 개수 기준 상위 1개 추출 (중복 여부 상관없이 1위 추출)
203+
MidPointCandidate hotnessSelection = candidates.stream()
204+
.sorted(Comparator.comparing(MidPointCandidate::getPlaceCount).reversed())
205+
.findFirst()
206+
.orElse(sortedByFairness.get(0)); // 만약 리스트가 비어있을 경우 대비
207+
208+
// 장소가 가장많은 중간지점은 Hot = true
209+
hotnessSelection.setHot(true);
210+
211+
// 중복 제거를 위한 LinkedHashSet
212+
Set<MidPointCandidate> set = new LinkedHashSet<>();
213+
// 중간지점 추가
214+
set.addAll(sortedByFairness);
215+
set.add(hotnessSelection);
216+
217+
List<MidPointCandidate> finalThree = new ArrayList<>(set);
218+
219+
// // List<List<SubwayRouteInfo>> 형식으로 변환
220+
// List<List<SubwayRouteInfo>> midlist = finalThree.stream()
221+
// .map(MidPointCandidate::getRoutes)
222+
// .toList();
223+
183224
// 1. 결과 데이터를 담을 리스트 (좌표 조회 캐시 적용)
184-
List<GetMidPointResponse> finalResult = midlist.stream()
225+
List<GetMidPointResponse> finalResult = finalThree.stream()
185226
.map(routeList -> {
186227
// 이 그룹의 공통 목적지 추출
187-
String endStationName = routeList.get(0).getEndStation();
188-
String endStationLine = routeList.get(0).getEndStationLine();
228+
String endStationName = routeList.getRoutes().get(0).getEndStation();
229+
String endStationLine = routeList.getRoutes().get(0).getEndStationLine();
230+
Boolean isHot = routeList.isHot();
231+
Integer placeCount = routeList.getPlaceCount();
189232

190233
// 목적지 좌표 캐싱
191234
StationCoordinate endStationCoord = stationCoordinateCache.computeIfAbsent( // 만약 데이터가 없으면 계산
@@ -194,9 +237,9 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
194237
);
195238

196239
// 2. 인덱스를 활용해 사용자별 닉네임과 경로 정보를 매핑 (IntStream 사용)
197-
List<UserRouteDto> userRouteDtos = IntStream.range(0, routeList.size())
240+
List<UserRouteDto> userRouteDtos = IntStream.range(0, routeList.getRoutes().size())
198241
.mapToObj(i -> {
199-
SubwayRouteInfo route = routeList.get(i);
242+
SubwayRouteInfo route = routeList.getRoutes().get(i);
200243
// 기존 참여자 리스트(departurelists)에서 같은 순서의 닉네임을 가져옴
201244
String nickname = departurelists.get(i).getNickname();
202245

@@ -225,13 +268,24 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
225268
// 경로 상 역들의 좌표 캐싱
226269
List<StationPathResponse> stationResponses = route.getStations().stream()
227270
.map(station -> {
271+
String name = station.getStationName();
272+
String line = station.getLineNumber();
273+
274+
//검색어
275+
String searchName = name;
276+
277+
//경의중앙선 양평 검색어 변경
278+
if ("양평".equals(name) && "경의선".equals(line)) {
279+
searchName = "양평(경의중앙선)";
280+
}
281+
228282
StationCoordinate coord = stationCoordinateCache.computeIfAbsent(
229-
station.getStationName(),
283+
searchName,
230284
stationName -> findStationCoordinateUseCase.excute(stationName)
231285
);
232286
return StationPathResponse.from(
233-
station.getLineNumber(),
234-
station.getStationName(),
287+
line,
288+
name,
235289
coord.getLatitude(),
236290
coord.getLongitude()
237291
);
@@ -257,6 +311,8 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
257311
.endStation(endStationName)
258312
.latitude(endStationCoord.getLatitude())
259313
.longitude(endStationCoord.getLongitude())
314+
.isHot(isHot)
315+
.placeCount(placeCount)
260316
.userRoutes(userRouteDtos)
261317
.build();
262318
})

src/main/java/swyp/mingling/domain/meeting/service/MidPointUseCase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
114114
int avgTime = sum / routes.size();
115115

116116
candidates.add(
117-
new MidPointCandidate(routes, deviation, avgTime)
117+
new MidPointCandidate(routes, deviation, avgTime, false, 0)
118118
);
119119
}
120120

@@ -173,6 +173,7 @@ public List<GetMidPointResponse> execute(UUID meetingId) {
173173
.endStation(endStationName)
174174
.latitude(findStationCoordinateUseCase.excute(endStationName).getLatitude())
175175
.longitude(findStationCoordinateUseCase.excute(endStationName).getLongitude())
176+
176177
.userRoutes(userRouteDtos)
177178
.build();
178179
})

src/main/java/swyp/mingling/domain/meeting/service/RecommendPlaceUseCase.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package swyp.mingling.domain.meeting.service;
22

3-
import java.util.Arrays;
4-
import java.util.List;
5-
import java.util.Optional;
63
import lombok.Getter;
74
import lombok.RequiredArgsConstructor;
85
import lombok.extern.slf4j.Slf4j;
@@ -14,6 +11,10 @@
1411
import swyp.mingling.global.enums.KakaoCategoryGroupCode;
1512
import swyp.mingling.global.exception.BusinessException;
1613

14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.Optional;
17+
1718
/**
1819
* 장소 추천 UseCase
1920
*/
@@ -33,11 +34,7 @@ public class RecommendPlaceUseCase {
3334
* @param size 조회할 개수 (기본값 15)
3435
* @return 추천 장소 목록
3536
*/
36-
@Cacheable(
37-
cacheNames = "place-recommend",
38-
cacheManager = "placeCacheManager",
39-
key = "'recommend:' + #midPlace + ':' + #category + ':' + #page + ':' + #size"
40-
)
37+
@Cacheable
4138
public RecommendResponse execute(String midPlace, String category, int page, int size) {
4239

4340
log.info("[CACHE MISS] Call Kakao place search API - midPlace: {}, category: {}, page: {}, size: {}"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package swyp.mingling.domain.meeting.service;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Service;
7+
import swyp.mingling.external.KakaoPlaceClient;
8+
import swyp.mingling.external.dto.response.KakaoPlaceSearchResponse;
9+
import swyp.mingling.global.enums.KakaoCategoryGroupCode;
10+
import swyp.mingling.global.exception.BusinessException;
11+
12+
import java.util.Arrays;
13+
import java.util.Optional;
14+
15+
@Slf4j
16+
@Service
17+
@RequiredArgsConstructor
18+
public class SearchPlaceCountUseCase {
19+
20+
private final KakaoPlaceClient kakaoPlaceClient;
21+
22+
public KakaoPlaceSearchResponse execute(String midPlace, String category, int page, int size) {
23+
24+
log.info("[CACHE MISS] Call Kakao place search API - midPlace: {}, category: {}, page: {}, size: {}"
25+
, midPlace, category, page, size);
26+
27+
// 1. 카테고리 한글명을 카카오 카테고리 그룹 코드로 변환
28+
SearchPlaceCountUseCase.Categorysub categoryEnum = SearchPlaceCountUseCase.Categorysub.from(category)
29+
.orElseThrow(BusinessException::purposeNotFound);
30+
31+
// 2. 검색어 생성
32+
String query = buildSearchQuery(midPlace, categoryEnum);
33+
34+
// 3. 카카오 장소 검색 API 호출
35+
KakaoPlaceSearchResponse response =
36+
kakaoPlaceClient.search(
37+
query,
38+
categoryEnum.getKakaoCode(),
39+
page,
40+
size
41+
);
42+
43+
return response;
44+
}
45+
/**
46+
* 중간 지점 키워드와 카테고리를 조합해 카카오 장소 검색에 사용할 검색어를 생성
47+
*
48+
* @param midPlace 중간 지점 키워드 (예: "합정역")
49+
* @param category 모임 목적 카테고리
50+
* @return 카카오 장소 검색에 사용할 검색 쿼리 문자열
51+
*/
52+
private String buildSearchQuery(String midPlace, SearchPlaceCountUseCase.Categorysub category) {
53+
return midPlace + " " + category.getQueryKeyword();
54+
}
55+
56+
/**
57+
* 모임 목적 카테고리
58+
* 외부에서 전달된 한글 카테고리를 기준으로 카카오 장소 검색 API 에서 사용할 category_group_code 로 변환하기 위한 내부 ENUM
59+
*/
60+
@Getter
61+
@RequiredArgsConstructor
62+
private enum Categorysub {
63+
64+
RESTAURANT("식당", "맛집", KakaoCategoryGroupCode.RESTAURANT.getCode()),
65+
CAFE("카페", "카페", KakaoCategoryGroupCode.CAFE.getCode()),
66+
BAR("술집", "술집", KakaoCategoryGroupCode.RESTAURANT.getCode()),
67+
STUDY_CAFE("스터디카페", "스터디카페", null),
68+
SPACE_RENTAL("장소대여", "모임공간", null),
69+
ENTERTAINMENT("놀거리", "실내데이트", null);
70+
71+
private final String categoryName;
72+
private final String queryKeyword;
73+
private final String kakaoCode;
74+
75+
/**
76+
* 외부에서 전달된 카테고리 문자열을 Category 를 enum 으로 변환
77+
*
78+
* @param category 외부 요청으로 전달된 카테고리 문자열
79+
* @return 일치하는 Optional<Category>
80+
*/
81+
static Optional<SearchPlaceCountUseCase.Categorysub> from(String category) {
82+
if (category == null || category.isBlank()) {
83+
return Optional.empty();
84+
}
85+
86+
String categoryWithoutSpaces = category.replaceAll("\\s+", "");
87+
88+
return Arrays.stream(values())
89+
.filter(c -> c.categoryName.equals(categoryWithoutSpaces))
90+
.findFirst();
91+
}
92+
}
93+
}
94+

src/main/java/swyp/mingling/domain/subway/dto/SubwayRouteInfo.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class SubwayRouteInfo {
2121
private final String endStation;
2222
private final String endStationLine;
2323
private final Integer totalTravelTime;
24+
private final Integer placeCount;
2425
private final Double totalDistance;
2526
private final Integer transferCount;
2627
private final List<TransferInfo> transferPath;

0 commit comments

Comments
 (0)