From 7ebee07a08e39ad0e498b240ae8fa86c606f44ea Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Mon, 22 Dec 2025 17:32:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=99=84?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C,=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConcertController.java | 21 ++++++++ .../ConcertSearchRedisTemplate.java | 50 +++++++++++++++++++ .../concerts/service/ConcertService.java | 31 +++++++++++- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java 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 d518fc5f..2e124243 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 @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.sql.ast.tree.expression.Summarization; import org.springdoc.core.converters.models.PageableAsQueryParam; +import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -209,4 +210,24 @@ public RsData placeDetail( return RsData.success(concertService.getConcertPlaceDetail(concertId)); } + @PostMapping("autoSet") + public RsData autoSetConcert(){ + concertService.saveTitles(); + return RsData.success(null); + } + + @GetMapping("autoComplete") + public RsData> autoCompleteConcert( + @RequestParam String keyword, + @RequestParam int start, + @RequestParam int end + ){ + return RsData.success(concertService.autoTest(keyword,start,end)); + } + + @PostMapping("autoDelete") + public RsData autoDeleteConcert(){ + concertService.resetAutoComplete(); + return RsData.success(null); + } } 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 new file mode 100644 index 00000000..0abfb431 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java @@ -0,0 +1,50 @@ +package com.back.web7_9_codecrete_be.domain.concerts.repository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class ConcertSearchRedisTemplate { + private final RedisTemplate redisTemplate; + + private static final String INDEX_KEY = "index:"; + private static final String DATE_KEY = "data:"; + + + public void addAllAutoCompleteWord(List autoCompleteWords) { + for (String title : autoCompleteWords) { + redisTemplate.opsForValue().set(DATE_KEY + title, title); + + for(int i = 0 ;i getAutoCompleteWord(String keyword,int start, int end) { + Set results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, start, end); + + return results != null ? new ArrayList<>(results) : Collections.emptyList(); + } + + 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); + } +} 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 88ea1476..f3b62d17 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 @@ -6,6 +6,7 @@ import com.back.web7_9_codecrete_be.domain.concerts.entity.*; import com.back.web7_9_codecrete_be.domain.concerts.repository.*; import com.back.web7_9_codecrete_be.domain.users.entity.User; +import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository; import com.back.web7_9_codecrete_be.global.error.code.ConcertErrorCode; import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; @@ -20,6 +21,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -40,6 +42,8 @@ public class ConcertService { private final ConcertRedisRepository concertRedisRepository; + private final ConcertSearchRedisTemplate concertSearchRedisTemplate; + // 공연 목록 조회 public List getConcertsList(Pageable pageable, ListSort sort) { List concertItems; @@ -77,6 +81,29 @@ public List getConcertListByKeyword(String keyword, Pageable pageab return concertRepository.getConcertItemsByKeyword(keyword, pageable); } + // 자동완성 + public List autoTest(String keyword,int start, int end) { + return concertSearchRedisTemplate.getAutoCompleteWord(keyword, start, end); + } + + // 자동완성 초기화 + public void resetAutoComplete(){ + concertSearchRedisTemplate.deleteAutoCompleteWords(); + } + + // 자동완성 단어 저장 + public void saveTitles(){ + List concerts = concertRepository.findAll(); + List names = concerts.stream().map(concert -> concert.getName()).toList(); + List eachWords = new ArrayList<>(); + for (String name : names) { + String[] words = name.split(" "); + eachWords.addAll(Arrays.stream(words).toList()); + } + concertSearchRedisTemplate.addAllAutoCompleteWord(names); + } + + // 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지 @Transactional public ConcertDetailResponse getConcertDetail(long concertId) { @@ -121,14 +148,14 @@ public Long getTotalConcertsCount() { return result; } - // todo : 티켓팅 공연 개수 조회 + // 티켓팅 공연 개수 조회 public Long getTotalTicketingConcertsCount() { Long result = concertRedisRepository.getTotalConcertsCount(ListSort.TICKETING); if(result == -1) result = concertRedisRepository.saveTotalConcertsCount(concertRepository.countTicketingConcertsFromLocalDateTime(LocalDateTime.of(LocalDate.now(), LocalTime.MIN)), ListSort.TICKETING); return result; } - // todo : 좋아요한 공연 개수 조회 + // 좋아요한 공연 개수 조회 public Long getTotalLikedConcertsCount(User user) { Long result = concertRedisRepository.getUserLikedCount(user); if(result == -1) result = concertRedisRepository.saveUserLikedCount(user,concertLikeRepository.countByUser(user)); From 684cc8a635f01dfdbd6b9d29b4270133a872556a Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 23 Dec 2025 10:06:17 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=97=B0=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=20=EC=9E=90=EB=8F=99=20=EC=99=84=EC=84=B1=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=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 --- .../controller/ConcertAdminController.java | 14 ++++++++++++++ .../concerts/controller/ConcertController.java | 12 +----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index e8321344..0cde5590 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java @@ -136,4 +136,18 @@ public RsData sendTicketingEmail(){ return RsData.success(resultMessage,null); } + @Operation(summary = "자동 검색어를 세팅합니다.", description = "검색어 자동완성을 위해 필요한 데이터를 저장합니다.") + @PostMapping("autoSet") + public RsData autoSetConcert(){ + concertService.saveTitles(); + return RsData.success(null); + } + + @Operation(summary = "세팅된 자동 검색어를 삭제합니다.", description = "검색어 자동 완성을 위해 세팅된 데이터를 삭제합니다.") + @PostMapping("autoDelete") + public RsData autoDeleteConcert(){ + concertService.resetAutoComplete(); + return RsData.success(null); + } + } 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 2e124243..93f910df 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 @@ -202,7 +202,7 @@ public RsData placeDetail( @Schema(description = """

조회 기준이 되는 concertId입니다.


- DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를조회합니다.
+ DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를 조회합니다.
?concertId={concertId} 로 값을 넘기시면 됩니다. """) long concertId @@ -210,11 +210,6 @@ public RsData placeDetail( return RsData.success(concertService.getConcertPlaceDetail(concertId)); } - @PostMapping("autoSet") - public RsData autoSetConcert(){ - concertService.saveTitles(); - return RsData.success(null); - } @GetMapping("autoComplete") public RsData> autoCompleteConcert( @@ -225,9 +220,4 @@ public RsData> autoCompleteConcert( return RsData.success(concertService.autoTest(keyword,start,end)); } - @PostMapping("autoDelete") - public RsData autoDeleteConcert(){ - concertService.resetAutoComplete(); - return RsData.success(null); - } } From ce8639c12a1316234fffd1842445bad9ef763876 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 23 Dec 2025 17:12:29 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=EC=96=B4=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20->=20pipeline=20=EC=9D=B4=EC=9A=A9=201?= =?UTF-8?q?=EB=B2=88=EC=9D=98=20IO=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=A8,?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=EC=B6=9C=EB=A0=A5=EC=8B=9C=20ID=20?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=99=EC=9D=B4=20=EB=8B=B4=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20redis=EC=97=90=20ID=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B2=B0=EA=B3=BC=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=20=EC=A0=9C=EB=AA=A9=20+=20ID=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConcertAdminController.java | 4 +- .../controller/ConcertController.java | 44 +++++++++------- .../dto/concert/AutoCompleteItem.java | 14 ++++++ .../concerts/dto/concert/WeightedString.java | 24 +++++++++ .../ConcertSearchRedisTemplate.java | 50 ++++++++++++++++--- .../service/ConcertNotifyService.java | 2 - .../concerts/service/ConcertService.java | 28 +++++------ .../concerts/service/KopisApiService.java | 5 +- .../global/scheduler/ConcertScheduler.java | 40 +++++++++++++++ 9 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/AutoCompleteItem.java create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/WeightedString.java create mode 100644 src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index 0cde5590..5a54d78e 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java @@ -138,8 +138,8 @@ public RsData sendTicketingEmail(){ @Operation(summary = "자동 검색어를 세팅합니다.", description = "검색어 자동완성을 위해 필요한 데이터를 저장합니다.") @PostMapping("autoSet") - public RsData autoSetConcert(){ - concertService.saveTitles(); + public RsData autoCompleteSetConcert(){ + concertService.saveTitles2(); return RsData.success(null); } 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 93f910df..864c598b 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 @@ -1,14 +1,9 @@ package com.back.web7_9_codecrete_be.domain.concerts.controller; -import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concert.ConcertListResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceDetailResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceListResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*; import com.back.web7_9_codecrete_be.domain.concerts.dto.concertPlace.PlaceDetailResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.ticketOffice.TicketOfficeElement; -import com.back.web7_9_codecrete_be.domain.concerts.entity.TicketOffice; import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertService; -import com.back.web7_9_codecrete_be.domain.concerts.service.KopisApiService; import com.back.web7_9_codecrete_be.domain.users.entity.User; import com.back.web7_9_codecrete_be.global.rq.Rq; import com.back.web7_9_codecrete_be.global.rsData.RsData; @@ -17,17 +12,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.sql.ast.tree.expression.Summarization; -import org.springdoc.core.converters.models.PageableAsQueryParam; -import org.springframework.boot.context.properties.bind.DefaultValue; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import javax.swing.*; import java.util.List; @Slf4j @@ -210,14 +197,33 @@ public RsData placeDetail( return RsData.success(concertService.getConcertPlaceDetail(concertId)); } - + @Operation(summary = "검색어 자동완성", description = "주어진 문자열을 가지고 있는 결과를 조회합니다.") @GetMapping("autoComplete") - public RsData> autoCompleteConcert( - @RequestParam String keyword, - @RequestParam int start, - @RequestParam int end + public RsData> autoCompleteConcert( + @RequestParam + @Schema(description = """ +

검색어 입니다.

+
+ 메모리에 캐싱되어 있는 공연의 정보들을 검색하고 표시합니다.
+ 검색 결과는 조회수 순으로 나옵니다. + """) + String keyword, + @RequestParam + @Schema(description = """ +

검색 시작 인덱스입니다.

+
+ 결과 목록 중 start에 입력한 번호의 결과부터 데이터가 나옵니다.
+ """) + int start, + @RequestParam + @Schema(description = """ +

검색 종료 인덱스입니다.

+
+ end에 입력한 번호까지 데이터가 나옵니다.
+ """) + int end ){ - return RsData.success(concertService.autoTest(keyword,start,end)); + return RsData.success(concertService.autoCompleteSerch(keyword,start,end)); } } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/AutoCompleteItem.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/AutoCompleteItem.java new file mode 100644 index 00000000..61114f0f --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/AutoCompleteItem.java @@ -0,0 +1,14 @@ +package com.back.web7_9_codecrete_be.domain.concerts.dto.concert; + +import lombok.Getter; + +@Getter +public class AutoCompleteItem { + private String name; + private Long Id; + + public AutoCompleteItem(String name, Long id) { + this.name = name; + Id = id; + } +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/WeightedString.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/WeightedString.java new file mode 100644 index 00000000..fbebff14 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/WeightedString.java @@ -0,0 +1,24 @@ +package com.back.web7_9_codecrete_be.domain.concerts.dto.concert; + +import com.back.web7_9_codecrete_be.domain.concerts.entity.Concert; +import lombok.Getter; + +@Getter +public class WeightedString { + private Long concertId; + private String word; + private double score; + + public WeightedString(String word, int score) { + this.word = word; + this.score = score; + } + + public WeightedString(Concert concert) { + this.concertId = concert.getConcertId(); + this.word = concert.getName(); + this.score = ((double)concert.getViewCount()) * 0.1; + } + + +} 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 0abfb431..3578fe01 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 @@ -1,12 +1,14 @@ package com.back.web7_9_codecrete_be.domain.concerts.repository; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.AutoCompleteItem; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.WeightedString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Range; -import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -20,7 +22,7 @@ public class ConcertSearchRedisTemplate { private static final String INDEX_KEY = "index:"; private static final String DATE_KEY = "data:"; - + private static final String CONCERT_ID_KEY = "concertName:"; public void addAllAutoCompleteWord(List autoCompleteWords) { for (String title : autoCompleteWords) { @@ -35,10 +37,41 @@ public void addAllAutoCompleteWord(List autoCompleteWords) { } } - public List getAutoCompleteWord(String keyword,int start, int end) { - Set results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, start, end); - return results != null ? new ArrayList<>(results) : Collections.emptyList(); + public void addAllWordsWithWeight(List weightedStrings) { + // PipeLine 사용해서 한번에 처리 -> IO 시간 감소 + redisTemplate.executePipelined((RedisCallback) connection ->{ + for (WeightedString weightedString : weightedStrings) { + String id = String.valueOf(weightedString.getConcertId()); + String word = weightedString.getWord(); + double score = weightedString.getScore(); + + // 개별 문자들에 대한 키-값 설정 + connection.commands().set((CONCERT_ID_KEY + word).getBytes(StandardCharsets.UTF_8), id.getBytes(StandardCharsets.UTF_8)); + + for(int i = 0 ;i getAutoCompleteWord(String keyword, int start, int end) { + Set results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, 0, 9); + 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); + }).toList(); } public void deleteAutoCompleteWords() { @@ -47,4 +80,9 @@ public void deleteAutoCompleteWords() { if (keys != null || !keys.isEmpty()) redisTemplate.delete(keys); if (datas != null || !keys.isEmpty()) redisTemplate.delete(datas); } + + public Long getConcertIdByName(String concertName) { + String raw = redisTemplate.opsForValue().get(CONCERT_ID_KEY + concertName); + return raw == null ? null : Long.parseLong(raw); + } } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertNotifyService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertNotifyService.java index 328be4b4..163bd6ec 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertNotifyService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertNotifyService.java @@ -26,7 +26,6 @@ @Slf4j @Service -@EnableScheduling @RequiredArgsConstructor public class ConcertNotifyService { private final UserRepository userRepository; @@ -106,7 +105,6 @@ private Map> getSendingEmailFromLikeUser(List concer return emailMap; } - @Scheduled(cron = "0 0 9 * * *") public String sendTodayTicketingConcertsNotifyingEmail() { List concerts = getTodayTicketingConcerts(); // 빠른 조회를 위해 Map으로 변환 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 bba5cd6b..c3b71504 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 @@ -1,20 +1,17 @@ package com.back.web7_9_codecrete_be.domain.concerts.service; import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.WeightedString; import com.back.web7_9_codecrete_be.domain.concerts.dto.concertPlace.PlaceDetailResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.ticketOffice.TicketOfficeElement; import com.back.web7_9_codecrete_be.domain.concerts.entity.*; import com.back.web7_9_codecrete_be.domain.concerts.repository.*; import com.back.web7_9_codecrete_be.domain.users.entity.User; -import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository; import com.back.web7_9_codecrete_be.global.error.code.ConcertErrorCode; -import com.back.web7_9_codecrete_be.global.error.code.ErrorCode; import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,14 +19,12 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @Slf4j @Service @RequiredArgsConstructor -@EnableScheduling public class ConcertService { private final ConcertRepository concertRepository; @@ -83,7 +78,7 @@ public List getConcertListByKeyword(String keyword, Pageable pageab } // 자동완성 - public List autoTest(String keyword,int start, int end) { + public List autoCompleteSerch(String keyword, int start, int end) { return concertSearchRedisTemplate.getAutoCompleteWord(keyword, start, end); } @@ -95,15 +90,21 @@ public void resetAutoComplete(){ // 자동완성 단어 저장 public void saveTitles(){ List concerts = concertRepository.findAll(); - List names = concerts.stream().map(concert -> concert.getName()).toList(); - List eachWords = new ArrayList<>(); - for (String name : names) { - String[] words = name.split(" "); - eachWords.addAll(Arrays.stream(words).toList()); - } + List names = concerts.stream() + .map(Concert::getName) + .toList(); concertSearchRedisTemplate.addAllAutoCompleteWord(names); } + // 자동완성 단어저장 v2 + public void saveTitles2(){ + List concerts = concertRepository.findAll(); + List weightedStrings = concerts.stream() + .map(WeightedString::new) + .toList(); + concertSearchRedisTemplate.addAllWordsWithWeight(weightedStrings); + } + // 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지 @Transactional @@ -127,7 +128,6 @@ public ConcertDetailResponse getConcertDetail(long concertId) { // 조회수 갱신 @Transactional - @Scheduled(cron = "0 0 5 * * * ") public void viewCountUpdate(){ Map viewCountMap = concertRedisRepository.getViewCountMap(); if(viewCountMap == null || viewCountMap.isEmpty()) { 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 70015a6a..491c5f4c 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 @@ -32,7 +32,6 @@ @Slf4j @Service @EnableAsync -@EnableScheduling public class KopisApiService { // 공연예술통합 전산망 조회를 위한 서비스 클래스입니다. private final ConcertRepository concertRepository; @@ -174,6 +173,7 @@ public void setConcertsList() throws InterruptedException { } catch (Exception e) { log.error("개별 공연 세부 내용 저장 도중 오류 발생"); log.error("오류 내용 : " + e.getMessage()); + e.printStackTrace(); return ; } ConcertUpdateTime concertUpdateTime = new ConcertUpdateTime(now); @@ -185,10 +185,7 @@ public void setConcertsList() throws InterruptedException { concertRedisRepository.unlockSave(key); } - - // 매주 월요일 새벽 2시 기준으로 데이터 갱신 @Transactional - @Scheduled(cron = "0 0 2 * * Mon") public SetResultResponse updateConcertData() throws InterruptedException { String key = "init"; String value = concertRedisRepository.lockGet(key); 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 new file mode 100644 index 00000000..ab5deca9 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java @@ -0,0 +1,40 @@ +package com.back.web7_9_codecrete_be.global.scheduler; + +import com.back.web7_9_codecrete_be.domain.concerts.controller.ConcertController; +import com.back.web7_9_codecrete_be.domain.concerts.repository.ConcertSearchRedisTemplate; +import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertNotifyService; +import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertService; +import com.back.web7_9_codecrete_be.domain.concerts.service.KopisApiService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@EnableScheduling +@Component +@RequiredArgsConstructor +public class ConcertScheduler { + private final ConcertService concertService; + private final KopisApiService kopisApiService; + private final ConcertNotifyService concertNotifyService; + + // 공연 데이터 업데이트를 진행합니다. + @Scheduled(cron = "0 0 2 * * *") + public void concertUpdateSchedule() throws InterruptedException { + kopisApiService.updateConcertData(); + } + + // 공연 관련 정보를 갱신합니다. + @Scheduled(cron = "0 0 3 * * *") + public void concertDataUpdateSchedule() { + concertService.viewCountUpdate(); + concertService.resetAutoComplete(); + } + + // 이메일 알림을 전송합니다. + @Scheduled(cron = "0 0 9 * * *") + public void notificationSendSchedule() { + concertNotifyService.sendTodayTicketingConcertsNotifyingEmail(); + } + +} From 4c53b0b28ae90dac9b88def3920a8960aca138de Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Tue, 23 Dec 2025 17:21:23 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EA=B5=B0=EB=8D=94=EB=8D=94?= =?UTF-8?q?=EA=B8=B0=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConcertAdminController.java | 2 +- .../repository/ConcertSearchRedisTemplate.java | 14 -------------- .../domain/concerts/service/ConcertService.java | 11 +---------- .../global/scheduler/ConcertScheduler.java | 1 + 4 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index 5a54d78e..0be5b755 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java @@ -139,7 +139,7 @@ public RsData sendTicketingEmail(){ @Operation(summary = "자동 검색어를 세팅합니다.", description = "검색어 자동완성을 위해 필요한 데이터를 저장합니다.") @PostMapping("autoSet") public RsData autoCompleteSetConcert(){ - concertService.saveTitles2(); + concertService.setAutoComplete(); return RsData.success(null); } 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 3578fe01..929a1fd9 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 @@ -24,20 +24,6 @@ public class ConcertSearchRedisTemplate { private static final String DATE_KEY = "data:"; private static final String CONCERT_ID_KEY = "concertName:"; - public void addAllAutoCompleteWord(List autoCompleteWords) { - for (String title : autoCompleteWords) { - redisTemplate.opsForValue().set(DATE_KEY + title, title); - - for(int i = 0 ;i weightedStrings) { // PipeLine 사용해서 한번에 처리 -> IO 시간 감소 redisTemplate.executePipelined((RedisCallback) connection ->{ 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 c3b71504..af098353 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 @@ -87,17 +87,8 @@ public void resetAutoComplete(){ concertSearchRedisTemplate.deleteAutoCompleteWords(); } - // 자동완성 단어 저장 - public void saveTitles(){ - List concerts = concertRepository.findAll(); - List names = concerts.stream() - .map(Concert::getName) - .toList(); - concertSearchRedisTemplate.addAllAutoCompleteWord(names); - } - // 자동완성 단어저장 v2 - public void saveTitles2(){ + public void setAutoComplete(){ List concerts = concertRepository.findAll(); List weightedStrings = concerts.stream() .map(WeightedString::new) 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 ab5deca9..dbf5c474 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 @@ -29,6 +29,7 @@ public void concertUpdateSchedule() throws InterruptedException { public void concertDataUpdateSchedule() { concertService.viewCountUpdate(); concertService.resetAutoComplete(); + concertService.setAutoComplete(); } // 이메일 알림을 전송합니다. From 1158af76d2be98d3a7090ee3f727d168e47e0008 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Wed, 24 Dec 2025 09:31:15 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat=20:=20@EnableScheduling=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20Applictaion=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=EC=A7=80=EC=A0=90=EC=97=90=20=EC=82=BD=EC=9E=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back/web7_9_codecrete_be/Web79CodecreteBeApplication.java | 2 ++ .../web7_9_codecrete_be/global/scheduler/ConcertScheduler.java | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/Web79CodecreteBeApplication.java b/src/main/java/com/back/web7_9_codecrete_be/Web79CodecreteBeApplication.java index bdd0297e..9ba0dc06 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/Web79CodecreteBeApplication.java +++ b/src/main/java/com/back/web7_9_codecrete_be/Web79CodecreteBeApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing +@EnableScheduling @SpringBootApplication public class Web79CodecreteBeApplication { 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 dbf5c474..27a8b2fd 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 @@ -10,7 +10,6 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -@EnableScheduling @Component @RequiredArgsConstructor public class ConcertScheduler { From 9f385831413910e2d2027fc984311f0d531aad9a Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Wed, 24 Dec 2025 10:03:47 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix=20:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/concerts/controller/ConcertController.java | 2 +- .../domain/concerts/service/ConcertService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 864c598b..33ccd06c 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 @@ -223,7 +223,7 @@ public RsData> autoCompleteConcert( """) int end ){ - return RsData.success(concertService.autoCompleteSerch(keyword,start,end)); + return RsData.success(concertService.autoCompleteSearch(keyword,start,end)); } } 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 af098353..d0cb5563 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 @@ -78,7 +78,7 @@ public List getConcertListByKeyword(String keyword, Pageable pageab } // 자동완성 - public List autoCompleteSerch(String keyword, int start, int end) { + public List autoCompleteSearch(String keyword, int start, int end) { return concertSearchRedisTemplate.getAutoCompleteWord(keyword, start, end); }