Skip to content

Commit d1666bb

Browse files
Merge pull request #294 from prgrms-web-devcourse-final-project/feat/#264
[Concert] 찜한 공연 기반 다른 공연 추천
2 parents ee3aeeb + 1c92fc8 commit d1666bb

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ public RsData<List<ConcertItem>> getConcertsListByArtist(
7979
return RsData.success(concertService.getArtistConcertList(artistId,type,pageable));
8080
}
8181

82+
@Operation(summary = "좋아요 기반 추천 공연 목록", description = """
83+
<h2>현재 사용자가 좋아요 한 공연들을 기반으로 앞으로 올 공연들을 추천합니다.</h2><hr/>
84+
좋아요 한 공연의 제목을 어절 단위로 잘라, 중복으로 나타나는 어절에 가중치를 부여합니다.<br/>
85+
이렇게 구성된 어절을 포함하고 있는 제목들을 전부 가져옵니다.<br/>
86+
자카드 유사도를 통해 유사도가 높은 공연을 가져오되, 가중치가 높은 어절을 포함하는 공연이 먼저 보이게끔 정렬합니다.<br/>
87+
<strong>좋아요 한 공연이 없거나, 어절을 포함한 공연이 없을 경우 빈 배열이 반환됩니다.</strong>
88+
""")
89+
@GetMapping("recommendByLike")
90+
public RsData<List<ConcertItem>> getRecommendByLike(){
91+
User user = rq.getUser();
92+
List<ConcertItem> likedList = concertService.concertsRecommendByLike(user);
93+
return RsData.success(likedList);
94+
}
95+
8296
@Operation(summary = "좋아요 한 공연 조회", description = "좋아요를 누른 공연에 대한 목록을 조회합니다. 저장 날짜를 기준으로 내림차순 정렬로 표시합니다.(최신으로 추가된 목록순입니다.)")
8397
@GetMapping("likedConcertList")
8498
public RsData<List<ConcertItem>> getLikedConcertList(

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
1414
import lombok.RequiredArgsConstructor;
1515
import lombok.extern.slf4j.Slf4j;
16+
import org.springframework.data.domain.PageRequest;
1617
import org.springframework.data.domain.Pageable;
1718
import org.springframework.stereotype.Service;
1819
import org.springframework.transaction.annotation.Transactional;
@@ -302,9 +303,7 @@ public List<ConcertItem> recommendSimilarConcerts(long concertId) {
302303
// 유사한 제목을 가지는 공연 추천
303304
public List<ConcertItem> recommendSimilarTitleConcerts(long concertId) {
304305
Concert concert = findConcertByConcertId(concertId);
305-
String name = concert.getName();
306-
String match = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\\s]";
307-
name = name.replaceAll(match, "");
306+
String name = simplifyKeyword(concert.getName());
308307
log.info("name: " + name);
309308
String[] words = name.split(" ");
310309
List<AutoCompleteItem> result = new ArrayList<>();
@@ -327,6 +326,78 @@ public List<ConcertItem> recommendSimilarTitleConcerts(long concertId) {
327326
return concertItemList;
328327
}
329328

329+
// 좋아요 한 제목에서 중복으로 나타나는 단어에 가중치 부여 후 자카드 유사도에 가점 부여하여 정렬 후 공연 추천
330+
public List<ConcertItem> concertsRecommendByLike(User user){
331+
Pageable pageable = PageRequest.of(0, 100);
332+
List<ConcertItem> likeList = concertRepository.getLikedConcertsList(pageable,user.getId());
333+
if(likeList.isEmpty()) return new ArrayList<>(); // 좋아요 한 공연이 없을 경우 빈 공연 반환
334+
Map<String, WeightedBits> weightedBitsMap = new HashMap<>();
335+
Set<Long> idSet = new HashSet<>();
336+
for(ConcertItem item : likeList){
337+
idSet.add(item.getId());
338+
String name = item.getName();
339+
String simpleName = simplifyKeyword(name);
340+
String[] words = simpleName.split(" ");
341+
342+
for (String word : words) { // 단어별로 가중치 적용
343+
WeightedBits weightedBits = weightedBitsMap.getOrDefault(word, new WeightedBits(word,0));
344+
weightedBits.plusWeight();
345+
weightedBitsMap.put(word, weightedBits);
346+
}
347+
}
348+
349+
List<AutoCompleteItem> result = new ArrayList<>();
350+
for (String word : weightedBitsMap.keySet()) {
351+
if(word.isEmpty()) continue;
352+
log.info("word: " + word);
353+
result.addAll(concertSearchRedisTemplate.getAutoCompleteWord(word,0,6));
354+
}
355+
356+
Set<Long> resultIdSet = new HashSet<>();
357+
for (AutoCompleteItem item : result) {
358+
if(idSet.contains(item.getId())) continue; // 찜한 목록 제거
359+
resultIdSet.add(item.getId()); // 중복 제거
360+
}
361+
362+
List<Long> idList = new ArrayList<>();
363+
for (Long id : resultIdSet) {
364+
idList.add(id);
365+
}
366+
367+
List<ConcertItem> concertItemList = concertRepository.getConcertItemsInIdList(idList,LocalDate.now());
368+
concertItemList.sort(Comparator.comparingDouble(
369+
ci -> jaccardSimilarityWithWeight(weightedBitsMap,ci.getName().split(" "))
370+
));
371+
return concertItemList;
372+
}
373+
374+
private double jaccardSimilarityWithWeight(Map<String,WeightedBits> weightedBitsMap, String[] words) {
375+
int intersection =0;
376+
for (String word : words) {
377+
if(word.isEmpty()) continue;
378+
WeightedBits weightedBits = weightedBitsMap.get(word);
379+
if(weightedBits == null) continue;
380+
intersection += weightedBits.weight;
381+
}
382+
int union = weightedBitsMap.size() + words.length;
383+
return (double)union/intersection;
384+
}
385+
386+
private class WeightedBits{
387+
String bit;
388+
int weight;
389+
390+
public WeightedBits(String bit, int weight) {
391+
this.bit = bit;
392+
this.weight = weight;
393+
}
394+
395+
public WeightedBits plusWeight(){
396+
this.weight++;
397+
return this;
398+
}
399+
}
400+
330401
private double jaccardSimilarity(String[] origin , String[] target) {
331402
Set<String> union = new HashSet<>();
332403
Set<String> intersection = new HashSet<>();
@@ -342,6 +413,13 @@ private double jaccardSimilarity(String[] origin , String[] target) {
342413
return (double) union.size() / intersection.size();
343414
}
344415

416+
// 특수 문자를 제거합니다.
417+
private static String simplifyKeyword(String name) {
418+
String match = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\\s]";
419+
name = name.replaceAll(match, "");
420+
return name;
421+
}
422+
345423
@Transactional(readOnly = true)
346424
public void validateConcertExists(Long concertId) {
347425
concertRepository.findById(concertId)

0 commit comments

Comments
 (0)