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/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index e8321344..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 @@ -136,4 +136,18 @@ public RsData sendTicketingEmail(){ return RsData.success(resultMessage,null); } + @Operation(summary = "자동 검색어를 세팅합니다.", description = "검색어 자동완성을 위해 필요한 데이터를 저장합니다.") + @PostMapping("autoSet") + public RsData autoCompleteSetConcert(){ + concertService.setAutoComplete(); + 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 d518fc5f..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 @@ -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,16 +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.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 @@ -201,7 +189,7 @@ public RsData placeDetail( @Schema(description = """

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


- DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를조회합니다.
+ DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를 조회합니다.
?concertId={concertId} 로 값을 넘기시면 됩니다. """) long concertId @@ -209,4 +197,33 @@ public RsData placeDetail( return RsData.success(concertService.getConcertPlaceDetail(concertId)); } + @Operation(summary = "검색어 자동완성", description = "주어진 문자열을 가지고 있는 결과를 조회합니다.") + @GetMapping("autoComplete") + public RsData> autoCompleteConcert( + @RequestParam + @Schema(description = """ +

검색어 입니다.

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

검색 시작 인덱스입니다.

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

검색 종료 인덱스입니다.

+
+ end에 입력한 번호까지 데이터가 나옵니다.
+ """) + int end + ){ + return RsData.success(concertService.autoCompleteSearch(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 new file mode 100644 index 00000000..929a1fd9 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertSearchRedisTemplate.java @@ -0,0 +1,74 @@ +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.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; +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:"; + private static final String CONCERT_ID_KEY = "concertName:"; + + 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() { + 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); + } + + 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 28adc335..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 @@ -1,19 +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.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; @@ -27,7 +25,6 @@ @Slf4j @Service @RequiredArgsConstructor -@EnableScheduling public class ConcertService { private final ConcertRepository concertRepository; @@ -41,6 +38,8 @@ public class ConcertService { private final ConcertRedisRepository concertRedisRepository; + private final ConcertSearchRedisTemplate concertSearchRedisTemplate; + // 공연 목록 조회 public List getConcertsList(Pageable pageable, ListSort sort) { List concertItems; @@ -78,6 +77,26 @@ public List getConcertListByKeyword(String keyword, Pageable pageab return concertRepository.getConcertItemsByKeyword(keyword, pageable); } + // 자동완성 + public List autoCompleteSearch(String keyword, int start, int end) { + return concertSearchRedisTemplate.getAutoCompleteWord(keyword, start, end); + } + + // 자동완성 초기화 + public void resetAutoComplete(){ + concertSearchRedisTemplate.deleteAutoCompleteWords(); + } + + // 자동완성 단어저장 v2 + public void setAutoComplete(){ + List concerts = concertRepository.findAll(); + List weightedStrings = concerts.stream() + .map(WeightedString::new) + .toList(); + concertSearchRedisTemplate.addAllWordsWithWeight(weightedStrings); + } + + // 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지 @Transactional public ConcertDetailResponse getConcertDetail(long concertId) { @@ -100,7 +119,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..27a8b2fd --- /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; + +@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(); + concertService.setAutoComplete(); + } + + // 이메일 알림을 전송합니다. + @Scheduled(cron = "0 0 9 * * *") + public void notificationSendSchedule() { + concertNotifyService.sendTodayTicketingConcertsNotifyingEmail(); + } + +}