Skip to content

Commit 0080967

Browse files
authored
Merge pull request #189 from Travlocks/fix/#176-template
[fix] #176 AI 블록 추천 관련 문제 해결
2 parents f655f11 + 26f1ba8 commit 0080967

File tree

4 files changed

+164
-155
lines changed

4 files changed

+164
-155
lines changed

src/main/java/org/umc/travlocksserver/domain/template/service/command/TemplateDayCommandService.java

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,25 @@
55
import org.springframework.beans.factory.annotation.Value;
66
import org.springframework.data.domain.PageRequest;
77
import org.springframework.stereotype.Service;
8-
import org.springframework.transaction.annotation.Propagation;
98
import org.springframework.transaction.annotation.Transactional;
109
import org.umc.travlocksserver.domain.template.dto.response.VlockSuggestionsResponseDTO;
1110
import org.umc.travlocksserver.domain.template.entity.Template;
1211
import org.umc.travlocksserver.domain.template.entity.TemplateDay;
1312
import org.umc.travlocksserver.domain.template.enums.TransportType;
1413
import org.umc.travlocksserver.domain.template.exception.TemplateDayException;
1514
import org.umc.travlocksserver.domain.template.code.TemplateDayErrorCode;
16-
import org.umc.travlocksserver.domain.template.projection.CityProjectionDTO;
1715
import org.umc.travlocksserver.domain.template.repository.TemplateDayRepository;
1816
import org.umc.travlocksserver.domain.template.service.query.TemplateCityQueryService;
1917
import org.umc.travlocksserver.domain.template.service.query.TemplateQueryService;
2018
import org.umc.travlocksserver.domain.template.service.query.TemplateVlockQueryService;
2119
import org.umc.travlocksserver.domain.vlock.entity.Vlock;
22-
import org.umc.travlocksserver.domain.vlock.service.command.VlockCommandService;
20+
import org.umc.travlocksserver.domain.vlock.service.command.VlockExternalCommandService;
2321
import org.umc.travlocksserver.domain.vlock.service.query.VlockQueryService;
2422
import org.umc.travlocksserver.global.geo.BoundingBox;
2523
import org.umc.travlocksserver.global.geo.GeoUtil;
2624
import org.umc.travlocksserver.global.geo.LatLng;
2725
import org.umc.travlocksserver.infra.ai.client.AiSuggestionClient;
2826
import org.umc.travlocksserver.infra.ai.dto.ScoredCandidate;
29-
import org.umc.travlocksserver.infra.kakao.KakaoPlaceClient;
30-
import org.umc.travlocksserver.infra.kakao.KakaoPlace;
3127
import org.umc.travlocksserver.infra.redis.vlock.CachedVlockSuggestions;
3228
import org.umc.travlocksserver.infra.redis.vlock.VlockSuggestionCache;
3329

