Skip to content

Commit e56b4ac

Browse files
authored
Merge pull request #89 from SWYP-mingling/fix/SW-94-cacheable-runtime-exception
[SW-94] fix: 장소추천 API 런타임 오류 수정
2 parents 8bfecd1 + 8ba1064 commit e56b4ac

12 files changed

Lines changed: 321 additions & 64 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: 155 additions & 44 deletions
Large diffs are not rendered by default.

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: 7 additions & 4 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
*/
@@ -130,8 +131,10 @@ static Optional<Category> from(String category) {
130131
return Optional.empty();
131132
}
132133

134+
String categoryWithoutSpaces = category.replaceAll("\\s+", "");
135+
133136
return Arrays.stream(values())
134-
.filter(c -> c.categoryName.equals(category))
137+
.filter(c -> c.categoryName.equals(categoryWithoutSpaces))
135138
.findFirst();
136139
}
137140
}
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/participant/service/EnterMeetingUseCase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ public void execute(UUID meetingId,
6060
} else {
6161
// 새로운 참여자인 경우, 실제 참여자 수 체크
6262
long currentParticipantCount = participantRepository.countByMeetingAndIsDeletedFalse(meeting);
63+
long maxParticipants = meeting.getCount();
6364

64-
if(currentParticipantCount >= 10) { // 참여자 MAX 값
65+
if(currentParticipantCount >= maxParticipants) { // 참여자 MAX 값
6566
throw BusinessException.capacityExceeded();
6667
}
6768

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;

src/main/java/swyp/mingling/domain/subway/parser/SeoulMetroRouteParser.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ public SubwayRouteInfo parse(SeoulMetroRouteResponse response) {
3030

3131
List<SeoulMetroRouteResponse.PathInfo> pathList = response.getPathInfoList();
3232

33-
// 출발역 = 첫 번째 경로의 출발역
34-
String startStation = pathList.get(0).getDptreStn().getStnNm();
33+
// 출발역 = 첫 번째 경로의 출발역 (역 이름 정규화: "역" 제거)
34+
String startStation = normalizeStationName(pathList.get(0).getDptreStn().getStnNm());
3535
String startStationLine = formatLineNumber(pathList.get(0).getDptreStn().getLineNm());
3636

37-
// 도착역 = 마지막 경로의 도착역
38-
String endStation = pathList.get(pathList.size() - 1).getArvlStn().getStnNm();
37+
// 도착역 = 마지막 경로의 도착역 (역 이름 정규화: "역" 제거)
38+
String endStation = normalizeStationName(pathList.get(pathList.size() - 1).getArvlStn().getStnNm());
3939
String endStationLine = formatLineNumber(pathList.get(pathList.size() - 1).getArvlStn().getLineNm());
4040

4141
// 총 이동 시간 (초 → 분 변환)
@@ -82,7 +82,7 @@ private List<SubwayRouteInfo.StationInfo> buildStationInfoList(
8282
// 출발역 추가 (첫 번째 경로일 때만)
8383
if (i == 0) {
8484
stations.add(SubwayRouteInfo.StationInfo.builder()
85-
.stationName(path.getDptreStn().getStnNm())
85+
.stationName(normalizeStationName(path.getDptreStn().getStnNm()))
8686
.lineNumber(formatLineNumber(path.getDptreStn().getLineNm()))
8787
.travelTime(0)
8888
.isTransfer(false)
@@ -94,12 +94,15 @@ private List<SubwayRouteInfo.StationInfo> buildStationInfoList(
9494
Integer travelTimeInSeconds = path.getReqHr();
9595
Integer travelTimeInMinutes = travelTimeInSeconds != null ? travelTimeInSeconds / 60 : 0;
9696

97+
String arrivalStationName = normalizeStationName(path.getArvlStn().getStnNm());
98+
boolean isTransfer = "Y".equalsIgnoreCase(path.getTrsitYn());
99+
97100
stations.add(SubwayRouteInfo.StationInfo.builder()
98-
.stationName(path.getArvlStn().getStnNm())
101+
.stationName(arrivalStationName)
99102
.lineNumber(formatLineNumber(path.getArvlStn().getLineNm()))
100103
.travelTime(travelTimeInMinutes)
101-
.isTransfer("Y".equalsIgnoreCase(path.getTrsitYn()))
102-
.transferStationName("Y".equalsIgnoreCase(path.getTrsitYn()) ? path.getArvlStn().getStnNm() : null)
104+
.isTransfer(isTransfer)
105+
.transferStationName(isTransfer ? arrivalStationName : null)
103106
.build());
104107
}
105108

@@ -126,7 +129,7 @@ private List<SubwayRouteInfo.TransferInfo> buildTransferPath(List<SeoulMetroRout
126129
// 각 경로의 도착역 호선 확인하여 변경 시 환승 정보 추가
127130
for (SeoulMetroRouteResponse.PathInfo path : pathList) {
128131
String arrivalLine = path.getArvlStn().getLineNm();
129-
String arrivalStation = path.getArvlStn().getStnNm();
132+
String arrivalStation = normalizeStationName(path.getArvlStn().getStnNm());
130133

131134
// 호선이 변경되었을 때 = 환승이 발생한 경우
132135
if (!arrivalLine.equals(currentLine)) {
@@ -166,4 +169,21 @@ private String formatLineNumber(String lineNumber) {
166169
// 특수 노선 (경의중앙선, 공항철도 등)은 그대로 반환
167170
return lineNumber;
168171
}
172+
173+
/**
174+
* @param stationName 역 이름
175+
* @return 정규화된 역 이름 ("역" 제거)
176+
*/
177+
private String normalizeStationName(String stationName) {
178+
if (stationName == null || stationName.isEmpty()) {
179+
return stationName;
180+
}
181+
182+
// "역"이 끝에 붙어있으면 제거
183+
if (stationName.endsWith("역")) {
184+
return stationName.substring(0, stationName.length() - 1);
185+
}
186+
187+
return stationName;
188+
}
169189
}

0 commit comments

Comments
 (0)