Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.back.web7_9_codecrete_be.domain.concerts.controller;

import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.result.SetResultResponse;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertTicketTimeSetRequest;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertUpdateRequest;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*;
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;
Expand Down Expand Up @@ -125,6 +122,13 @@ public RsData<SetResultResponse> updateConcert() throws InterruptedException {
return RsData.success(kopisApiService.updateConcertData());
}

@Operation(summary = "공연 조회수 갱신", description = "캐시에 저장되어 있는 조회수를 DB에 반영하고 캐싱된 공연 목록을 초기화합니다.")
@PostMapping("updateConcertViewCount")
public RsData<Void> updateConcertViewCount(){
concertService.viewCountUpdate();
return RsData.success(null);
}

@Operation(summary = "알림 이메일 전송", description = "예매일이 오늘인 공연에 대해 알림 이메일을 전송합니다.")
@PostMapping("sendTicketingEmail")
public RsData<String> sendTicketingEmail(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
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.ConcertDetailResponse;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertLikeResponse;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort;
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;
Expand All @@ -20,6 +17,7 @@
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;
Expand Down Expand Up @@ -78,9 +76,30 @@ public RsData<List<ConcertItem>> getLikedConcertList(
return RsData.success(concertService.getLikedConcertsList(pageable, user));
}

@Operation(summary = "공연 총 개수 조회", description = "저장된 공연의 총 개수를 조회합니다.")
@GetMapping("totalConcertCount")
public RsData<Long> getTotalConcertCount(){
return RsData.success(concertService.getTotalConcertsCount());
}

@Operation(summary = "티켓팅 일정이 다가오는 공연의 개수 조회" ,
description = "티켓팅 일정 정보가 있고, 티켓팅 일정이 다가오는 공연의 개수를 조회합니다. <br/>list/TICKETING에 대응하는 공연의 개수입니다.")
@GetMapping("totalTicketingConcertCount")
public RsData<Long> getTotalTicketingConcertCount(){
return RsData.success(concertService.getTotalTicketingConcertsCount());
}

@Operation(summary = "좋아요 누른 공연의 개수 조회", description = "현재 로그인한 사용자가 좋아요를 누른 공연의 개수를 조회합니다.")
@GetMapping("likedConcertCount")
public RsData<Long> getLikedConcertCount(){
User user = rq.getUser();
return RsData.success(concertService.getTotalLikedConcertsCount(user));
}


@Operation(summary = "공연 상세 조회", description = "공연에 대한 상세 목록을 조회합니다.")
@GetMapping("concertDetail")
public ConcertDetailResponse getConcertDetail(
public RsData<ConcertDetailResponse> getConcertDetail(
@RequestParam
@Schema(description = """
<h3>조회 기준이 되는 concertId입니다.</h3>
Expand All @@ -90,7 +109,7 @@ public ConcertDetailResponse getConcertDetail(
""")
long concertId
) {
return concertService.getConcertDetail(concertId);
return RsData.success(concertService.getConcertDetail(concertId));
}

@Operation(summary = "공연 예매처 조회", description = "공연에 대한 예매처들을 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.back.web7_9_codecrete_be.domain.concerts.dto.concert;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down Expand Up @@ -29,12 +35,18 @@ public class ConcertDetailResponse {
@Schema(description = "콘서트 장 주소입니다.")
private String placeAddress;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@Schema(description = "콘서트 예매 시작 날짜입니다.")
private LocalDateTime ticketTime;

@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
@Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd")
private LocalDate startDate;

@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
@Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd")
private LocalDate endDate;

Expand All @@ -55,4 +67,6 @@ public class ConcertDetailResponse {

@Schema(description = "콘서트 이미지 목록입니다.")
private List<String> concertImageUrls;


}
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.back.web7_9_codecrete_be.domain.concerts.dto.concert;

import com.back.web7_9_codecrete_be.domain.concerts.entity.Concert;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
@Setter
@RequiredArgsConstructor
public class ConcertItem {

@Schema(description = "콘서트 Id입니다.")
Expand All @@ -21,12 +29,18 @@ public class ConcertItem {
@Schema(description = "콘서트 장소 이름입니다.")
private String placeName;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@Schema(description = "콘서트 예매 시작 날짜입니다.",format = "yyyy-MM-ddThh:mm:ss")
private LocalDateTime ticketTime;

@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
@Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd")
private LocalDate startDate ;

@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
@Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd")
private LocalDate endDate ;

Expand Down Expand Up @@ -59,7 +73,7 @@ public ConcertItem(Concert concert) {
this.likeCount = concert.getLikeCount();
}

public ConcertItem(long id, String name, String placeName,LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) {
public ConcertItem(long id, String name, String placeName, LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) {
this.id = id;
this.name = name;
this.placeName = placeName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,7 @@ public interface ConcertLikeRepository extends JpaRepository<ConcertLike, Long>
List<ConcertLike> getTodayConcertTicketingLikes(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);

Long countByUser(User user);
}

Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
package com.back.web7_9_codecrete_be.domain.concerts.repository;

import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort;
import com.back.web7_9_codecrete_be.domain.users.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Repository;

import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
@Repository
@RequiredArgsConstructor
public class ConcertRedisRepository {
private final RedisTemplate<String,String> redisTemplate;
private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, Object> objectRedisTemplate;

private static final String LOCK_FLAG_PREFIX = "initLoad: ";

private static final String CONCERT_DETAIL_PREFIX = "concertDetail: ";

private static final String CONCERT_LIST_PREFIX = "concertList: ";

private static final String VIEW_COUNT_MAP = "viewCountMap";

private static final String CONCERTS_COUNT_PREFIX = "totalConcertsCount: ";

private static final int HOUR = 3600;

// 최초 공연 로드 락
public void lockSave(String key, String value) {
redisTemplate.opsForValue().set(
LOCK_FLAG_PREFIX + key,
Expand All @@ -29,4 +49,160 @@ public void unlockSave(String key) {
redisTemplate.delete(LOCK_FLAG_PREFIX + key);
}

// 공연 목록 캐싱
public void listSave(ListSort sort, Pageable pageable, List<ConcertItem> list) {
String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber();
objectRedisTemplate.opsForValue().set(key, list, HOUR, TimeUnit.SECONDS);
}

// 공연 목록 가져오기
public List<ConcertItem> getConcertsList(Pageable pageable, ListSort sort) {
String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber();
Object object = objectRedisTemplate.opsForValue().get(key);
List<ConcertItem> list = (List<ConcertItem>) object;
if (list == null || list.isEmpty()) return List.of(); // null 이 아닌 empty 값 반환
return list;
}

// 캐싱된 모든 공연 목록 삭제
public void deleteAllConcertsList() {
deleteAllItemsByPREFIX(CONCERT_LIST_PREFIX);
}

// 공연 상세 캐싱
public void detailSave(long concertId, ConcertDetailResponse concertDetailResponse) {
objectRedisTemplate.opsForValue().set(
CONCERT_DETAIL_PREFIX + concertId,
concertDetailResponse,
HOUR,
TimeUnit.SECONDS
);
}

// todo : 객체 일부의 값만 바뀌는거니 해당 값만 바꿔서 저장하거나 Redis 내부의 값만 갱신할 수 있는 방법 찾기
public ConcertDetailResponse getDetail(long concertId) {
ConcertDetailResponse concertDetailResponse = (ConcertDetailResponse) objectRedisTemplate.opsForValue().get(CONCERT_DETAIL_PREFIX + concertId);
if (concertDetailResponse == null) return null;
int viewCount = concertDetailResponse.getViewCount();
viewCountSet(concertId, viewCount + 1);
concertDetailResponse.setViewCount(viewCount + 1);
detailSave(concertId, concertDetailResponse);
return concertDetailResponse;
}

// 공연 상세 삭제
public void deleteDetail(String concertId) {
redisTemplate.delete(CONCERT_DETAIL_PREFIX + concertId);
}

// 모든 공연 상세 삭제
public void deleteAllConcertDetail() {
deleteAllItemsByPREFIX(CONCERT_DETAIL_PREFIX);
}

// 조회수 처리 -> 좀 지저분한데 개선 여지 찾아보기
public int viewCountSet(long concertId, int viewCount) {
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);

if (rawMap == null) {
Map<Long, Integer> viewCountMap = new HashMap<>();
viewCountMap.put(concertId, viewCount);
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
} else {
Map<Long, Integer> viewCountMap = convertViewCountMap(rawMap);
viewCountMap.put(concertId, viewCount);
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
log.info(viewCountMap.size() + "view count size.");
}
return viewCount;
}

// 조회수 맵 조회
public Map<Long, Integer> getViewCountMap() {
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);
if (rawMap == null) return null;
objectRedisTemplate.delete(VIEW_COUNT_MAP);
return convertViewCountMap(rawMap);
}

// 조회수 맵 삭제
public void deleteViewCountMap() {
objectRedisTemplate.delete(VIEW_COUNT_MAP);
}

// String Integer 타입 맵을 Long Integer로 변환
private Map<Long, Integer> convertViewCountMap(Map<String, Integer> rawMap) {
Map<Long, Integer> viewCountMap = new HashMap<>();
for (Map.Entry<String, Integer> stringIntegerEntry : rawMap.entrySet()) {
viewCountMap.put(Long.parseLong(stringIntegerEntry.getKey()), stringIntegerEntry.getValue());
}
return viewCountMap;
}

// 해당 접두어의 모든 항목 삭제
private void deleteAllItemsByPREFIX(String prefix) {
String pattern = CONCERT_LIST_PREFIX + "*";
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> keySet = new HashSet<>();
try (Cursor<byte[]> cursor = connection.scan(options)) {
while (cursor.hasNext()) {
keySet.add(new String(cursor.next()));
}
}
return keySet;
}
);

if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
log.info("Successfully deleted %s items with prefix: %s".formatted(keys.size() + "", prefix));
} else {
log.info("no items with prefix: %s".formatted(prefix));
}
}

// 총 공연의 개수 저장
public Long saveTotalConcertsCount(Long totalConcertsCount, ListSort sort) {
redisTemplate.opsForValue().set(CONCERTS_COUNT_PREFIX + sort.name(),totalConcertsCount.toString());
return totalConcertsCount;
}

// 총 공연의 개수 조회
public Long getTotalConcertsCount(ListSort sort) {
String raw = redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name());
if (raw == null) return -1L;
else return Long.parseLong(redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name()));
}

// 총 공연의 개수 삭제
public void deleteTotalConcertsCount(ListSort sort) {
redisTemplate.delete(CONCERTS_COUNT_PREFIX+sort.name());
}

// 사용자가 좋아요를 누른 공연의 개수 조회(임시 캐시 느낌으로 짧게 저장, 조회시 시간 갱신 ~1일
public Long getUserLikedCount(User user) {
String raw = redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + user.getId());
if (raw == null) return -1L;
redisTemplate.expire(CONCERTS_COUNT_PREFIX + user.getId(), 1, TimeUnit.DAYS);
return Long.parseLong(redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + user.getId()));
}

// 사용자가 좋아요를 누른 공연의 개수 저장
public Long saveUserLikedCount(User user,Long count) {
redisTemplate.opsForValue().set(
CONCERTS_COUNT_PREFIX + user.getId(),
count.toString(),
1,
TimeUnit.DAYS
);
return count;
}

// 좋아요 누른 공연의 개수 캐시 삭제
public void deleteUserLikedCount(User user) {
redisTemplate.delete(CONCERTS_COUNT_PREFIX + user.getId());
}


}
Loading