@@ -55,14 +51,13 @@ public class TemplateDayCommandService {
5551
private final VlockRepository vlockRepository;
5652

5753
private final TemplateCityQueryService templateCityQueryService;
58-
private final VlockCommandService vlockCommandService;
54+
private final VlockExternalCommandService vlockExternalCommandService;
5955
private final TemplateVlockQueryService templateVlockQueryService;
6056
private final VlockQueryService vlockQueryService;
6157
private final TemplateQueryService templateQueryService;
6258

6359
private final VlockSuggestionCache vlockSuggestionCache;
6460

65-
private final KakaoPlaceClient kakaoPlaceClient;
6661
private final AiSuggestionClient aiClient;
6762

6863
@Value("${suggestion.vlock.popular-pool}")
@@ -80,9 +75,6 @@ public class TemplateDayCommandService {
8075
@Value("${suggestion.vlock.pool-size}")
8176
private int poolSize;
8277

83-
@Value("${kakao.keyword-search.size}")
84-
private int kakaoKeywordSearchSize;
85-
8678
@Value("${suggestion.vlock.size}")
8779
private int vlockSuggestionSize;
8880

@@ -301,7 +293,7 @@ private CachedVlockSuggestions buildSuggestion(Template template) {
301293

302294
// 블록 추천 후보 수가 minPool 이하면 외부 API(카카오맵)에서 가져와서 저장
303295
if (candidates.size() < minPool) {
304-
fetchFromExternal(templateId, null, null);
296+
vlockExternalCommandService.fetchFromExternal(templateId, null, null);
305297
candidates = vlockQueryService.getPopularByCityIds(cityIdsOfTemplate, PageRequest.of(0, popularPool));
306298
}
307299

@@ -563,51 +555,6 @@ private List<Vlock> findNearVlocksByCenter(
563555
.toList();
564556
}
565557

566-
/**
567-
* 카카오맵 API로부터 Vlock들을 추가하는 메서드
568-
*/
569-
@Transactional(propagation = Propagation.NOT_SUPPORTED)
570-
protected void fetchFromExternal(Long templateId, LatLng center, Integer radiusKm) {
571-
List<CityProjectionDTO> cities = templateCityQueryService.getCitiesByTemplateId(templateId);
572-
if (cities.isEmpty())
573-
return;
574-
575-
Double x = (center == null) ? null : center.lng();
576-
Double y = (center == null) ? null : center.lat();
577-
Integer radiusM = (center == null) ? null : radiusKm * 1000;
578-
579-
for (CityProjectionDTO city : cities) {
580-
List<KakaoPlace> results = new ArrayList<>();
581-
results.addAll(fetchKakaoPlaces(city.cityName() + " 관광지", x, y, radiusM));
582-
results.addAll(fetchKakaoPlaces(city.cityName() + " 맛집", x, y, radiusM));
583-
results.addAll(fetchKakaoPlaces(city.cityName() + " 카페", x, y, radiusM));
584-
585-
List<KakaoPlace> deduplicated = deduplicateByPlaceId(results);
586-
587-
vlockCommandService.upsertVlocksFromExternal(city.cityId(), deduplicated);
588-
}
589-
}
590-
591-
/**
592-
* KakaoPlaceId 기준으로 중복되는 결과를 삭제하는 메서드
593-
*/
594-
private List<KakaoPlace> deduplicateByPlaceId(List<KakaoPlace> places) {
595-
Map<String, KakaoPlace> result = new LinkedHashMap<>();
596-
for (KakaoPlace place : places) {
597-
if (place.placeId() == null)
598-
continue;
599-
result.put(place.placeId(), place);
600-
}
601-
return new ArrayList<>(result.values());
602-
}
603-
604-
/**
605-
* 외부(카카오맵) API를 통해 키워드 검색 후 내부 KakaoPlaceDTO로 변환하는 메서드
606-
*/
607-
private List<KakaoPlace> fetchKakaoPlaces(String query, Double lng, Double lat, Integer radiusM) {
608-
return kakaoPlaceClient.searchPlaces(query, lng, lat, radiusM, kakaoKeywordSearchSize);
609-
}
610-
611558
/**
612559
* 추천 후보들을 뽑는 메서드
613560
* - 해당 template에 블록이 없는 경우 Vlock 사용수로, 있는 경우 거리 및 Vlock 사용수로 선별

src/main/java/org/umc/travlocksserver/domain/vlock/service/command/VlockCommandService.java

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import lombok.RequiredArgsConstructor;
44
import org.springframework.stereotype.Service;
5-
import org.springframework.transaction.annotation.Propagation;
65
import org.springframework.transaction.annotation.Transactional;
76
import org.springframework.web.multipart.MultipartFile;
87
import org.umc.travlocksserver.domain.location.constant.CityErrorCode;
@@ -23,16 +22,12 @@
2322
import org.umc.travlocksserver.domain.vlock.dto.response.VlockResponseDTO;
2423
import org.umc.travlocksserver.domain.vlock.entity.Vlock;
2524
import org.umc.travlocksserver.domain.vlock.entity.VlockCategory;
26-
import org.umc.travlocksserver.domain.vlock.exception.VlockCategoryException;
2725
import org.umc.travlocksserver.domain.vlock.exception.VlockException;
2826
import org.umc.travlocksserver.domain.vlock.repository.VlockCategoryRepository;
2927
import org.umc.travlocksserver.domain.vlock.repository.VlockRepository;
3028
import org.umc.travlocksserver.domain.vlock.service.query.VlockCategoryQueryService;
3129
import org.umc.travlocksserver.global.aws.S3Properties;
3230
import org.umc.travlocksserver.global.aws.S3Provider;
33-
import org.umc.travlocksserver.infra.kakao.KakaoPlace;
34-
35-
import java.util.List;
3631

3732
@Service
3833
@RequiredArgsConstructor
@@ -47,7 +42,6 @@ public class VlockCommandService {
4742
private final CityQueryService cityQueryService;
4843
private final S3Provider s3Provider;
4944
private final S3Properties s3Properties;
50-
private final VlockSaveCommandService vlockSaveCommandService;
5145

5246
public VlockResponseDTO createVlock(Long memberId, VlockRequestDTO request, MultipartFile coverImg) {
5347
String imageUrl = uploadImageIfPresent(coverImg);
@@ -106,60 +100,6 @@ public void deleteVlock(Long memberId, Long vlockId) {
106100
vlock.softDelete();
107101
}
108102

109-
// ⚪ 외부(카카오맵) API를 통해 블록을 삽입하는 메서드 (추천시 블록에 데이터가 너무 적을 경우 사용)
110-
@Transactional(propagation = Propagation.NOT_SUPPORTED)
111-
public void upsertVlocksFromExternal(Long cityId, List<KakaoPlace> places) {
112-
City city = cityQueryService.getReferenceById(cityId);
113-
114-
for (KakaoPlace place : places) {
115-
if (place.placeId() == null || place.placeId().isBlank())
116-
continue;
117-
118-
VlockCategory category = mapCategory(place.categoryName());
119-
vlockSaveCommandService.saveVlocksFromExternal(place, category, city);
120-
121-
// boolean exists = vlockRepository.existsByExternalPlaceIdAndIsPublicTrue(place.placeId());
122-
//
123-
// if (exists) {
124-
// continue;
125-
// }
126-
//
127-
// try {
128-
// VlockCategory category = mapCategory(place.categoryName());
129-
//
130-
// Vlock vlock = Vlock.createByExternal(
131-
// place.placeId(),
132-
// category,
133-
// city,
134-
// place.name(),
135-
// place.latitude(),
136-
// place.longitude(),
137-
// place.address()
138-
// );
139-
//
140-
// vlockRepository.save(vlock);
141-
// } catch(DataIntegrityViolationException e) {
142-
// // UNIQUE 충돌 -> 이미 존재하는 블록 -> 무시
143-
// }
144-
}
145-
}
146-
147-
// ⚪ 외부(카카오맵) API를 통해 가져온 카테고리를 우리 서비스 내의 카테고리로 매핑하는 메서드
148-
private VlockCategory mapCategory(String name) {
149-
String mappedName = switch (name) {
150-
case "음식점" -> "식당";
151-
case "카페" -> "카페";
152-
case "관광명소" -> "관광지";
153-
case "문화시설" -> "문화";
154-
default -> "기타";
155-
};
156-
157-
return vlockCategoryQueryService.getByName(mappedName)
158-
.orElseGet(() -> vlockCategoryQueryService.getByName("기타")
159-
.orElseThrow(
160-
() -> new VlockCategoryException(VlockCategoryErrorCode.DEFAULT_VLOCK_CATEGORY_NOT_FOUND)));
161-
}
162-
163103
private void validateMemberExists(Long memberId) {
164104
if (!memberRepository.existsById(memberId)) {
165105
throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package org.umc.travlocksserver.domain.vlock.service.command;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.dao.DataIntegrityViolationException;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Propagation;
8+
import org.springframework.transaction.annotation.Transactional;
9+
import org.umc.travlocksserver.domain.location.entity.City;
10+
import org.umc.travlocksserver.domain.location.service.query.CityQueryService;
11+
import org.umc.travlocksserver.domain.template.projection.CityProjectionDTO;
12+
import org.umc.travlocksserver.domain.template.service.query.TemplateCityQueryService;
13+
import org.umc.travlocksserver.domain.vlock.code.VlockCategoryErrorCode;
14+
import org.umc.travlocksserver.domain.vlock.entity.Vlock;
15+
import org.umc.travlocksserver.domain.vlock.entity.VlockCategory;
16+
import org.umc.travlocksserver.domain.vlock.exception.VlockCategoryException;
17+
import org.umc.travlocksserver.domain.vlock.repository.VlockRepository;
18+
import org.umc.travlocksserver.domain.vlock.service.query.VlockCategoryQueryService;
19+
import org.umc.travlocksserver.global.geo.LatLng;
20+
import org.umc.travlocksserver.infra.kakao.KakaoPlace;
21+
import org.umc.travlocksserver.infra.kakao.KakaoPlaceClient;
22+
23+
import java.util.ArrayList;
24+
import java.util.LinkedHashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
@Service
29+
@RequiredArgsConstructor
30+
public class VlockExternalCommandService {
31+
32+
@Value("${kakao.keyword-search.size}")
33+
private int kakaoKeywordSearchSize;
34+
35+
private final VlockRepository vlockRepository;
36+
private final TemplateCityQueryService templateCityQueryService;
37+
private final VlockCommandService vlockCommandService;
38+
private final KakaoPlaceClient kakaoPlaceClient;
39+
private final CityQueryService cityQueryService;
40+
private final VlockCategoryQueryService vlockCategoryQueryService;
41+
42+
@Transactional(propagation = Propagation.REQUIRES_NEW)
43+
public void saveVlocksFromExternal(KakaoPlace place, VlockCategory category, City city) {
44+
try {
45+
Vlock vlock = Vlock.createByExternal(
46+
place.placeId(),
47+
category,
48+
city,
49+
place.name(),
50+
place.latitude(),
51+
place.longitude(),
52+
place.address()
53+
);
54+
55+
vlockRepository.save(vlock);
56+
} catch(
57+
DataIntegrityViolationException e) {
58+
// UNIQUE 충돌 -> 이미 존재하는 블록 -> 무시
59+
}
60+
}
61+
62+
/**
63+
* 카카오맵 API로부터 Vlock들을 추가하는 메서드
64+
*/
65+
@Transactional(propagation = Propagation.NOT_SUPPORTED)
66+
public void fetchFromExternal(Long templateId, LatLng center, Integer radiusKm) {
67+
List<CityProjectionDTO> cities = templateCityQueryService.getCitiesByTemplateId(templateId);
68+
if (cities.isEmpty())
69+
return;
70+
71+
Double x = (center == null) ? null : center.lng();
72+
Double y = (center == null) ? null : center.lat();
73+
Integer radiusM = (center == null) ? null : radiusKm * 1000;
74+
75+
for (CityProjectionDTO city : cities) {
76+
List<KakaoPlace> results = new ArrayList<>();
77+
results.addAll(fetchKakaoPlaces(city.cityName() + " 관광지", x, y, radiusM));
78+
results.addAll(fetchKakaoPlaces(city.cityName() + " 맛집", x, y, radiusM));
79+
results.addAll(fetchKakaoPlaces(city.cityName() + " 카페", x, y, radiusM));
80+
81+
List<KakaoPlace> deduplicated = deduplicateByPlaceId(results);
82+
83+
upsertVlocksFromExternal(city.cityId(), deduplicated);
84+
}
85+
}
86+
87+
/**
88+
* KakaoPlaceId 기준으로 중복되는 결과를 삭제하는 메서드
89+
*/
90+
private List<KakaoPlace> deduplicateByPlaceId(List<KakaoPlace> places) {
91+
Map<String, KakaoPlace> result = new LinkedHashMap<>();
92+
for (KakaoPlace place : places) {
93+
if (place.placeId() == null)
94+
continue;
95+
result.put(place.placeId(), place);
96+
}
97+
return new ArrayList<>(result.values());
98+
}
99+
100+
/**
101+
* 외부(카카오맵) API를 통해 키워드 검색 후 내부 KakaoPlaceDTO로 변환하는 메서드
102+
*/
103+
private List<KakaoPlace> fetchKakaoPlaces(String query, Double lng, Double lat, Integer radiusM) {
104+
return kakaoPlaceClient.searchPlaces(query, lng, lat, radiusM, kakaoKeywordSearchSize);
105+
}
106+
107+
108+
// ⚪ 외부(카카오맵) API를 통해 블록을 삽입하는 메서드 (추천시 블록에 데이터가 너무 적을 경우 사용)
109+
@Transactional(propagation = Propagation.NOT_SUPPORTED)
110+
public void upsertVlocksFromExternal(Long cityId, List<KakaoPlace> places) {
111+
City city = cityQueryService.getReferenceById(cityId);
112+
113+
for (KakaoPlace place : places) {
114+
if (place.placeId() == null || place.placeId().isBlank())
115+
continue;
116+
117+
VlockCategory category = mapCategory(place.categoryName());
118+
saveVlocksFromExternal(place, category, city);
119+
120+
// boolean exists = vlockRepository.existsByExternalPlaceIdAndIsPublicTrue(place.placeId());
121+
//
122+
// if (exists) {
123+
// continue;
124+
// }
125+
//
126+
// try {
127+
// VlockCategory category = mapCategory(place.categoryName());
128+
//
129+
// Vlock vlock = Vlock.createByExternal(
130+
// place.placeId(),
131+
// category,
132+
// city,
133+
// place.name(),
134+
// place.latitude(),
135+
// place.longitude(),
136+
// place.address()
137+
// );
138+
//
139+
// vlockRepository.save(vlock);
140+
// } catch(DataIntegrityViolationException e) {
141+
// // UNIQUE 충돌 -> 이미 존재하는 블록 -> 무시
142+
// }
143+
}
144+
}
145+
146+
// ⚪ 외부(카카오맵) API를 통해 가져온 카테고리를 우리 서비스 내의 카테고리로 매핑하는 메서드
147+
private VlockCategory mapCategory(String name) {
148+
String mappedName = switch (name) {
149+
case "음식점" -> "식당";
150+
case "카페" -> "카페";
151+
case "관광명소" -> "관광지";
152+
case "문화시설" -> "문화";
153+
default -> "기타";
154+
};
155+
156+
return vlockCategoryQueryService.getByName(mappedName)
157+
.orElseGet(() -> vlockCategoryQueryService.getByName("기타")
158+
.orElseThrow(
159+
() -> new VlockCategoryException(VlockCategoryErrorCode.DEFAULT_VLOCK_CATEGORY_NOT_FOUND)));
160+
}
161+
}

0 commit comments

Comments
 (0)