diff --git a/src/main/java/com/palettee/PaletteApplication.java b/src/main/java/com/palettee/PaletteApplication.java index 300c54f0..efe39a5f 100644 --- a/src/main/java/com/palettee/PaletteApplication.java +++ b/src/main/java/com/palettee/PaletteApplication.java @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing diff --git a/src/main/java/com/palettee/gathering/controller/GatheringController.java b/src/main/java/com/palettee/gathering/controller/GatheringController.java index bc934ed3..935c2834 100644 --- a/src/main/java/com/palettee/gathering/controller/GatheringController.java +++ b/src/main/java/com/palettee/gathering/controller/GatheringController.java @@ -3,9 +3,10 @@ import com.palettee.gathering.controller.dto.Request.GatheringCommonRequest; import com.palettee.gathering.controller.dto.Response.GatheringCommonResponse; import com.palettee.gathering.controller.dto.Response.GatheringDetailsResponse; +import com.palettee.gathering.repository.GatheringRedisRepository; import com.palettee.gathering.service.GatheringService; import com.palettee.global.security.validation.UserUtils; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; +import com.palettee.gathering.controller.dto.Response.CustomSliceResponse; import com.palettee.user.domain.User; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -23,11 +24,14 @@ public class GatheringController { private final GatheringService gatheringService; + private final GatheringRedisRepository redisRepository; + @PostMapping() public GatheringCommonResponse create(@RequestBody @Valid GatheringCommonRequest request) { GatheringCommonResponse gathering = gatheringService.createGathering(request, UserUtils.getContextUser()); + redisRepository.addGatheringInRedis(gathering.gatheringId()); return gathering; } @@ -44,8 +48,7 @@ public CustomSliceResponse findAll( @RequestParam(required = false, defaultValue = "0") int personnel, Pageable pageable ) { - log.info("positions.size = {}", positions.size()); - return gatheringService.findAll(sort, subject, period, contact, positions, status, personnel, gatheringId, pageable); + return gatheringService.findAll(sort, subject, period, contact, positions, status, personnel, gatheringId, pageable, isFirstTrue(gatheringId, sort, subject, period, contact, status, positions, personnel)); } @GetMapping("/{gatheringId}") @@ -60,6 +63,8 @@ public GatheringCommonResponse update( ) { GatheringCommonResponse gatheringCommonResponse = gatheringService.updateGathering(gatheringId, request, UserUtils.getContextUser()); + redisRepository.updateGatheringInRedis(gatheringCommonResponse.gatheringId()); + return gatheringCommonResponse; } @@ -88,4 +93,11 @@ public CustomSliceResponse findLike( return gatheringService.findLikeList(pageable, contextUser.getId(), likeId); } + private static boolean isFirstTrue(Long gatheringId, String sort, String subject, String period, String contact,String status ,List positions, int personnel) { + if(gatheringId != null || sort != null || subject != null || period != null || contact != null || !status.equals("모집중") || !positions.isEmpty() || personnel > 0){ + return false; + } + return true; + } + } diff --git a/src/main/java/com/palettee/gathering/controller/dto/Response/CustomSliceResponse.java b/src/main/java/com/palettee/gathering/controller/dto/Response/CustomSliceResponse.java new file mode 100644 index 00000000..87db3361 --- /dev/null +++ b/src/main/java/com/palettee/gathering/controller/dto/Response/CustomSliceResponse.java @@ -0,0 +1,14 @@ +package com.palettee.gathering.controller.dto.Response; + +import java.util.List; + +public record CustomSliceResponse( + List content, + boolean hasNext, + Long nextId +) { + + public static CustomSliceResponse toDTO(List content, boolean hasNext, Long nextLikeId) { + return new CustomSliceResponse (content, hasNext, nextLikeId); + } +} diff --git a/src/main/java/com/palettee/gathering/controller/dto/Response/GatheringResponse.java b/src/main/java/com/palettee/gathering/controller/dto/Response/GatheringResponse.java index fe10e866..344b839f 100644 --- a/src/main/java/com/palettee/gathering/controller/dto/Response/GatheringResponse.java +++ b/src/main/java/com/palettee/gathering/controller/dto/Response/GatheringResponse.java @@ -1,8 +1,15 @@ package com.palettee.gathering.controller.dto.Response; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.palettee.gathering.domain.Gathering; +import com.palettee.gathering.domain.Sort; +import com.palettee.gathering.domain.Subject; +import com.palettee.global.exception.InvalidCategoryException; -import java.time.format.DateTimeFormatter; +import java.time.LocalDateTime; import java.util.List; public record GatheringResponse( @@ -14,6 +21,9 @@ public record GatheringResponse( String title, String deadLine, String username, + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + LocalDateTime createDateTime, List tags, List positions @@ -31,17 +41,31 @@ public static GatheringResponse toDto(Gathering gathering) { return new GatheringResponse( gathering.getId(), gathering.getUser().getId(), - gathering.getSort().getSort(), + getSort(gathering.getSort()), gathering.getPersonnel(), - gathering.getSubject().getSubject(), + getSubject(gathering.getSubject()), gathering.getTitle(), deadLine, gathering.getUser().getName(), + gathering.getCreateAt(), gatheringTagList, positions ); } + private static String getSort(Sort sort) { + if(sort!= null){ + return sort.getSort(); + } + throw InvalidCategoryException.EXCEPTION; + } + + private static String getSubject(Subject subject) { + if(subject != null){ + return subject.getSubject(); + } + throw InvalidCategoryException.EXCEPTION; + } private static List checkGatheringTag(Gathering gathering) { if(gathering.getGatheringTagList() != null && !gathering.getGatheringTagList().isEmpty()){ diff --git a/src/main/java/com/palettee/gathering/domain/Contact.java b/src/main/java/com/palettee/gathering/domain/Contact.java index a5f150c7..373f3a2e 100644 --- a/src/main/java/com/palettee/gathering/domain/Contact.java +++ b/src/main/java/com/palettee/gathering/domain/Contact.java @@ -1,6 +1,8 @@ package com.palettee.gathering.domain; +import com.palettee.global.exception.InvalidCategoryException; import lombok.RequiredArgsConstructor; +import org.webjars.NotFoundException; import java.util.Arrays; @@ -23,7 +25,7 @@ public static Contact findContact(final String input) { return Arrays.stream(Contact.values()) .filter(it -> it.contact.equals(input)) .findFirst() - .orElse(null); + .orElseThrow(()-> InvalidCategoryException.EXCEPTION); } diff --git a/src/main/java/com/palettee/gathering/domain/Gathering.java b/src/main/java/com/palettee/gathering/domain/Gathering.java index 2d9247f3..da43a6a8 100644 --- a/src/main/java/com/palettee/gathering/domain/Gathering.java +++ b/src/main/java/com/palettee/gathering/domain/Gathering.java @@ -55,6 +55,8 @@ public class Gathering extends BaseEntity { @JoinColumn(name = "user_id") private User user; + private int hits; + @OneToMany(mappedBy = "gathering", cascade = CascadeType.ALL, orphanRemoval = true) private List gatheringTagList = new ArrayList<>(); @@ -91,6 +93,7 @@ public Gathering( this.title = title; this.content = content; this.user = user; + this.hits = 0; user.addGathering(this); setGatheringTagList(gatheringTagList); setGatheringImages(gatheringImages); diff --git a/src/main/java/com/palettee/gathering/domain/Position.java b/src/main/java/com/palettee/gathering/domain/Position.java index 15853d5f..31e4cffa 100644 --- a/src/main/java/com/palettee/gathering/domain/Position.java +++ b/src/main/java/com/palettee/gathering/domain/Position.java @@ -9,6 +9,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "positions") public class Position { @Id diff --git a/src/main/java/com/palettee/gathering/domain/PositionContent.java b/src/main/java/com/palettee/gathering/domain/PositionContent.java index d71dee0f..8069df1d 100644 --- a/src/main/java/com/palettee/gathering/domain/PositionContent.java +++ b/src/main/java/com/palettee/gathering/domain/PositionContent.java @@ -1,5 +1,6 @@ package com.palettee.gathering.domain; +import com.palettee.global.exception.InvalidCategoryException; import lombok.RequiredArgsConstructor; import java.util.Arrays; @@ -23,6 +24,6 @@ public static PositionContent findPosition(String input) { return Arrays.stream(PositionContent.values()) .filter(it -> it.position.equals(input)) .findFirst() - .orElse(null); + .orElseThrow(()-> InvalidCategoryException.EXCEPTION); } } diff --git a/src/main/java/com/palettee/gathering/repository/GatheringRedisRepository.java b/src/main/java/com/palettee/gathering/repository/GatheringRedisRepository.java new file mode 100644 index 00000000..ed420ecf --- /dev/null +++ b/src/main/java/com/palettee/gathering/repository/GatheringRedisRepository.java @@ -0,0 +1,70 @@ +package com.palettee.gathering.repository; + +import com.palettee.gathering.controller.dto.Response.GatheringResponse; +import com.palettee.gathering.domain.Gathering; +import com.palettee.gathering.service.GatheringService; +import com.palettee.global.redis.utils.TypeConverter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +import static com.palettee.global.Const.gathering_Page_Size; + + +@Repository +@Slf4j +@RequiredArgsConstructor +public class GatheringRedisRepository { + + public final static String RedisConstKey_Gathering = "cache:firstPage:gatherings"; + + private final RedisTemplate redisTemplate; + private final GatheringService gatheringService; + + @Transactional(readOnly = true) + public void addGatheringInRedis(Long gatheringId) { + log.info("저장 이벤트"); + Set range = redisTemplate.opsForZSet().range(RedisConstKey_Gathering, 0, -1); + if(!range.isEmpty()){ + Gathering gathering = gatheringService.getGathering(gatheringId); + + if(range.size() == gathering_Page_Size){ + log.info("내부 캐시 삭제"); + //맨 마지막 요소 빼기 즉 score가 가장 낮은애를 빼줌 + redisTemplate.opsForZSet().removeRange(RedisConstKey_Gathering, 0, 0); + } + GatheringResponse gatheringResponse = GatheringResponse.toDto(gathering); + redisTemplate.opsForZSet().add(RedisConstKey_Gathering, gatheringResponse, TypeConverter.LocalDateTimeToDouble(gatheringResponse.createDateTime())); + } + + } + + @Transactional(readOnly = true) + public void updateGatheringInRedis(Long gatheringId){ + log.info("수정 이벤트"); + Set keys = redisTemplate.keys(RedisConstKey_Gathering); + + if(!keys.isEmpty()){ + Gathering gathering = gatheringService.getGathering(gatheringId); + + GatheringResponse gatheringResponse = GatheringResponse.toDto(gathering); + + Double score = TypeConverter.LocalDateTimeToDouble(gatheringResponse.createDateTime()); + + Long removeCount = redisTemplate.opsForZSet().removeRangeByScore(RedisConstKey_Gathering, score, score); + + if(removeCount != 0){ + log.info("캐시 수정으로 인한 새로운 값 재캐싱"); + redisTemplate.opsForZSet().add(RedisConstKey_Gathering, gatheringResponse, TypeConverter.LocalDateTimeToDouble(gatheringResponse.createDateTime())); + } + } + } + + + + +} diff --git a/src/main/java/com/palettee/gathering/repository/GatheringRepositoryCustom.java b/src/main/java/com/palettee/gathering/repository/GatheringRepositoryCustom.java index c8e06fd2..5e820db4 100644 --- a/src/main/java/com/palettee/gathering/repository/GatheringRepositoryCustom.java +++ b/src/main/java/com/palettee/gathering/repository/GatheringRepositoryCustom.java @@ -1,6 +1,6 @@ package com.palettee.gathering.repository; -import com.palettee.portfolio.controller.dto.response.*; +import com.palettee.gathering.controller.dto.Response.CustomSliceResponse; import com.palettee.user.controller.dto.response.users.*; import java.util.*; import org.springframework.data.domain.*; diff --git a/src/main/java/com/palettee/gathering/repository/GatheringRepositoryImpl.java b/src/main/java/com/palettee/gathering/repository/GatheringRepositoryImpl.java index 4251d3db..77369398 100644 --- a/src/main/java/com/palettee/gathering/repository/GatheringRepositoryImpl.java +++ b/src/main/java/com/palettee/gathering/repository/GatheringRepositoryImpl.java @@ -10,7 +10,6 @@ import com.palettee.gathering.domain.Sort; import com.palettee.gathering.domain.*; import com.palettee.likes.domain.*; -import com.palettee.portfolio.controller.dto.response.*; import com.palettee.user.controller.dto.response.users.*; import com.querydsl.core.*; import com.querydsl.core.types.dsl.*; @@ -47,7 +46,7 @@ public CustomSliceResponse pageGathering( List result = queryFactory .selectFrom(gathering) .join(gathering.user, user).fetchJoin() - .join(gathering.positions, position).fetchJoin() + .leftJoin(gathering.positions, position).fetchJoin() .where( sortEq(sort), subjectEq(subject), @@ -58,7 +57,7 @@ public CustomSliceResponse pageGathering( positionIn(positions), pageIdLoe(gatheringId) ) - .orderBy(gathering.id.desc()) + .orderBy(gathering.createAt.desc()) .limit(pageable.getPageSize() + 1) .fetch(); diff --git a/src/main/java/com/palettee/gathering/service/GatheringService.java b/src/main/java/com/palettee/gathering/service/GatheringService.java index a71c6953..8ab9a2ff 100644 --- a/src/main/java/com/palettee/gathering/service/GatheringService.java +++ b/src/main/java/com/palettee/gathering/service/GatheringService.java @@ -2,21 +2,23 @@ import com.palettee.gathering.GatheringNotFoundException; import com.palettee.gathering.controller.dto.Request.GatheringCommonRequest; +import com.palettee.gathering.controller.dto.Response.CustomSliceResponse; import com.palettee.gathering.controller.dto.Response.GatheringCommonResponse; import com.palettee.gathering.controller.dto.Response.GatheringDetailsResponse; +import com.palettee.gathering.controller.dto.Response.GatheringResponse; import com.palettee.gathering.domain.Contact; import com.palettee.gathering.domain.Gathering; import com.palettee.gathering.domain.Sort; import com.palettee.gathering.domain.Subject; import com.palettee.gathering.repository.GatheringRepository; import com.palettee.global.redis.service.RedisService; +import com.palettee.global.redis.utils.TypeConverter; import com.palettee.global.s3.service.ImageService; import com.palettee.likes.domain.LikeType; import com.palettee.likes.domain.Likes; import com.palettee.likes.repository.LikeRepository; import com.palettee.notification.controller.dto.NotificationRequest; import com.palettee.notification.service.NotificationService; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; import com.palettee.user.domain.User; import com.palettee.user.exception.UserAccessException; import com.palettee.user.exception.UserNotFoundException; @@ -24,10 +26,18 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.palettee.global.Const.*; + +import static com.palettee.gathering.repository.GatheringRedisRepository.RedisConstKey_Gathering; @Service @RequiredArgsConstructor @@ -43,9 +53,16 @@ public class GatheringService { private final NotificationService notificationService; + private final RedisTemplate redisTemplate; + private final RedisService redisService; + private static boolean hasNext; + + + + @Transactional public GatheringCommonResponse createGathering(GatheringCommonRequest request, User user) { @@ -67,7 +84,9 @@ public GatheringCommonResponse createGathering(GatheringCommonRequest request, U .gatheringTagList(GatheringCommonRequest.getGatheringTag(request.gatheringTag())) .build(); - return GatheringCommonResponse.toDTO(gatheringRepository.save(gathering)); + Gathering saveGathering = gatheringRepository.save(gathering); + + return GatheringCommonResponse.toDTO(saveGathering); } public CustomSliceResponse findAll( @@ -79,19 +98,38 @@ public CustomSliceResponse findAll( String status, int personnel, Long gatheringId, - Pageable pageable + Pageable pageable, + boolean isFirstTrue ) { + + if(isFirstTrue){ // 첫 페이지 인지 + CustomSliceResponse cachedFirstPage = getCachedFirstPage(pageable); + + if(cachedFirstPage != null){ + return cachedFirstPage; + } + + // 캐시가 비어 있는 경우 DB에서 데이터를 가져오고 캐시에 저장 + CustomSliceResponse customSliceResponse = gatheringRepository.pageGathering( + sort, subject, period, contact, positions, personnel, status, gatheringId, pageable); + hasNext = customSliceResponse.hasNext(); + + List results = customSliceResponse.content(); + + results.forEach(result -> + redisTemplate.opsForZSet().add(RedisConstKey_Gathering, result, TypeConverter.LocalDateTimeToDouble(result.createDateTime())) + ); + + gathering_Page_Size = pageable.getPageSize(); + + redisTemplate.expire(RedisConstKey_Gathering, 1, TimeUnit.HOURS); // 1시간으로 고정 + + return customSliceResponse; + } + + // 첫 페이지가 아니면 DB에서 바로 가져옴 return gatheringRepository.pageGathering( - sort, - subject, - period, - contact, - positions, - personnel, - status, - gatheringId, - pageable - ); + sort, subject, period, contact, positions, personnel, status, gatheringId, pageable); } public GatheringDetailsResponse findByDetails(Long gatheringId, Long userId) { @@ -127,6 +165,7 @@ public GatheringCommonResponse updateGathering(Long gatheringId, GatheringCommon if(request.gatheringImages()!= null) deleteImages(gathering); // 업데이트시 이미지가 들어왓을시 본래 s3 이미지삭제 + return GatheringCommonResponse.toDTO(gathering); } @@ -143,6 +182,8 @@ public GatheringCommonResponse deleteGathering(Long gatheringId, User user) { gatheringRepository.delete(gathering); + redisTemplate.delete(RedisConstKey_Gathering); + return GatheringCommonResponse.toDTO(gathering); } @Transactional @@ -234,6 +275,30 @@ private void deleteImages(Gathering gathering) { } } + // 첫 페이지 이면서 캐시에 데이터가 있는지 검증 + private CustomSliceResponse getCachedFirstPage(Pageable pageable){ + Set range = redisTemplate.opsForZSet().reverseRange(RedisConstKey_Gathering, 0, pageable.getPageSize()); + + if(range != null && !range.isEmpty()){ + log.info("캐시에 값이 잇음"); + List gatheringResponses = new ArrayList<>(range); + + if(gatheringResponses.size() != pageable.getPageSize()){ //페이지 사이즈가 바뀌면 + log.info("range.size = {}", gatheringResponses.size()); + log.info("pageable.getPageSize = {}", pageable.getPageSize()); + log.info("사이즈가 다름"); + redisTemplate.delete(RedisConstKey_Gathering); + return null; + } + + Long nextId = hasNext ? gatheringResponses.get(gatheringResponses.size() - 1).gatheringId() : null; + + + return new CustomSliceResponse(gatheringResponses,hasNext, nextId); + } + return null; + } + } diff --git a/src/main/java/com/palettee/gathering/service/schedule/CheckScheduledTasks.java b/src/main/java/com/palettee/gathering/service/schedule/CheckScheduledTasks.java index d322c150..7f78a6f1 100644 --- a/src/main/java/com/palettee/gathering/service/schedule/CheckScheduledTasks.java +++ b/src/main/java/com/palettee/gathering/service/schedule/CheckScheduledTasks.java @@ -1,11 +1,14 @@ package com.palettee.gathering.service.schedule; +import com.palettee.gathering.controller.dto.Response.GatheringResponse; import com.palettee.gathering.service.GatheringService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; + @Component @RequiredArgsConstructor @Slf4j @@ -13,8 +16,12 @@ public class CheckScheduledTasks { private final GatheringService gatheringService; - // 오전 12시와 오후 12시에 스케줄 실행 - @Scheduled(cron = "0 0 0,12 * * ?") + private final RedisTemplate redisTemplate; + + /** + * 상태값 변경시 자정에 키 삭제 -> 정합성을 맞추기 위해 + */ + @Scheduled(cron = "0 0 0 * * ?") public void checkScheduledTasks() { log.info("만료 스케줄러 실행"); gatheringService.updateGatheringStatus(); @@ -25,4 +32,6 @@ public void checkScheduledTasks() { + + } diff --git a/src/main/java/com/palettee/global/Const.java b/src/main/java/com/palettee/global/Const.java index 593f0a87..f6f42862 100644 --- a/src/main/java/com/palettee/global/Const.java +++ b/src/main/java/com/palettee/global/Const.java @@ -6,4 +6,8 @@ public class Const { public static final String LIKE_PREFIX = "Like_"; + public static int portFolio_Page_Size; + + public static int gathering_Page_Size; + } diff --git a/src/main/java/com/palettee/global/configs/RedisConfig.java b/src/main/java/com/palettee/global/configs/RedisConfig.java index b6ca4be9..14e49e96 100644 --- a/src/main/java/com/palettee/global/configs/RedisConfig.java +++ b/src/main/java/com/palettee/global/configs/RedisConfig.java @@ -6,13 +6,13 @@ import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.palettee.chat.controller.dto.response.ChatResponse; +import com.palettee.gathering.controller.dto.Response.GatheringResponse; import com.palettee.global.redis.sub.RedisSubscriber; +import com.palettee.portfolio.controller.dto.response.PortFolioResponse; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -115,24 +115,31 @@ public RedisTemplate chatRedisTemplate(RedisConnectionFact } @Bean - public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { - RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) - .entryTtl(Duration.ofHours(1)); + public RedisTemplate RedisGatheringTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setConnectionFactory(connectionFactory); + return redisTemplate; + } - return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); + @Bean + public RedisTemplate RedisPortFolioTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setConnectionFactory(connectionFactory); + return redisTemplate; } - @Bean(name = "customCacheManager") - @Primary - public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { + + + @Bean + public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) - .entryTtl(Duration.ofMinutes(30)); + .entryTtl(Duration.ofHours(3)); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(redisCacheConfiguration) diff --git a/src/main/java/com/palettee/global/redis/service/RedisCleanUp.java b/src/main/java/com/palettee/global/redis/service/RedisCleanUp.java new file mode 100644 index 00000000..e08d8b5b --- /dev/null +++ b/src/main/java/com/palettee/global/redis/service/RedisCleanUp.java @@ -0,0 +1,26 @@ +package com.palettee.global.redis.service; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import static com.palettee.gathering.repository.GatheringRedisRepository.RedisConstKey_Gathering; +import static com.palettee.portfolio.repository.PortFolioRedisRepository.RedisConstKey_PortFolio; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RedisCleanUp { + + private final RedisTemplate redisTemplate; + + @PostConstruct + public void initRedisKey(){ + log.info("Redis 서버 로딩 시 해당 캐시 키 삭제"); + redisTemplate.delete(RedisConstKey_Gathering); + redisTemplate.delete(RedisConstKey_PortFolio); + } + +} diff --git a/src/main/java/com/palettee/global/redis/utils/TypeConverter.java b/src/main/java/com/palettee/global/redis/utils/TypeConverter.java index af6ab975..d6434a34 100644 --- a/src/main/java/com/palettee/global/redis/utils/TypeConverter.java +++ b/src/main/java/com/palettee/global/redis/utils/TypeConverter.java @@ -6,9 +6,7 @@ public class TypeConverter { public static Double LocalDateTimeToDouble(LocalDateTime timeStamp) { - long epochMilli = timeStamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - int nanos = timeStamp.getNano(); - return epochMilli + (nanos / 1000000.0); + return ((Long) timeStamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()).doubleValue(); } public static String LongToString(Long id) { diff --git a/src/main/java/com/palettee/likes/domain/Likes.java b/src/main/java/com/palettee/likes/domain/Likes.java index 42c5d726..2c759087 100644 --- a/src/main/java/com/palettee/likes/domain/Likes.java +++ b/src/main/java/com/palettee/likes/domain/Likes.java @@ -1,6 +1,7 @@ package com.palettee.likes.domain; +import com.palettee.global.entity.BaseEntity; import com.palettee.user.domain.*; import jakarta.persistence.*; import lombok.*; @@ -9,9 +10,9 @@ @Entity @Getter -@Table(name = "likes") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Likes { + +public class Likes extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/palettee/portfolio/controller/PortFolioController.java b/src/main/java/com/palettee/portfolio/controller/PortFolioController.java index 27f34805..30eaefbb 100644 --- a/src/main/java/com/palettee/portfolio/controller/PortFolioController.java +++ b/src/main/java/com/palettee/portfolio/controller/PortFolioController.java @@ -1,14 +1,13 @@ package com.palettee.portfolio.controller; import com.palettee.global.security.validation.UserUtils; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; -import com.palettee.portfolio.controller.dto.response.PortFolioResponse; +import com.palettee.portfolio.controller.dto.response.CustomOffSetResponse; +import com.palettee.portfolio.controller.dto.response.CustomPortFolioResponse; import com.palettee.portfolio.controller.dto.response.PortFolioWrapper; import com.palettee.portfolio.service.PortFolioService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.*; @RestController @@ -22,13 +21,13 @@ public class PortFolioController @GetMapping() - public Slice findAll( + public CustomOffSetResponse findAll( Pageable pageable, @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) String majorJobGroup, @RequestParam(required = false) String minorJobGroup ){ - return portFolioService.findAllPortFolio(pageable,majorJobGroup, minorJobGroup,sort); + return portFolioService.findAllPortFolio(pageable,majorJobGroup, minorJobGroup,sort, isFirst(pageable,majorJobGroup,minorJobGroup,sort)); } @GetMapping("/{portFolioId}") @@ -37,7 +36,7 @@ public boolean clickPortFolio(@PathVariable Long portFolioId){ } @GetMapping("/my-page") - public CustomSliceResponse findLike( + public CustomPortFolioResponse findLike( Pageable pageable , @RequestParam(required = false) Long likeId){ @@ -58,13 +57,9 @@ public PortFolioWrapper findPopular(){ return portFolioService.popularPortFolio(); } -// private static boolean firstPage(Pageable pageable) { -// boolean isFirst = pageable.getPageNumber() == 0; -// return isFirst; -// } - private static boolean isLikedFirst(Long likeId) { - if(likeId == null){ + private static boolean isFirst(Pageable pageable, String majorJobGroup, String minorJobGroup,String sort) { + if(pageable.getOffset() == 0 && majorJobGroup == null && minorJobGroup == null && sort.equals("latest")){ return true; } return false; diff --git a/src/main/java/com/palettee/portfolio/controller/dto/response/CustomOffSetResponse.java b/src/main/java/com/palettee/portfolio/controller/dto/response/CustomOffSetResponse.java new file mode 100644 index 00000000..97997364 --- /dev/null +++ b/src/main/java/com/palettee/portfolio/controller/dto/response/CustomOffSetResponse.java @@ -0,0 +1,15 @@ +package com.palettee.portfolio.controller.dto.response; + +import java.util.List; + +public record CustomOffSetResponse( + List content, + boolean hasNext, + Long offset, + int pageSize +) { + + public static CustomOffSetResponse toDto(List content, boolean hasNext, Long offset, int pageSize) { + return new CustomOffSetResponse(content, hasNext, offset, pageSize); + } +} diff --git a/src/main/java/com/palettee/portfolio/controller/dto/response/CustomPortFolioResponse.java b/src/main/java/com/palettee/portfolio/controller/dto/response/CustomPortFolioResponse.java new file mode 100644 index 00000000..bc70bb20 --- /dev/null +++ b/src/main/java/com/palettee/portfolio/controller/dto/response/CustomPortFolioResponse.java @@ -0,0 +1,14 @@ +package com.palettee.portfolio.controller.dto.response; + +import java.util.List; + +public record CustomPortFolioResponse( + List content, + boolean hasNext, + Long nextId +) { + + public static CustomPortFolioResponse toDTO(List content, boolean hasNext, Long nextLikeId) { + return new CustomPortFolioResponse (content, hasNext, nextLikeId); + } +} diff --git a/src/main/java/com/palettee/portfolio/controller/dto/response/CustomSliceResponse.java b/src/main/java/com/palettee/portfolio/controller/dto/response/CustomSliceResponse.java deleted file mode 100644 index 9d6e4095..00000000 --- a/src/main/java/com/palettee/portfolio/controller/dto/response/CustomSliceResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.palettee.portfolio.controller.dto.response; - -import java.util.List; - -public record CustomSliceResponse( - List content, - boolean hasNext, - Long nextId -) { - - public static CustomSliceResponse toDTO(List content, boolean hasNext, Long nextLikeId) { - return new CustomSliceResponse(content, hasNext, nextLikeId); - } -} diff --git a/src/main/java/com/palettee/portfolio/controller/dto/response/PortFolioResponse.java b/src/main/java/com/palettee/portfolio/controller/dto/response/PortFolioResponse.java index cfd6e735..ef9fc187 100644 --- a/src/main/java/com/palettee/portfolio/controller/dto/response/PortFolioResponse.java +++ b/src/main/java/com/palettee/portfolio/controller/dto/response/PortFolioResponse.java @@ -1,8 +1,13 @@ package com.palettee.portfolio.controller.dto.response; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.palettee.portfolio.domain.PortFolio; import com.palettee.user.domain.RelatedLink; +import java.time.LocalDateTime; import java.util.List; public record PortFolioResponse( @@ -15,23 +20,38 @@ public record PortFolioResponse( String majorJobGroup, String minorJobGroup, String memberImageUrl, - List relatedUrl + List relatedUrl, + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + LocalDateTime createAt ) { - public static PortFolioResponse toDto(PortFolio portFolio){ - - List relationUrl= checkRelationUrl(portFolio); - - return new PortFolioResponse(portFolio.getPortfolioId(),portFolio.getUser().getId(), portFolio.getUser().getJobTitle(), portFolio.getUrl(),portFolio.getUser().getName() , portFolio.getUser().getBriefIntro(), portFolio.getUser().getMajorJobGroup().name(), portFolio.getUser().getMinorJobGroup().name(), portFolio.getUser().getImageUrl(),relationUrl); + public static PortFolioResponse toDto(PortFolio portFolio) { + List relationUrl = checkRelationUrl(portFolio); + + return new PortFolioResponse( + portFolio.getPortfolioId(), + portFolio.getUser().getId(), + portFolio.getUser().getJobTitle(), + portFolio.getUrl(), + portFolio.getUser().getName(), + portFolio.getUser().getBriefIntro(), + portFolio.getMajorJobGroup().name(), + portFolio.getMinorJobGroup().name(), + portFolio.getUser().getImageUrl(), + relationUrl, + portFolio.getCreateAt() + ); } private static List checkRelationUrl(PortFolio portFolio) { List relatedLinks = portFolio.getUser().getRelatedLinks(); - if(relatedLinks != null && !relatedLinks.isEmpty()){ + if (relatedLinks != null && !relatedLinks.isEmpty()) { return relatedLinks.stream() - .map(RelatedLink::getLink).toList(); + .map(RelatedLink::getLink) + .toList(); } return null; } -} \ No newline at end of file +} diff --git a/src/main/java/com/palettee/portfolio/domain/PortFolio.java b/src/main/java/com/palettee/portfolio/domain/PortFolio.java index fe508371..10099091 100644 --- a/src/main/java/com/palettee/portfolio/domain/PortFolio.java +++ b/src/main/java/com/palettee/portfolio/domain/PortFolio.java @@ -1,6 +1,8 @@ package com.palettee.portfolio.domain; import com.palettee.global.entity.BaseEntity; +import com.palettee.user.domain.MajorJobGroup; +import com.palettee.user.domain.MinorJobGroup; import com.palettee.user.domain.User; import jakarta.persistence.*; import lombok.AccessLevel; @@ -28,11 +30,19 @@ public class PortFolio extends BaseEntity { @JoinColumn(name = "user_id") private User user; + @Enumerated(EnumType.STRING) + private MajorJobGroup majorJobGroup; + + @Enumerated(EnumType.STRING) + private MinorJobGroup minorJobGroup; + @Builder - public PortFolio(User user, String url) { + public PortFolio(User user, String url,MajorJobGroup majorJobGroup, MinorJobGroup minorJobGroup) { this.hits = 0; this.user = user; this.url = url; + this.minorJobGroup = minorJobGroup; + this.majorJobGroup = majorJobGroup; user.addPortfolio(this); } diff --git a/src/main/java/com/palettee/portfolio/repository/PortFolioRedisRepository.java b/src/main/java/com/palettee/portfolio/repository/PortFolioRedisRepository.java new file mode 100644 index 00000000..0f775df0 --- /dev/null +++ b/src/main/java/com/palettee/portfolio/repository/PortFolioRedisRepository.java @@ -0,0 +1,89 @@ +package com.palettee.portfolio.repository; + +import com.palettee.global.redis.utils.TypeConverter; +import com.palettee.portfolio.controller.dto.response.PortFolioResponse; +import com.palettee.portfolio.domain.PortFolio; +import com.palettee.portfolio.service.PortFolioService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Set; + +import static com.palettee.global.Const.portFolio_Page_Size; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class PortFolioRedisRepository +{ + + public final static String RedisConstKey_PortFolio = "cache:firstPage:portFolios"; + + private final PortFolioService portFolioService; + private final RedisTemplate redisTemplate; + + + public void addPortFolioInRedis(Long portFolioId){ + + log.info("저장 이벤트"); + Set keys = redisTemplate.keys(RedisConstKey_PortFolio); + + if(!keys.isEmpty()){ + PortFolio portFolio = portFolioService.getUserPortFolio(portFolioId); + Long preSize = redisTemplate.opsForZSet().size(RedisConstKey_PortFolio); + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1);// 전체 범위 조회 + if(range != null){ + removeMatchingPortFolioFromCache(range, portFolio); + + Long afterSize = redisTemplate.opsForZSet().size(RedisConstKey_PortFolio); + + log.info("afterSize = {}", afterSize); + + if(preSize.equals(afterSize) && range.size() == portFolio_Page_Size){ + log.info("캐시에 해당 아이디가 없어서 마지막 데이터 삭제"); + redisTemplate.opsForZSet().removeRange(RedisConstKey_PortFolio, 0, 0); //맨 마지막 요소 빼기 즉 score가 가장 낮은애를 빼줌 + } + } + + PortFolioResponse portFolioResponse = PortFolioResponse.toDto(portFolio); + redisTemplate.opsForZSet().add(RedisConstKey_PortFolio, portFolioResponse, TypeConverter.LocalDateTimeToDouble(portFolioResponse.createAt())); + } + + + } + + + public void updatePortFolio(Long portFolioId){ + log.info("수정 이벤트"); + + Set keys = redisTemplate.keys(RedisConstKey_PortFolio); + + if(!keys.isEmpty()){ + PortFolio portFolio = portFolioService.getUserPortFolio(portFolioId); + + Long isRemove = removeMatchingPortFolioFromCache(redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1), portFolio); + + if(isRemove != 0){ + PortFolioResponse portFolioResponse = PortFolioResponse.toDto(portFolio); + redisTemplate.opsForZSet().add(RedisConstKey_PortFolio, portFolioResponse, TypeConverter.LocalDateTimeToDouble(portFolioResponse.createAt())); + } + } + } + + private Long removeMatchingPortFolioFromCache(Set range, PortFolio portFolio) { + return range.stream() + .filter(portFolioResponse -> portFolioResponse.userId().equals(portFolio.getUser().getId())) // 조건 필터링 + .findFirst() + .map(matchingResponse -> { + log.info("같은 포트폴리오 Redis 캐시에 존재"); + log.info("삭제된 Redis PortFolioId = {}", matchingResponse.portFolioId()); + // ZSet에서 조건에 맞는 항목 제거 + return redisTemplate.opsForZSet().remove(RedisConstKey_PortFolio, matchingResponse); + }) + .orElse(0L); // 조건에 맞는 포트폴리오가 없을 경우 기본값 반환 + } + +} diff --git a/src/main/java/com/palettee/portfolio/repository/PortFolioRepository.java b/src/main/java/com/palettee/portfolio/repository/PortFolioRepository.java index e3041fd7..78762527 100644 --- a/src/main/java/com/palettee/portfolio/repository/PortFolioRepository.java +++ b/src/main/java/com/palettee/portfolio/repository/PortFolioRepository.java @@ -21,4 +21,9 @@ public interface PortFolioRepository extends JpaRepository, @Query("select p from PortFolio p join fetch p.user where p.portfolioId in :portFolioIds") List findAllByPortfolioIdIn(List portFolioIds); + + @Query("SELECT p FROM PortFolio p " + "JOIN FETCH p.user u " + "LEFT JOIN FETCH u.relatedLinks " + "WHERE p.portfolioId = :portFolioId") + Optional findByFetchUserPortFolio(@Param("portFolioId") Long portFolioId); + + } diff --git a/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryCustom.java b/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryCustom.java index 232d6c18..1789b232 100644 --- a/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryCustom.java +++ b/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryCustom.java @@ -1,13 +1,13 @@ package com.palettee.portfolio.repository; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; -import com.palettee.portfolio.controller.dto.response.PortFolioResponse; +import com.palettee.portfolio.controller.dto.response.CustomOffSetResponse; +import com.palettee.gathering.controller.dto.Response.CustomSliceResponse; +import com.palettee.portfolio.controller.dto.response.CustomPortFolioResponse; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; public interface PortFolioRepositoryCustom { - Slice PageFindAllPortfolio(Pageable pageable, String majorJobGroup, String minorJobGroup, String sort); + CustomOffSetResponse PageFindAllPortfolio(Pageable pageable, String majorJobGroup, String minorJobGroup, String sort); - CustomSliceResponse PageFindLikePortfolio(Pageable pageable, Long userId , Long likeId); + CustomPortFolioResponse PageFindLikePortfolio(Pageable pageable, Long userId , Long likeId); } diff --git a/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryImpl.java b/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryImpl.java index 6c9b6b58..75fd2537 100644 --- a/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryImpl.java +++ b/src/main/java/com/palettee/portfolio/repository/PortFolioRepositoryImpl.java @@ -1,7 +1,9 @@ package com.palettee.portfolio.repository; import com.palettee.likes.domain.LikeType; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; +import com.palettee.portfolio.controller.dto.response.CustomOffSetResponse; +import com.palettee.gathering.controller.dto.Response.CustomSliceResponse; +import com.palettee.portfolio.controller.dto.response.CustomPortFolioResponse; import com.palettee.portfolio.controller.dto.response.PortFolioResponse; import com.palettee.portfolio.domain.PortFolio; import com.palettee.user.domain.MajorJobGroup; @@ -14,8 +16,6 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -43,7 +43,7 @@ public PortFolioRepositoryImpl(JPAQueryFactory queryFactory) { * */ @Override - public Slice PageFindAllPortfolio(Pageable pageable, String majorJobGroup, String minorJobGroup, String sort) { + public CustomOffSetResponse PageFindAllPortfolio(Pageable pageable, String majorJobGroup, String minorJobGroup, String sort) { List result = queryFactory .select(portFolio @@ -62,14 +62,17 @@ public Slice PageFindAllPortfolio(Pageable pageable, String m // 페이지 존재 여부를 나타내기 위해 하나 더 가져온걸 삭제 boolean hasNext = hasNextPage(pageable, result); - return new SliceImpl<>(result, pageable, hasNext); + log.info("offset ={}", pageable.getOffset()); + + + return CustomOffSetResponse.toDto(result, hasNext, pageable.getOffset(), pageable.getPageSize()); } /* 좋아요한 포트폴리오 조회(noOffSet) */ @Override - public CustomSliceResponse PageFindLikePortfolio(Pageable pageable, Long userId, Long likeId) { + public CustomPortFolioResponse PageFindLikePortfolio(Pageable pageable, Long userId, Long likeId) { /* NoOffset으로 먼저 targetId 들 조회 -> 유저가 좋아요한 포트폴리오 아이디들 조회 @@ -82,10 +85,10 @@ public CustomSliceResponse PageFindLikePortfolio(Pageable pageable, Long userId, .where( likes.user.id.eq(userId) .and(likes.likeType.eq(LikeType.PORTFOLIO)) - .and(likeIdLoe(likeId)) + .and(likeIdLoe(likeId)) ) .leftJoin(likes.user, user) - .orderBy(likes.likeId.desc()) + .orderBy(likes.createAt.desc()) .limit(pageable.getPageSize() + 1) .fetch(); @@ -112,7 +115,7 @@ public CustomSliceResponse PageFindLikePortfolio(Pageable pageable, Long userId, list.sort(Comparator.comparingInt(item -> targetIds.indexOf(item.portFolioId()))); - return new CustomSliceResponse(list, hasNext, nextId); + return new CustomPortFolioResponse(list, hasNext, nextId); } private BooleanExpression majorJobGroupEquals(String majorJobGroup) { @@ -120,7 +123,7 @@ private BooleanExpression majorJobGroupEquals(String majorJobGroup) { MajorJobGroup majorGroup = MajorJobGroup.findMajorGroup(majorJobGroup); if(majorGroup != null){ - return user.majorJobGroup.eq(majorGroup); + return portFolio.majorJobGroup.eq(majorGroup); } return null; @@ -131,7 +134,7 @@ private BooleanExpression minorJobEquals(String minorJobGroup) { MinorJobGroup findMinorJobGroup = MinorJobGroup.findMinorJobGroup(minorJobGroup); if(findMinorJobGroup != null){ - return user.minorJobGroup.eq(findMinorJobGroup); + return portFolio.minorJobGroup.eq(findMinorJobGroup); } return null; } diff --git a/src/main/java/com/palettee/portfolio/service/PortFolioService.java b/src/main/java/com/palettee/portfolio/service/PortFolioService.java index 5a37b98b..508a179b 100644 --- a/src/main/java/com/palettee/portfolio/service/PortFolioService.java +++ b/src/main/java/com/palettee/portfolio/service/PortFolioService.java @@ -1,14 +1,12 @@ package com.palettee.portfolio.service; import com.palettee.global.redis.service.RedisService; +import com.palettee.global.redis.utils.TypeConverter; import com.palettee.likes.domain.LikeType; import com.palettee.likes.domain.Likes; import com.palettee.likes.repository.LikeRepository; import com.palettee.notification.service.NotificationService; -import com.palettee.portfolio.controller.dto.response.CustomSliceResponse; -import com.palettee.portfolio.controller.dto.response.PortFolioPopularResponse; -import com.palettee.portfolio.controller.dto.response.PortFolioResponse; -import com.palettee.portfolio.controller.dto.response.PortFolioWrapper; +import com.palettee.portfolio.controller.dto.response.*; import com.palettee.portfolio.domain.PortFolio; import com.palettee.portfolio.exception.PortFolioNotFoundException; import com.palettee.portfolio.repository.PortFolioRepository; @@ -17,13 +15,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.palettee.global.Const.portFolio_Page_Size; +import static com.palettee.portfolio.repository.PortFolioRedisRepository.RedisConstKey_PortFolio; + @Service @Slf4j @Transactional(readOnly = true) @@ -34,15 +36,43 @@ public class PortFolioService { private final LikeRepository likeRepository; private final NotificationService notificationService; + private final RedisTemplate redisTemplate; + + + private static boolean hasNext; + private final RedisService redisService; - public Slice findAllPortFolio( + public CustomOffSetResponse findAllPortFolio( Pageable pageable, String majorJobGroup, String minorJobGroup, - String sort + String sort, + boolean isFirstPage + ) { + + if(isFirstPage){ + CustomOffSetResponse cachedFirstPage = getCachedFirstPage(pageable); + + if(cachedFirstPage != null){ + return cachedFirstPage; + } + CustomOffSetResponse response = portFolioRepository.PageFindAllPortfolio(pageable, majorJobGroup, minorJobGroup, sort); + log.info("캐시에 데이터 없음"); + hasNext = response.hasNext(); + log.info("hasNext ={} ", hasNext); + List results = response.content(); + + results.forEach(result -> + redisTemplate.opsForZSet().add(RedisConstKey_PortFolio, result, TypeConverter.LocalDateTimeToDouble(result.createAt())) + ); + portFolio_Page_Size = pageable.getPageSize(); + + redisTemplate.expire(RedisConstKey_PortFolio, 1, TimeUnit.HOURS); // 6시간으로 고정 + return response; + } return portFolioRepository.PageFindAllPortfolio(pageable, majorJobGroup, minorJobGroup, sort); } @@ -63,7 +93,7 @@ public boolean likePortFolio(User user, Long portFolioId) { return redisService.likeCount(portFolioId, user.getId(),"portFolio"); } - public CustomSliceResponse findListPortFolio( + public CustomPortFolioResponse findListPortFolio( Pageable pageable, Long userId, Long likeId @@ -110,6 +140,32 @@ public PortFolio getPortFolio(Long portFolioId){ .orElseThrow(() -> PortFolioNotFoundException.EXCEPTION); } + public PortFolio getUserPortFolio(Long portFolioId){ + return portFolioRepository.findByFetchUserPortFolio(portFolioId) + .orElseThrow(() -> PortFolioNotFoundException.EXCEPTION); + } + + private CustomOffSetResponse getCachedFirstPage(Pageable pageable){ + Set range = redisTemplate.opsForZSet().reverseRange(RedisConstKey_PortFolio, 0, pageable.getPageSize()); + + if(range != null && !range.isEmpty()){ + log.info("캐시에 값이 잇음"); + List portFolioResponses= new ArrayList<>(range); + + if(portFolioResponses.size() != pageable.getPageSize()){ //페이지 사이즈가 바뀌면 + log.info("range.size = {}", portFolioResponses.size()); + log.info("pageable.getPageSize = {}", pageable.getPageSize()); + log.info("사이즈가 다름"); + redisTemplate.delete(RedisConstKey_PortFolio); + return null; + } + + + return new CustomOffSetResponse(portFolioResponses,hasNext,0L, portFolioResponses.size()); + } + return null; + } + diff --git a/src/main/java/com/palettee/user/controller/BasicRegisterController.java b/src/main/java/com/palettee/user/controller/BasicRegisterController.java index ea61c990..42a231b4 100644 --- a/src/main/java/com/palettee/user/controller/BasicRegisterController.java +++ b/src/main/java/com/palettee/user/controller/BasicRegisterController.java @@ -1,6 +1,7 @@ package com.palettee.user.controller; import com.palettee.global.security.validation.*; +import com.palettee.portfolio.repository.PortFolioRedisRepository; import com.palettee.user.controller.dto.request.users.*; import com.palettee.user.controller.dto.response.users.*; import com.palettee.user.domain.*; @@ -18,6 +19,10 @@ public class BasicRegisterController { private final BasicRegisterService basicRegisterService; + private final PortFolioRedisRepository redisRepository; + + + /** * 유저 기본 정보 등록시 기초 정보 보여주기 */ @@ -43,13 +48,16 @@ public UserResponse registerBasicInfo( * 유저 포폴 정보 등록하기 */ @PostMapping("/portfolio") - public UserResponse registerPortfolio( + public UserSavePortFolioResponse registerPortfolio( @Valid @RequestBody RegisterPortfolioRequest registerPortfolioRequest ) { - return basicRegisterService.registerPortfolio( + UserSavePortFolioResponse userSavePortFolioResponse = basicRegisterService.registerPortfolio( getUserFromContext(), registerPortfolioRequest ); + redisRepository.addPortFolioInRedis(userSavePortFolioResponse.portFolioId()); + + return userSavePortFolioResponse; } private User getUserFromContext() { diff --git a/src/main/java/com/palettee/user/controller/UserController.java b/src/main/java/com/palettee/user/controller/UserController.java index b0c676bf..39628389 100644 --- a/src/main/java/com/palettee/user/controller/UserController.java +++ b/src/main/java/com/palettee/user/controller/UserController.java @@ -1,6 +1,7 @@ package com.palettee.user.controller; import com.palettee.global.security.validation.*; +import com.palettee.portfolio.repository.PortFolioRedisRepository; import com.palettee.user.controller.dto.request.users.*; import com.palettee.user.controller.dto.response.users.*; import com.palettee.user.domain.*; @@ -21,6 +22,8 @@ public class UserController { private final UserService userService; + private final PortFolioRedisRepository redisRepository; + private static final String REFRESH_TOKEN_COOKIE_KEY = "refresh_token"; /** @@ -66,12 +69,16 @@ public UserEditFormResponse getUserEditForm(@PathVariable("userId") Long id) { * @param id 수정하고자 하는 사용자의 id (자기 자신) */ @PutMapping("/{userId}/edit") - public UserResponse editUserInfo( + public UserSavePortFolioResponse editUserInfo( @PathVariable("userId") Long id, @Valid @RequestBody EditUserInfoRequest editUserInfoRequest ) { - return userService.editUserInfo(editUserInfoRequest, id, getUserFromContext()); + UserSavePortFolioResponse userSavePortFolioResponse = userService.editUserInfo(editUserInfoRequest, id, getUserFromContext()); + + redisRepository.updatePortFolio(userSavePortFolioResponse.portFolioId()); + + return userSavePortFolioResponse; } /** diff --git a/src/main/java/com/palettee/user/controller/dto/response/users/UserSavePortFolioResponse.java b/src/main/java/com/palettee/user/controller/dto/response/users/UserSavePortFolioResponse.java new file mode 100644 index 00000000..e684324c --- /dev/null +++ b/src/main/java/com/palettee/user/controller/dto/response/users/UserSavePortFolioResponse.java @@ -0,0 +1,11 @@ +package com.palettee.user.controller.dto.response.users; + +import com.palettee.portfolio.domain.PortFolio; +import com.palettee.user.domain.User; + +public record UserSavePortFolioResponse(Long userId, Long portFolioId) { + + public static UserSavePortFolioResponse of(User user, PortFolio portFolio) { + return new UserSavePortFolioResponse(user.getId(), portFolio.getPortfolioId()); + } +} diff --git a/src/main/java/com/palettee/user/domain/User.java b/src/main/java/com/palettee/user/domain/User.java index fea401fd..ccbd89d5 100644 --- a/src/main/java/com/palettee/user/domain/User.java +++ b/src/main/java/com/palettee/user/domain/User.java @@ -15,7 +15,7 @@ @Entity @Table(indexes = { @Index(name = "idx_email", columnList = "user_email"), - @Index(name = "idx_oauth_identity", columnList = "oauth_identity") + @Index(name = "idx_oauth_identity", columnList = "oauth_identity"), }) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/palettee/user/service/BasicRegisterService.java b/src/main/java/com/palettee/user/service/BasicRegisterService.java index d0c4095e..93a3aad7 100644 --- a/src/main/java/com/palettee/user/service/BasicRegisterService.java +++ b/src/main/java/com/palettee/user/service/BasicRegisterService.java @@ -6,6 +6,7 @@ import com.palettee.user.controller.dto.request.users.RegisterPortfolioRequest; import com.palettee.user.controller.dto.response.users.BasicInfoResponse; import com.palettee.user.controller.dto.response.users.UserResponse; +import com.palettee.user.controller.dto.response.users.UserSavePortFolioResponse; import com.palettee.user.domain.RelatedLink; import com.palettee.user.domain.StoredProfileImageUrl; import com.palettee.user.domain.User; @@ -19,6 +20,7 @@ import com.palettee.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +42,8 @@ public class BasicRegisterService { private final PortFolioRepository portFolioRepo; private final StoredProfileImageUrlRepository storedProfileImageUrlRepo; + private final ApplicationEventPublisher eventPublisher; + /** * 유저 기본 정보 등록시 기초 정보 보여주기 */ @@ -102,7 +106,7 @@ public UserResponse registerBasicInfo(User user, * 유저 포폴 정보 (링크) 등록하기 */ @Transactional - public UserResponse registerPortfolio( + public UserSavePortFolioResponse registerPortfolio( User user, RegisterPortfolioRequest registerPortfolioRequest ) { @@ -113,18 +117,19 @@ public UserResponse registerPortfolio( user = this.getUserByIdFetchWithPortfolio(user.getId()); // 이전 포폴 정보 삭제 - portFolioRepo.deleteAllByUserId(user.getId()); + portFolioRepo.deleteAllByUserId(user.getId()); log.debug("Deleted user {}'s all portfolio links", user.getId()); // 포폴 정보 등록 -> validation 으로 빈 링크는 안들어옴. String url = registerPortfolioRequest.portfolioUrl(); - portFolioRepo.save(new PortFolio(user, url)); + PortFolio portFolio = new PortFolio(user, url, user.getMajorJobGroup(), user.getMinorJobGroup()); + portFolioRepo.save(portFolio); log.info("Registered user portfolio info on id: {}", user.getId()); - return UserResponse.of(user); + return UserSavePortFolioResponse.of(user, portFolio); } private User getUser(String email) { diff --git a/src/main/java/com/palettee/user/service/UserService.java b/src/main/java/com/palettee/user/service/UserService.java index 2390f97e..fa324abc 100644 --- a/src/main/java/com/palettee/user/service/UserService.java +++ b/src/main/java/com/palettee/user/service/UserService.java @@ -1,23 +1,35 @@ package com.palettee.user.service; -import com.palettee.archive.domain.*; -import com.palettee.archive.repository.*; -import com.palettee.gathering.repository.*; -import com.palettee.global.security.jwt.services.*; -import com.palettee.portfolio.domain.*; -import com.palettee.portfolio.repository.*; -import com.palettee.user.controller.dto.request.users.*; +import com.palettee.archive.domain.Archive; +import com.palettee.archive.domain.ArchiveType; +import com.palettee.archive.repository.ArchiveRepository; +import com.palettee.gathering.repository.GatheringRepository; +import com.palettee.gathering.repository.GatheringTagRepository; +import com.palettee.global.security.jwt.services.RefreshTokenRedisService; +import com.palettee.portfolio.domain.PortFolio; +import com.palettee.portfolio.repository.PortFolioRepository; +import com.palettee.user.controller.dto.request.users.EditUserInfoRequest; import com.palettee.user.controller.dto.response.users.*; -import com.palettee.user.domain.*; -import com.palettee.user.exception.*; -import com.palettee.user.repository.*; -import java.util.*; -import java.util.function.*; -import java.util.stream.*; -import lombok.*; -import lombok.extern.slf4j.*; -import org.springframework.stereotype.*; -import org.springframework.transaction.annotation.*; +import com.palettee.user.domain.RelatedLink; +import com.palettee.user.domain.StoredProfileImageUrl; +import com.palettee.user.domain.User; +import com.palettee.user.domain.UserRole; +import com.palettee.user.exception.NotOwnUserException; +import com.palettee.user.exception.UserNotFoundException; +import com.palettee.user.repository.RelatedLinkRepository; +import com.palettee.user.repository.StoredProfileImageUrlRepository; +import com.palettee.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Service @@ -34,6 +46,7 @@ public class UserService { private final GatheringTagRepository gatheringTagRepo; private final RefreshTokenRedisService refreshTokenRedisService; + /** * 자신의 정보를 조회 * @@ -132,7 +145,7 @@ public UserEditFormResponse getUserEditForm( * @throws NotOwnUserException 로그인 안 되어 있거나 다른 유저의 정보를 수정하려 할 때 */ @Transactional - public UserResponse editUserInfo(EditUserInfoRequest editUserInfoRequest, + public UserSavePortFolioResponse editUserInfo(EditUserInfoRequest editUserInfoRequest, Long userId, Optional loggedInUser) throws UserNotFoundException, NotOwnUserException { @@ -174,7 +187,9 @@ public UserResponse editUserInfo(EditUserInfoRequest editUserInfoRequest, portFolioRepo.deleteAllByUserId(userOnTarget.getId()); log.debug("Deleted user {}'s all portfolio links", userOnTarget.getId()); - portFolioRepo.save(new PortFolio(userOnTarget, portfolioLink)); + PortFolio portFolio = new PortFolio(userOnTarget, portfolioLink, userOnTarget.getMajorJobGroup(), userOnTarget.getMinorJobGroup()); + + portFolioRepo.save(portFolio); log.debug("Edited user {}'s portfolio link", userOnTarget.getId()); // 사용자가 S3 에 업로드한 자원들 추가 @@ -192,7 +207,8 @@ public UserResponse editUserInfo(EditUserInfoRequest editUserInfoRequest, log.info("Edited user {}'s info successfully", userOnTarget.getId()); - return UserResponse.of(userOnTarget); + + return UserSavePortFolioResponse.of(userOnTarget, portFolio); } /** diff --git a/src/test/java/com/palettee/gathering/service/GatheringServiceTest.java b/src/test/java/com/palettee/gathering/service/GatheringServiceTest.java index f28a956c..2c32e4f3 100644 --- a/src/test/java/com/palettee/gathering/service/GatheringServiceTest.java +++ b/src/test/java/com/palettee/gathering/service/GatheringServiceTest.java @@ -1,25 +1,15 @@ package com.palettee.gathering.service; -import static org.junit.jupiter.api.Assertions.*; - import com.palettee.gathering.controller.dto.Request.*; import com.palettee.gathering.controller.dto.Response.*; import com.palettee.gathering.domain.Sort; import com.palettee.gathering.domain.*; import com.palettee.gathering.repository.*; import com.palettee.global.exception.*; -import com.palettee.portfolio.controller.dto.response.*; import com.palettee.user.domain.*; import com.palettee.user.repository.*; -import java.time.*; -import java.util.*; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.*; -import org.springframework.boot.test.context.*; -import org.springframework.data.domain.*; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -29,7 +19,6 @@ import org.springframework.data.domain.PageRequest; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -167,7 +156,7 @@ public void gathering_paging() throws Exception { //when - CustomSliceResponse customSliceResponse = gatheringService.findAll("프로젝트", "개발","3개월", "온라인" ,null, "모집중", 3,null, PageRequest.of(0, 10)); + CustomSliceResponse customSliceResponse = gatheringService.findAll("프로젝트", "개발","3개월", "온라인" ,null, "모집중", 3,null, PageRequest.of(0, 10), true); //then diff --git a/src/test/java/com/palettee/portfolio/service/PortFolioCacheServiceTest.java b/src/test/java/com/palettee/portfolio/service/PortFolioCacheServiceTest.java new file mode 100644 index 00000000..fcc0954b --- /dev/null +++ b/src/test/java/com/palettee/portfolio/service/PortFolioCacheServiceTest.java @@ -0,0 +1,356 @@ +package com.palettee.portfolio.service; + +import com.palettee.portfolio.controller.dto.response.CustomOffSetResponse; +import com.palettee.portfolio.controller.dto.response.PortFolioResponse; +import com.palettee.portfolio.domain.PortFolio; +import com.palettee.portfolio.repository.PortFolioRedisRepository; +import com.palettee.portfolio.repository.PortFolioRepository; +import com.palettee.user.domain.MajorJobGroup; +import com.palettee.user.domain.MinorJobGroup; +import com.palettee.user.domain.User; +import com.palettee.user.domain.UserRole; +import com.palettee.user.repository.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Optional; +import java.util.Set; + +import static com.palettee.portfolio.repository.PortFolioRedisRepository.RedisConstKey_PortFolio; + +@SpringBootTest +public class PortFolioCacheServiceTest { + + @Autowired + private PortFolioService portFolioService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PortFolioRepository portFolioRepository; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private PortFolioRedisRepository redisRepository; + + private User user; + + private PortFolio portFolio; + + @BeforeEach + void setUp() { + user = User.builder() + .imageUrl("image") + .email("hello") + .name("테스트") + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + userRepository.save(user); + + portFolio = PortFolio.builder() + .user(user) + .url("테스트테스트") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + portFolioRepository.save(portFolio); + } + + @AfterEach + void tearDown() { + portFolioRepository.deleteAll(); + userRepository.deleteAll(); + + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @Test + @DisplayName("첫 번쨰 페이지 zset 캐싱") + public void 포트폴리오_캐싱() throws Exception { + // given + for (int i = 0; i < 20; i++) { + PortFolio portFolio = PortFolio.builder() + .user(user) + .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + portFolioRepository.save(portFolio); + } + + //when + PageRequest pageRequest = PageRequest.of(0, 10); + CustomOffSetResponse results = portFolioService.findAllPortFolio( + pageRequest, + null, + null, + "latest" + ,true + ); + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1); + + //then + Assertions.assertThat(range.size()).isEqualTo(10); + } + + + @Test + @DisplayName("포트폴리오 등록 캐시 정합성") + public void 포트폴리오_등록_캐시정합성() throws Exception { + //given + for(int i =0; i < 5; i ++){ + User testUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("hello" + i) + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio testPortFolio = PortFolio.builder() + .user(testUser) + .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(testUser); + portFolioRepository.save(testPortFolio); + } + //when + PageRequest pageRequest = PageRequest.of(0, 6); + portFolioService.findAllPortFolio( + pageRequest, + null, + null, + "latest" + ,true //캐싱 여부 + ); + + portFolio = PortFolio.builder() + .user(user) + .url("새로운거지롱") + .majorJobGroup(MajorJobGroup.DESIGN) + .minorJobGroup(MinorJobGroup.SERVICE) + .build(); + PortFolio save = portFolioRepository.save(portFolio); + + redisRepository.addPortFolioInRedis(save.getPortfolioId()); + + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1); + + Optional samplePortFolio = range.stream().filter(portFolioResponse -> portFolioResponse.userId().equals(portFolio.getUser().getId())).findFirst(); + + + //then + Assertions.assertThat(range.size()).isEqualTo(6); + Assertions.assertThat(samplePortFolio.isPresent()).isTrue(); + Assertions.assertThat(samplePortFolio.get().majorJobGroup()).isEqualTo(MajorJobGroup.DESIGN.name()); + } + + @Test + @DisplayName("포트폴리오_수정_캐시 정합성") + public void 퐅트폴리오_수정_캐시정합성() throws Exception { + //given + for(int i =0; i < 5; i ++){ + User testUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("hello" + i) + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio testPortFolio = PortFolio.builder() + .user(testUser) + .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(testUser); + portFolioRepository.save(testPortFolio); + } + //when + PageRequest pageRequest = PageRequest.of(0, 6); + portFolioService.findAllPortFolio( + pageRequest, + null, + null, + "latest" + ,true //캐싱 여부 + ); + + // 새로 만들어진 포트폴리오 + PortFolio updateNewPortFolio= PortFolio.builder() + .user(user) + .url("수정된 유저 포트폴리오") + .majorJobGroup(MajorJobGroup.ETC) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + portFolioRepository.save(updateNewPortFolio); + + redisRepository.updatePortFolio(updateNewPortFolio.getPortfolioId()); + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1); + + Optional samplePortFolio = range.stream().filter(portFolioResponse -> portFolioResponse.userId().equals(updateNewPortFolio.getUser().getId())).findFirst(); + + //then + Assertions.assertThat(range.size()).isEqualTo(6); + Assertions.assertThat(samplePortFolio.isPresent()).isEqualTo(true); + Assertions.assertThat(samplePortFolio.get().majorJobGroup()).isEqualTo(MajorJobGroup.ETC.name()); + } + + @Test + @DisplayName("포트폴리오 추가 시 사이즈 레디스 내부 size 만큼 없으면 삭제 작업 없음") + public void 포트폴리오_캐시확인후_삭제작업없음() throws Exception { + //given + for(int i =0; i < 5; i ++){ + User testUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("hello" + i) + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio testPortFolio = PortFolio.builder() + .user(testUser) + .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(testUser); + portFolioRepository.save(testPortFolio); + } + //when + PageRequest pageRequest = PageRequest.of(0, 8); + portFolioService.findAllPortFolio( + pageRequest, + null, + null, + "latest" + ,true //캐싱 여부 + ); + + User newUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("ㄹㅇㄹㅇ") + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(newUser); + + PortFolio updateNewPortFolio= PortFolio.builder() + .user(newUser) + .url("수정된 유저 포트폴리오") + .majorJobGroup(MajorJobGroup.ETC) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio save = portFolioRepository.save(updateNewPortFolio); + + redisRepository.addPortFolioInRedis(save.getPortfolioId()); + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1); + + //then + Assertions.assertThat(range.size()).isEqualTo(7); + } + + @Test + @DisplayName("수정시 redis내에 유저의 값이 있으면 삭제") + public void 수정시_유저여부() throws Exception { + //given + for(int i =0; i < 5; i ++){ + User testUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("hello" + i) + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio testPortFolio = PortFolio.builder() + .user(testUser) + .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(testUser); + portFolioRepository.save(testPortFolio); + } + //when + PageRequest pageRequest = PageRequest.of(0, 6); + portFolioService.findAllPortFolio( + pageRequest, + null, + null, + "latest" + ,true //캐싱 여부 + ); + + User newUser = User.builder() + .imageUrl("image") + .name("테스트") + .email("ㄹㅇㄹㅇ") + .briefIntro("안녕하세요") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + userRepository.save(newUser); + + PortFolio updateNewPortFolio= PortFolio.builder() + .user(newUser) + .url("수정된 유저 포트폴리오") + .majorJobGroup(MajorJobGroup.ETC) + .minorJobGroup(MinorJobGroup.BACKEND) + .build(); + + PortFolio save = portFolioRepository.save(updateNewPortFolio); + + redisRepository.updatePortFolio(save.getPortfolioId()); + + Set range = redisTemplate.opsForZSet().range(RedisConstKey_PortFolio, 0, -1); + + Optional samplePortFolio = range.stream().filter(portFolioResponse -> portFolioResponse.userId().equals(save.getUser().getId())).findFirst(); + + + //then + Assertions.assertThat(range.size()).isEqualTo(6); + Assertions.assertThat(samplePortFolio.isPresent()).isEqualTo(false); + } +} diff --git a/src/test/java/com/palettee/portfolio/service/PortFolioServiceTest.java b/src/test/java/com/palettee/portfolio/service/PortFolioServiceTest.java index 322dd1e7..241c04e3 100644 --- a/src/test/java/com/palettee/portfolio/service/PortFolioServiceTest.java +++ b/src/test/java/com/palettee/portfolio/service/PortFolioServiceTest.java @@ -1,27 +1,42 @@ package com.palettee.portfolio.service; -import static com.palettee.global.Const.*; - -import com.palettee.global.cache.*; -import com.palettee.global.redis.service.*; -import com.palettee.likes.domain.*; -import com.palettee.likes.repository.*; -import com.palettee.portfolio.controller.dto.response.*; -import com.palettee.portfolio.domain.*; -import com.palettee.portfolio.repository.*; -import com.palettee.user.domain.*; -import com.palettee.user.repository.*; -import java.util.*; +import com.palettee.global.cache.MemoryCache; +import com.palettee.global.redis.service.RedisService; +import com.palettee.likes.domain.LikeType; +import com.palettee.likes.domain.Likes; +import com.palettee.likes.repository.LikeRepository; +import com.palettee.portfolio.controller.dto.response.CustomOffSetResponse; +import com.palettee.portfolio.controller.dto.response.CustomPortFolioResponse; +import com.palettee.portfolio.domain.PortFolio; +import com.palettee.portfolio.repository.PortFolioRepository; +import com.palettee.user.domain.MajorJobGroup; +import com.palettee.user.domain.MinorJobGroup; +import com.palettee.user.domain.User; +import com.palettee.user.domain.UserRole; +import com.palettee.user.repository.UserRepository; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.*; -import org.springframework.boot.test.context.*; -import org.springframework.data.domain.*; -import org.springframework.data.redis.core.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.palettee.global.Const.LIKE_PREFIX; +import static com.palettee.global.Const.VIEW_PREFIX; @SpringBootTest class PortFolioServiceTest { + private static final Logger log = LoggerFactory.getLogger(PortFolioServiceTest.class); @Autowired private PortFolioService portFolioService; @@ -60,6 +75,8 @@ void setUp() { portFolio = PortFolio.builder() .user(user) .url("테스트테스트") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) .build(); portFolioRepository.save(portFolio); } @@ -81,6 +98,8 @@ void portfolio_pageNation() { PortFolio portFolio = PortFolio.builder() .user(user) .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) .build(); portFolioRepository.save(portFolio); } @@ -90,15 +109,17 @@ void portfolio_pageNation() { System.out.println(all.size()); PageRequest pageRequest = PageRequest.of(0, 10); - Slice results = portFolioService.findAllPortFolio( + CustomOffSetResponse results = portFolioService.findAllPortFolio( pageRequest, MajorJobGroup.DEVELOPER.getMajorGroup(), MinorJobGroup.BACKEND.getMinorJobGroup(), "popularlity" + ,true ); + System.out.println(results.hasNext()); // then - Assertions.assertThat(results.getSize()).isEqualTo(10); + Assertions.assertThat(results.pageSize()).isEqualTo(10); Assertions.assertThat(results.hasNext()).isEqualTo(true); } @@ -110,6 +131,8 @@ void userLike_portFolio() { PortFolio portFolio = PortFolio.builder() .user(user) .url("테스트테스트1") + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) .build(); portFolioRepository.save(portFolio); @@ -123,7 +146,7 @@ void userLike_portFolio() { // when PageRequest pageRequest = PageRequest.of(0, 10); - CustomSliceResponse customSliceResponse = portFolioService.findListPortFolio(pageRequest, user.getId(), null); + CustomPortFolioResponse customSliceResponse = portFolioService.findListPortFolio(pageRequest, user.getId(), null); // then Assertions.assertThat(customSliceResponse.content().size()).isEqualTo(10); diff --git a/src/test/java/com/palettee/user/controller/BasicRegisterControllerTest.java b/src/test/java/com/palettee/user/controller/BasicRegisterControllerTest.java index 63922189..bedb9c13 100644 --- a/src/test/java/com/palettee/user/controller/BasicRegisterControllerTest.java +++ b/src/test/java/com/palettee/user/controller/BasicRegisterControllerTest.java @@ -64,6 +64,8 @@ void setup() { .email("test@test.com") .name("test") .userRole(UserRole.REAL_NEWBIE) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) .build() ); otherUser = userRepo.save( diff --git a/src/test/java/com/palettee/user/controller/UserControllerTest.java b/src/test/java/com/palettee/user/controller/UserControllerTest.java index fd828175..80f167ec 100644 --- a/src/test/java/com/palettee/user/controller/UserControllerTest.java +++ b/src/test/java/com/palettee/user/controller/UserControllerTest.java @@ -27,6 +27,7 @@ import java.util.function.*; import java.util.stream.*; import lombok.extern.slf4j.*; +import org.hibernate.Hibernate; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.*; import org.springframework.beans.factory.annotation.*; @@ -43,7 +44,6 @@ @SpringBootTest @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) -@Transactional class UserControllerTest { @Autowired @@ -126,7 +126,7 @@ void setUp() { .userRole(UserRole.REAL_NEWBIE) .build() ); - testPortFolio = portFolioRepo.save(new PortFolio(testUser, "portfolioLink.com")); + testPortFolio = portFolioRepo.save(new PortFolio(testUser, "portfolioLink.com", testUser.getMajorJobGroup(), testUser.getMinorJobGroup())); testRelatedLinks = relatedLinkRepo.saveAll(List.of( new RelatedLink("github", testUser), new RelatedLink("blog", testUser) @@ -274,6 +274,10 @@ void editUserInfo() throws Exception { EditUserInfoRequest request = genReq("etc", "etc", "student", List.of("11", "22")); String body = mapper.writeValueAsString(request); + // Lazy 로딩된 컬렉션 초기화 + Hibernate.initialize(testUser.getRelatedLinks()); + + mvc.perform(put(BASE_URL + "/edit") .header("Authorization", ACCESS_TOKEN) .contentType(MediaType.APPLICATION_JSON) diff --git a/src/test/java/com/palettee/user/service/BasicRegisterServiceTest.java b/src/test/java/com/palettee/user/service/BasicRegisterServiceTest.java index 90faaeb8..56ffa465 100644 --- a/src/test/java/com/palettee/user/service/BasicRegisterServiceTest.java +++ b/src/test/java/com/palettee/user/service/BasicRegisterServiceTest.java @@ -116,10 +116,10 @@ void registerBasicInfo() { void registerPortfolio() { RegisterPortfolioRequest request = new RegisterPortfolioRequest("test.com"); - UserResponse result = basicRegisterService.registerPortfolio(testUser, request); + UserSavePortFolioResponse userSavePortFolioResponse = basicRegisterService.registerPortfolio(testUser, request); // 응답 검증 - checkResult(result); + checkResult(userSavePortFolioResponse); // 정보 진짜 변경 됐는지 확인 User verify = userRepo.findById(testUser.getId()).orElseThrow(); @@ -139,6 +139,11 @@ private void checkResult(UserResponse result) { assertThat(result.userId()).isNotNull().isEqualTo(testUser.getId()); } + private void checkResult(UserSavePortFolioResponse result) { + assertThat(result).isNotNull(); + assertThat(result.userId()).isNotNull().isEqualTo(testUser.getId()); + } + private void checkEquality(User user) { // 요청대로 이름, 자기소개, 직무 타이틀 변경 됐는지 확인 assertThat(user).satisfies( diff --git a/src/test/java/com/palettee/user/service/UserServiceTest.java b/src/test/java/com/palettee/user/service/UserServiceTest.java index d2e31924..3f0a80df 100644 --- a/src/test/java/com/palettee/user/service/UserServiceTest.java +++ b/src/test/java/com/palettee/user/service/UserServiceTest.java @@ -102,7 +102,7 @@ void setUp() { .userRole(UserRole.REAL_NEWBIE) .build() ); - portFolioRepo.save(new PortFolio(testUser, "portfolioLink.com")); + portFolioRepo.save(new PortFolio(testUser, "portfolioLink.com", testUser.getMajorJobGroup(), testUser.getMinorJobGroup())); relatedLinkRepo.saveAll(List.of( new RelatedLink("github", testUser), new RelatedLink("blog", testUser) @@ -245,7 +245,7 @@ void editUserInfo() { ); // 정상 실행 검증 - UserResponse result = userService.editUserInfo(req, testUser.getId(), + UserSavePortFolioResponse result = userService.editUserInfo(req, testUser.getId(), Optional.of(testUser)); assertThat(result.userId()).isEqualTo(testUser.getId());