Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,18 @@ public RsData<String> sendTicketingEmail(){
return RsData.success(resultMessage,null);
}

@Operation(summary = "자동 검색어를 세팅합니다.", description = "검색어 자동완성을 위해 필요한 데이터를 저장합니다.")
@PostMapping("autoSet")
public RsData<Void> autoCompleteSetConcert(){
concertService.setAutoComplete();
return RsData.success(null);
}

@Operation(summary = "세팅된 자동 검색어를 삭제합니다.", description = "검색어 자동 완성을 위해 세팅된 데이터를 삭제합니다.")
@PostMapping("autoDelete")
public RsData<Void> autoDeleteConcert(){
concertService.resetAutoComplete();
return RsData.success(null);
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -201,12 +189,41 @@ public RsData<PlaceDetailResponse> placeDetail(
@Schema(description = """
<h3>조회 기준이 되는 concertId입니다.</h3>
<hr/>
DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를조회합니다. <br/>
DB에 저장되어 있는 공연의 ID 값을 기준으로 해당 공연의 공연장 상세 정보를 조회합니다. <br/>
<strong>?concertId={concertId}</strong> 로 값을 넘기시면 됩니다.
""")
long concertId
){
return RsData.success(concertService.getConcertPlaceDetail(concertId));
}

@Operation(summary = "검색어 자동완성", description = "주어진 문자열을 가지고 있는 결과를 조회합니다.")
@GetMapping("autoComplete")
public RsData<List<AutoCompleteItem>> autoCompleteConcert(
@RequestParam
@Schema(description = """
<h3>검색어 입니다.</h3>
<hr/>
메모리에 캐싱되어 있는 공연의 정보들을 검색하고 표시합니다. <br/>
검색 결과는 조회수 순으로 나옵니다.
""")
String keyword,
@RequestParam
@Schema(description = """
<h3>검색 시작 인덱스입니다.</h3>
<hr/>
결과 목록 중 start에 입력한 번호의 결과부터 데이터가 나옵니다.<br/>
""")
int start,
@RequestParam
@Schema(description = """
<h3>검색 종료 인덱스입니다.</h3>
<hr/>
end에 입력한 번호까지 데이터가 나옵니다.<br/>
""")
int end
){
return RsData.success(concertService.autoCompleteSerch(keyword,start,end));
Comment thread
Creamcheesepie marked this conversation as resolved.
Outdated
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}


}
Original file line number Diff line number Diff line change
@@ -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<String,String> 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<WeightedString> 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<word.length();i++){
for(int j = i+1;j<= word.length();j++ ){
String subWord = word.substring(i,j);

// 공백은 검색어 인덱스에서 제외
if(subWord.isBlank()) continue;

byte[] indexKey = (INDEX_KEY + subWord).getBytes(StandardCharsets.UTF_8);
connection.zAdd(indexKey,score,word.getBytes(StandardCharsets.UTF_8));
}
}
}
return null;
});
}

public List<AutoCompleteItem> getAutoCompleteWord(String keyword, int start, int end) {
Set<String> results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, 0, 9);
List<String> 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<String> keys = redisTemplate.keys("index:*");
Set<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

@Slf4j
@Service
@EnableScheduling
@RequiredArgsConstructor
public class ConcertNotifyService {
private final UserRepository userRepository;
Expand Down Expand Up @@ -106,7 +105,6 @@ private Map<String, List<Long>> getSendingEmailFromLikeUser(List<Concert> concer
return emailMap;
}

@Scheduled(cron = "0 0 9 * * *")
public String sendTodayTicketingConcertsNotifyingEmail() {
List<Concert> concerts = getTodayTicketingConcerts();
// 빠른 조회를 위해 Map으로 변환
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -27,7 +25,6 @@
@Slf4j
@Service
@RequiredArgsConstructor
@EnableScheduling
public class ConcertService {
private final ConcertRepository concertRepository;

Expand All @@ -41,6 +38,8 @@ public class ConcertService {

private final ConcertRedisRepository concertRedisRepository;

private final ConcertSearchRedisTemplate concertSearchRedisTemplate;

// 공연 목록 조회
public List<ConcertItem> getConcertsList(Pageable pageable, ListSort sort) {
List<ConcertItem> concertItems;
Expand Down Expand Up @@ -78,6 +77,26 @@ public List<ConcertItem> getConcertListByKeyword(String keyword, Pageable pageab
return concertRepository.getConcertItemsByKeyword(keyword, pageable);
}

// 자동완성
public List<AutoCompleteItem> autoCompleteSerch(String keyword, int start, int end) {
return concertSearchRedisTemplate.getAutoCompleteWord(keyword, start, end);
}

// 자동완성 초기화
public void resetAutoComplete(){
concertSearchRedisTemplate.deleteAutoCompleteWords();
}

// 자동완성 단어저장 v2
public void setAutoComplete(){
List<Concert> concerts = concertRepository.findAll();
List<WeightedString> weightedStrings = concerts.stream()
.map(WeightedString::new)
.toList();
concertSearchRedisTemplate.addAllWordsWithWeight(weightedStrings);
}


// 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지
@Transactional
public ConcertDetailResponse getConcertDetail(long concertId) {
Expand All @@ -100,7 +119,6 @@ public ConcertDetailResponse getConcertDetail(long concertId) {

// 조회수 갱신
@Transactional
@Scheduled(cron = "0 0 5 * * * ")
public void viewCountUpdate(){
Map<Long,Integer> viewCountMap = concertRedisRepository.getViewCountMap();
if(viewCountMap == null || viewCountMap.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
@Slf4j
@Service
@EnableAsync
@EnableScheduling
public class KopisApiService {
// 공연예술통합 전산망 조회를 위한 서비스 클래스입니다.
private final ConcertRepository concertRepository;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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();
concertService.setAutoComplete();
}

// 이메일 알림을 전송합니다.
@Scheduled(cron = "0 0 9 * * *")
public void notificationSendSchedule() {
concertNotifyService.sendTodayTicketingConcertsNotifyingEmail();
}

}