From 6d4da35b42789d05395d50f64eb07049ae975293 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Mon, 5 Jan 2026 19:41:20 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat=20:=20=EA=B0=80=EC=A4=91=EC=B9=98=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concerts/service/ConcertService.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java index 47df3267..2e2ade0a 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java @@ -13,6 +13,7 @@ import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -327,6 +328,24 @@ public List recommendSimilarTitleConcerts(long concertId) { return concertItemList; } + public List concertsRecommendByLike(User user){ + Pageable pageable = PageRequest.of(0, 100); + List likeList = concertRepository.getLikedConcertsList(pageable,user.getId()); + + + return null; + } + + private class WeightedBits{ + String bit; + int weight; + + public WeightedBits(String bit, int weight) { + this.bit = bit; + this.weight = weight; + } + } + private double jaccardSimilarity(String[] origin , String[] target) { Set union = new HashSet<>(); Set intersection = new HashSet<>(); From 0e00d36fb2dd54fd0592e321f5080d23760c6047 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Mon, 5 Jan 2026 21:40:30 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EA=B3=B5=EC=97=B0=20=EC=B0=9C?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EA=B8=B0=EC=A4=80=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concerts/service/ConcertService.java | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java index 2e2ade0a..2579986b 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java @@ -303,9 +303,7 @@ public List recommendSimilarConcerts(long concertId) { // 유사한 제목을 가지는 공연 추천 public List recommendSimilarTitleConcerts(long concertId) { Concert concert = findConcertByConcertId(concertId); - String name = concert.getName(); - String match = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\\s]"; - name = name.replaceAll(match, ""); + String name = simplifyKeyword(concert.getName()); log.info("name: " + name); String[] words = name.split(" "); List result = new ArrayList<>(); @@ -328,12 +326,59 @@ public List recommendSimilarTitleConcerts(long concertId) { return concertItemList; } + // 좋아요 한 제목에서 중복으로 나타나는 단어에 가중치 부여 후 자카드 유사도에 가점 부여하여 정렬 후 공연 추천 public List concertsRecommendByLike(User user){ Pageable pageable = PageRequest.of(0, 100); List likeList = concertRepository.getLikedConcertsList(pageable,user.getId()); + Map weightedBitsMap = new HashMap<>(); + Set idSet = new HashSet<>(); + for(ConcertItem item : likeList){ + idSet.add(item.getId()); + String name = item.getName(); + String simpleName = simplifyKeyword(name); + String[] words = simpleName.split(" "); + + for (String word : words) { // 단어별로 가중치 적용 + WeightedBits weightedBits = weightedBitsMap.getOrDefault(word, new WeightedBits(word,0)); + weightedBits.plusWeight(); + weightedBitsMap.put(word, weightedBits); + } + } + + List result = new ArrayList<>(); + for (String word : weightedBitsMap.keySet()) { + if(word.isEmpty()) continue; + log.info("word: " + word); + result.addAll(concertSearchRedisTemplate.getAutoCompleteWord(word,0,6)); + } + Set resultIdSet = new HashSet<>(); + for (AutoCompleteItem item : result) { + if(idSet.contains(item.getId())) continue; // 찜한 목록 제거 + resultIdSet.add(item.getId()); + } + + List idList = new ArrayList<>(); + for (Long id : resultIdSet) { + idList.add(id); + } - return null; + List concertItemList = concertRepository.getConcertItemsInIdList(idList,LocalDate.now()); + concertItemList.sort(Comparator.comparingDouble( + ci -> jaccardSimilarityWithWeight(weightedBitsMap,ci.getName().split(" ")) + )); + return concertItemList; + } + + private double jaccardSimilarityWithWeight(Map weightedBitsMap, String[] words) { + int intersection =0; + for (String word : words) { + if(word.isEmpty()) continue; + WeightedBits weightedBits = weightedBitsMap.get(word); + intersection += weightedBits.weight; + } + int union = weightedBitsMap.size() + words.length; + return (double)union/intersection; } private class WeightedBits{ @@ -344,6 +389,11 @@ public WeightedBits(String bit, int weight) { this.bit = bit; this.weight = weight; } + + public WeightedBits plusWeight(){ + this.weight++; + return this; + } } private double jaccardSimilarity(String[] origin , String[] target) { @@ -361,6 +411,12 @@ private double jaccardSimilarity(String[] origin , String[] target) { return (double) union.size() / intersection.size(); } + private static String simplifyKeyword(String name) { + String match = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\\s]"; + name = name.replaceAll(match, ""); + return name; + } + @Transactional(readOnly = true) public void validateConcertExists(Long concertId) { concertRepository.findById(concertId) From c3f6ee81549eaaaf56eac8ef92dcae7b0a39a581 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 10:12:30 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20null=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/concerts/controller/ConcertController.java | 8 ++++++++ .../domain/concerts/service/ConcertService.java | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java index f00392c6..9584d593 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java @@ -79,6 +79,14 @@ public RsData> getConcertsListByArtist( return RsData.success(concertService.getArtistConcertList(artistId,type,pageable)); } + @Operation(summary = "좋아요 기반 추천 공연 목록", description = "좋아요 데이터 기반으로 공연을 추천합니다.") + @GetMapping("recommendByLike") + public RsData> getRecommendByLike(){ + User user = rq.getUser(); + List likedList = concertService.concertsRecommendByLike(user); + return RsData.success(likedList); + } + @Operation(summary = "좋아요 한 공연 조회", description = "좋아요를 누른 공연에 대한 목록을 조회합니다. 저장 날짜를 기준으로 내림차순 정렬로 표시합니다.(최신으로 추가된 목록순입니다.)") @GetMapping("likedConcertList") public RsData> getLikedConcertList( diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java index 2579986b..3576a524 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java @@ -355,7 +355,7 @@ public List concertsRecommendByLike(User user){ Set resultIdSet = new HashSet<>(); for (AutoCompleteItem item : result) { if(idSet.contains(item.getId())) continue; // 찜한 목록 제거 - resultIdSet.add(item.getId()); + resultIdSet.add(item.getId()); // 중복 제거 } List idList = new ArrayList<>(); @@ -375,6 +375,7 @@ private double jaccardSimilarityWithWeight(Map weightedBits for (String word : words) { if(word.isEmpty()) continue; WeightedBits weightedBits = weightedBitsMap.get(word); + if(weightedBits == null) continue; intersection += weightedBits.weight; } int union = weightedBitsMap.size() + words.length; @@ -411,6 +412,7 @@ private double jaccardSimilarity(String[] origin , String[] target) { return (double) union.size() / intersection.size(); } + // 특수 문자를 제거합니다. private static String simplifyKeyword(String name) { String match = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\\s]"; name = name.replaceAll(match, ""); From 1c92fc800970cfd1bf23643ac62ddd5803711664 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 10:39:16 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=ED=99=94=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/concerts/controller/ConcertController.java | 8 +++++++- .../domain/concerts/service/ConcertService.java | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java index 9584d593..9719505b 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java @@ -79,7 +79,13 @@ public RsData> getConcertsListByArtist( return RsData.success(concertService.getArtistConcertList(artistId,type,pageable)); } - @Operation(summary = "좋아요 기반 추천 공연 목록", description = "좋아요 데이터 기반으로 공연을 추천합니다.") + @Operation(summary = "좋아요 기반 추천 공연 목록", description = """ +

현재 사용자가 좋아요 한 공연들을 기반으로 앞으로 올 공연들을 추천합니다.


+ 좋아요 한 공연의 제목을 어절 단위로 잘라, 중복으로 나타나는 어절에 가중치를 부여합니다.
+ 이렇게 구성된 어절을 포함하고 있는 제목들을 전부 가져옵니다.
+ 자카드 유사도를 통해 유사도가 높은 공연을 가져오되, 가중치가 높은 어절을 포함하는 공연이 먼저 보이게끔 정렬합니다.
+ 좋아요 한 공연이 없거나, 어절을 포함한 공연이 없을 경우 빈 배열이 반환됩니다. + """) @GetMapping("recommendByLike") public RsData> getRecommendByLike(){ User user = rq.getUser(); diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java index 3576a524..5421a979 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java @@ -330,6 +330,7 @@ public List recommendSimilarTitleConcerts(long concertId) { public List concertsRecommendByLike(User user){ Pageable pageable = PageRequest.of(0, 100); List likeList = concertRepository.getLikedConcertsList(pageable,user.getId()); + if(likeList.isEmpty()) return new ArrayList<>(); // 좋아요 한 공연이 없을 경우 빈 공연 반환 Map weightedBitsMap = new HashMap<>(); Set idSet = new HashSet<>(); for(ConcertItem item : likeList){ From 34e077cef62a9643f303d84fd7416a5a482e4034 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 11:31:32 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix=20:=20=EA=B3=B5=EC=97=B0=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B0=B1=EC=8B=A0=EC=8B=9C=EC=97=90=EB=8F=84=20fin?= =?UTF-8?q?ally=20=ED=95=B4=EC=A0=9C=20=EB=8F=84=EC=9E=85,=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EC=8B=9C=20catch=EC=97=90=EC=84=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0=EB=A1=9D.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concerts/service/KopisApiService.java | 146 +++++++++++------- .../global/scheduler/ConcertScheduler.java | 2 +- 2 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java index d18decb6..33d343e7 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java @@ -53,6 +53,8 @@ public class KopisApiService { private final ConcertRedisRepository concertRedisRepository; + private final ConcertService concertService; + private final ArtistRepository artistRepository; private final ConcertArtistRepository concertArtistRepository; @@ -73,7 +75,8 @@ public KopisApiService( ConcertRedisRepository concertRedisRepository, ConcertKopisApiLogService kopisApiLogService, ArtistRepository artistRepository, - ConcertArtistRepository concertArtistRepository + ConcertArtistRepository concertArtistRepository, + ConcertService concertService ) { this.concertRepository = concertRepository; this.placeRepository = placeRepository; @@ -83,6 +86,7 @@ public KopisApiService( this.concertKopisApiLogService = kopisApiLogService; this.artistRepository = artistRepository; this.concertArtistRepository = concertArtistRepository; + this.concertService = concertService; this.restClient = RestClient.builder() .baseUrl("https://kopis.or.kr/openApi/restful") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -136,6 +140,7 @@ public void setConcertsList() throws InterruptedException { // 한국어 실명 기만 artistMap 가져오기 Map artistMap = getKoNameArtistMap(); + // 개별 공연 항목에 대한 상세 데이터 저장 for(int i = index.intValue(); i < totalConcertsList.size(); i++) { ConcertListElement concertListElement = totalConcertsList.get(i); // API에서 공연 상세 가져오기 @@ -167,7 +172,10 @@ public void setConcertsList() throws InterruptedException { long endNs = System.currentTimeMillis(); long durationSec = ((endNs - startNs) / 1000); log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." ); + // 캐시 초기화 cacheClear(); + // 자동 완성 검색어 재설정 + resetSearchKeyword(); } catch (Exception e) { log.error("개별 공연 세부 내용 저장 도중 오류 발생"); log.error("오류 내용 : " + e.getMessage()); @@ -195,73 +203,73 @@ public SetResultResponse updateConcertData() throws InterruptedException { LocalDate edate = LocalDate.now().plusYears(1); SetResultResponse setResultResponse = new SetResultResponse(); - - ConcertListResponse plr; - List totalConcertsList = new ArrayList<>(); Map concertPlaceMap = new HashMap<>(); - int page = 1; - while (true) { - plr = getConcertListResponse(serviceKey, sdate, edate, page, lastUpdatedDate); - page++; - if (plr.getConcertList() == null) break; - for (ConcertListElement p : plr.getConcertList()) { - totalConcertsList.add(p); - } - Thread.sleep(200); - } - log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장"); + try { + // API에서 공연 목록 가져오기 + List totalConcertsList = getConcertListElements(sdate, edate, lastUpdatedDate); + log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장"); - // artist map 가져오기 - Map artistMap = getKoNameArtistMap(); - for (ConcertListElement performanceListElement : totalConcertsList) { - ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId()); - ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail(); - log.info("concert detail: " + concertDetailResponse.getConcertDetail()); + // artist map 가져오기 + Map artistMap = getKoNameArtistMap(); - // 콘서트 위치 탐색 또는 추가 - String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id(); - ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey); + // 개별 공연 항목에 대한 상세 데이터 저장 + for (ConcertListElement performanceListElement : totalConcertsList) { + ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId()); + ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail(); + log.info("concert detail: " + concertDetailResponse.getConcertDetail()); - // 콘서트 저장 - Concert concert = concertRepository.getConcertByApiConcertId(concertDetail.getApiConcertId()); + // 콘서트 위치 탐색 또는 추가 + String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id(); + ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey); - if (concert == null) { - // 새 공연 저장 - Concert savedConcert = saveConcert(concertPlace,concertDetail); - // 공연 예매처 저장 - setResultResponse.addedTicketOfficeAccumulator(saveConcertTicketOffice(concertDetail, savedConcert)); - // 공연 이미지 저장 - setResultResponse.addedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert)); - // 공연 아티스트 저장 - setConcertArtist(artistMap,concertDetail,savedConcert); - setResultResponse.addConcerts(); - } else { - // 공연 데이터 갱신 후 저장 - Concert savedConcert = updateConcert(concert, concertPlace, concertDetail); - // 기존에 저장되어 있던 연관 테이블 데이터 삭제 - ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId()); - imageRepository.deleteByConcertId(savedConcert.getConcertId()); - // 갱신된 데이터로 연관 테이블 저장 - setResultResponse.updatedTicketOfficesAccumulator(saveConcertTicketOffice(concertDetail, savedConcert)); - setResultResponse.updatedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert)); - setResultResponse.addUpdatedConcerts(); + // 콘서트 저장 + Concert concert = concertRepository.getConcertByApiConcertId(concertDetail.getApiConcertId()); + + if (concert == null) { + // 새 공연 저장 + Concert savedConcert = saveConcert(concertPlace,concertDetail); + // 공연 예매처 저장 + setResultResponse.addedTicketOfficeAccumulator(saveConcertTicketOffice(concertDetail, savedConcert)); + // 공연 이미지 저장 + setResultResponse.addedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert)); + // 공연 아티스트 저장 + setConcertArtist(artistMap,concertDetail,savedConcert); + setResultResponse.addConcerts(); + } else { + // 공연 데이터 갱신 후 저장 + Concert savedConcert = updateConcert(concert, concertPlace, concertDetail); + // 기존에 저장되어 있던 연관 테이블 데이터 삭제 + ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId()); + imageRepository.deleteByConcertId(savedConcert.getConcertId()); + // 갱신된 데이터로 연관 테이블 저장 + setResultResponse.updatedTicketOfficesAccumulator(saveConcertTicketOffice(concertDetail, savedConcert)); + setResultResponse.updatedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert)); + setResultResponse.addUpdatedConcerts(); + } + + Thread.sleep(300); } - Thread.sleep(300); + // 갱신 후 업데이트 시간 저장 + concertKopisApiLogService.saveSuccessLog("save","공연 데이터 업데이트 완료",0L); + // 락 해제 + concertRedisRepository.unlockSave(key); + // 이전 캐시 데이터 삭제 + cacheClear(); + // 자동 완성 검색어 재설정 + resetSearchKeyword(); + return setResultResponse; + } catch (Exception e) { + log.error("공연 정보 갱신 도중 오류 발생 : " +e.getMessage()); + concertKopisApiLogService.saveErrorLog("save",e,0L); + } finally { + concertRedisRepository.unlockSave(key); } - - // 갱신 후 업데이트 시간 저장 - concertKopisApiLogService.saveSuccessLog("save","공연 데이터 업데이트 완료",0L); - // 락 해제 - concertRedisRepository.unlockSave(key); - // 이전 캐시 데이터 삭제 - cacheClear(); return setResultResponse; } - - + // concertId의 공연만 API에서 갱신 @Transactional public void concertUpdateByKopisApi(Long concertId){ // 해당 콘서트 ID로 콘서트 객체 찾기 @@ -290,11 +298,10 @@ public void concertUpdateByKopisApi(Long concertId){ // 새로 받아온 예매처, 이미지 데이터 저장 saveConcertTicketOffice(concertDetail, savedConcert); saveConcertImages(concertDetail, savedConcert); - - } + // API에서 모든 공연 목록 가져오기 private List getAllConcertsListFromKopisAPI(int page) throws InterruptedException { List totalConcertsList = new ArrayList<>(); while (true) { @@ -311,6 +318,23 @@ private List getAllConcertsListFromKopisAPI(int page) throws return totalConcertsList; } + // API에서 이전 업데이트 날짜 이후로 변경된 공연 목록 가져오기 + private List getConcertListElements(LocalDate sdate, LocalDate edate, LocalDate lastUpdatedDate) throws InterruptedException { + List totalConcertsList = new ArrayList<>(); + int page = 1; + while (true) { + ConcertListResponse plr = getConcertListResponse(serviceKey, sdate, edate, page, lastUpdatedDate); + page++; + if (plr.getConcertList() == null) break; + for (ConcertListElement p : plr.getConcertList()) { + totalConcertsList.add(p); + } + Thread.sleep(120); + } + return totalConcertsList; + } + + // 공연 데이터 저장 private Concert saveConcert(ConcertPlace concertPlace, ConcertDetailElement concertDetail) { TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice()); @@ -427,6 +451,12 @@ private void cacheClear() { concertRedisRepository.deleteTotalConcertsCount(ListSort.VIEW); } + // 캐시에 저장된 자동완성 단어들을 삭제하고 다시 설정합니다. + private void resetSearchKeyword() { + concertService.resetAutoComplete(); + concertService.setAutoComplete(); + } + public ConcertListResponse getConcertsList() { return getConcertListResponse(serviceKey, sdate, edate, 1); } diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java b/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java index 2d8da0a7..59f0a610 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java @@ -24,7 +24,7 @@ public void concertUpdateSchedule() throws InterruptedException { } // 공연 관련 정보를 갱신합니다. - @Scheduled(cron = "0 0 3 * * *") + @Scheduled(cron = "0 10 3 * * *") public void concertDataUpdateSchedule() { concertService.viewCountUpdate(); concertService.resetAutoComplete(); From a4aabc46ade27c5a2762f103e60e190689f83bb0 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 11:45:30 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B6=80=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80,=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/concerts/service/KopisApiService.java | 1 + .../web7_9_codecrete_be/global/scheduler/ConcertScheduler.java | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java index 33d343e7..e27f45aa 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java @@ -264,6 +264,7 @@ public SetResultResponse updateConcertData() throws InterruptedException { log.error("공연 정보 갱신 도중 오류 발생 : " +e.getMessage()); concertKopisApiLogService.saveErrorLog("save",e,0L); } finally { + // 락 해제 concertRedisRepository.unlockSave(key); } return setResultResponse; diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java b/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java index 59f0a610..5d8f1b06 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java @@ -27,8 +27,6 @@ public void concertUpdateSchedule() throws InterruptedException { @Scheduled(cron = "0 10 3 * * *") public void concertDataUpdateSchedule() { concertService.viewCountUpdate(); - concertService.resetAutoComplete(); - concertService.setAutoComplete(); } // 이메일 알림을 전송합니다. From c9d7a1f0f9ae8da3eef23a4843eacbf88db8d9c0 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 12:28:06 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=EC=96=B4=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=95=88=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concerts/repository/ConcertRedisRepository.java | 12 +++++------- .../repository/ConcertSearchRedisTemplate.java | 7 ++++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java index 3c30eec2..5b2ffb42 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java @@ -122,14 +122,12 @@ public Map getCachedViewCountMap() { // 공연의 조회수 조회 public Long getCachedViewCount(Long concertId) { - Long viewCount = Long.valueOf(redisTemplate.opsForHash() - .get( - CONCERTS_VIEW_COUNTS, - concertId.toString() - ) + return Long.valueOf(Objects.requireNonNull(redisTemplate.opsForHash() + .get( + CONCERTS_VIEW_COUNTS, + concertId.toString() + )) .toString()); - - return viewCount; } // 캐시된 조회수 삭제 diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java index 273b1c07..230345d4 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java @@ -62,9 +62,10 @@ public List getAutoCompleteWord(String keyword, int start, int public void deleteAutoCompleteWords() { Set keys = redisTemplate.keys("index:*"); - Set datas = redisTemplate.keys("data:*"); - if (keys != null || !keys.isEmpty()) redisTemplate.delete(keys); - if (datas != null || !keys.isEmpty()) redisTemplate.delete(datas); + Set concertIdKeys = redisTemplate.keys("concertName:*"); + redisTemplate.delete(keys); + redisTemplate.delete(concertIdKeys); + log.info("자동 검색 키워드 삭제: " + keys.size() + "개의 키워드, " + concertIdKeys.size() + "개의 제목이 삭제되었습니다."); } public Long getConcertIdByName(String concertName) { From f3574ce319a29e6fc790ab3508280dc20ab130b7 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 6 Jan 2026 13:56:11 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=99=84?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=83=89=20=EC=97=AD=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8B=B1=20=EC=A0=80=EC=9E=A5=20=EA=B0=9C=EC=84=A0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20redis=20=EC=BA=90=EC=8B=9C=20=EC=9A=A9=EB=9F=89=20?= =?UTF-8?q?=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConcertSearchRedisTemplate.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java index 230345d4..b10781a0 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java @@ -21,8 +21,7 @@ public class ConcertSearchRedisTemplate { private final RedisTemplate redisTemplate; private static final String INDEX_KEY = "index:"; - private static final String DATE_KEY = "data:"; - private static final String CONCERT_ID_KEY = "concertName:"; + private static final String CONCERT_ID_KEY = "concertId:"; public void addAllWordsWithWeight(List weightedStrings) { // PipeLine 사용해서 한번에 처리 -> IO 시간 감소 @@ -32,18 +31,18 @@ public void addAllWordsWithWeight(List weightedStrings) { String word = weightedString.getWord(); double score = weightedString.getScore(); - // 개별 문자들에 대한 키-값 설정 - connection.commands().set((CONCERT_ID_KEY + word).getBytes(StandardCharsets.UTF_8), id.getBytes(StandardCharsets.UTF_8)); + // 검색 결과를 ID - 제목 쌍으로 저장 + connection.commands().set((CONCERT_ID_KEY + id).getBytes(StandardCharsets.UTF_8),word.getBytes(StandardCharsets.UTF_8)); + // 역 인덱싱 for(int i = 0 ;i weightedStrings) { public List getAutoCompleteWord(String keyword, int start, int end) { Set results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, start, end); List resultList = new ArrayList<>(results); - return resultList.stream().map(name ->{ - Long id = Long.valueOf(redisTemplate.opsForValue().get(CONCERT_ID_KEY + name)); - return new AutoCompleteItem(name,id); + return resultList.stream().map(id ->{ + String name = redisTemplate.opsForValue().get(CONCERT_ID_KEY + id); + return new AutoCompleteItem(name,Long.valueOf(id)); }).toList(); } public void deleteAutoCompleteWords() { Set keys = redisTemplate.keys("index:*"); - Set concertIdKeys = redisTemplate.keys("concertName:*"); + Set concertIdKeys = redisTemplate.keys("concertId:*"); redisTemplate.delete(keys); redisTemplate.delete(concertIdKeys); log.info("자동 검색 키워드 삭제: " + keys.size() + "개의 키워드, " + concertIdKeys.size() + "개의 제목이 삭제되었습니다.");