diff --git a/.DS_Store b/.DS_Store index 33940d9..dbef988 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/build.gradle b/build.gradle index 02bcb26..920461c 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,11 @@ dependencies { // Mail implementation 'org.springframework.boot:spring-boot-starter-mail' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.data:spring-data-redis:3.0.1' + } tasks.named('test') { diff --git a/src/.DS_Store b/src/.DS_Store index 1e6b66a..e3c0b5f 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store index f786cec..6d1eef5 100644 Binary files a/src/main/.DS_Store and b/src/main/.DS_Store differ diff --git a/src/main/java/com/kkokkomu/short_news/core/config/RedisConfig.java b/src/main/java/com/kkokkomu/short_news/core/config/RedisConfig.java new file mode 100644 index 0000000..c7dd510 --- /dev/null +++ b/src/main/java/com/kkokkomu/short_news/core/config/RedisConfig.java @@ -0,0 +1,17 @@ +package com.kkokkomu.short_news.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } +} diff --git a/src/main/java/com/kkokkomu/short_news/core/config/service/RedisService.java b/src/main/java/com/kkokkomu/short_news/core/config/service/RedisService.java new file mode 100644 index 0000000..81a2a4d --- /dev/null +++ b/src/main/java/com/kkokkomu/short_news/core/config/service/RedisService.java @@ -0,0 +1,53 @@ +package com.kkokkomu.short_news.core.config.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class RedisService { + + @Autowired + private RedisTemplate redisTemplate; + + // 뉴스 조회수 + private static final String NEWS_VIEW_COUNT_PREFIX = "news:viewCount:"; + + public void incrementViewCount(Long newsId) { + String key = NEWS_VIEW_COUNT_PREFIX + newsId; + redisTemplate.opsForValue().increment(key); + } + + public Integer getViewCount(Long newsId) { + String key = NEWS_VIEW_COUNT_PREFIX + newsId; + String count = redisTemplate.opsForValue().get(key); + return count != null ? Integer.parseInt(count) : 0; + } + + // 뉴스 시청기록 + private static final String VIEW_HISTORY_PREFIX = "news:viewHistory:"; + + public void saveNewsViewHistory(Long userId, Long newsId) { + String key = VIEW_HISTORY_PREFIX + userId; + redisTemplate.opsForSet().add(key, String.valueOf(newsId)); + } + + public Set getNewsViewHistory(Long userId) { + String key = VIEW_HISTORY_PREFIX + userId; + Set stringSet = redisTemplate.opsForSet().members(key); // Set 반환 + + // Set을 Set으로 변환 + return stringSet.stream() + .map(Long::valueOf) // String -> Long 변환 + .collect(Collectors.toSet()); + } + + public void deleteNewsViewHistory(Long userId) { + String key = VIEW_HISTORY_PREFIX + userId; + redisTemplate.delete(key); + } +} + diff --git a/src/main/java/com/kkokkomu/short_news/core/event/NewsViewHistListener.java b/src/main/java/com/kkokkomu/short_news/core/event/NewsViewHistListener.java deleted file mode 100644 index 0667ce7..0000000 --- a/src/main/java/com/kkokkomu/short_news/core/event/NewsViewHistListener.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.kkokkomu.short_news.core.event; - - -import com.kkokkomu.short_news.news.domain.News; -import com.kkokkomu.short_news.news.domain.NewsViewHist; -import com.kkokkomu.short_news.news.repository.NewsRepository; -import jakarta.persistence.PostPersist; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class NewsViewHistListener { - @Autowired - private NewsRepository newsRepository; - - @PostPersist - public void postPersist(NewsViewHist newsViewHist) { - News news = newsViewHist.getNews(); - news.incrementViewCnt(); - newsRepository.save(news); - } -} \ No newline at end of file diff --git a/src/main/java/com/kkokkomu/short_news/core/scheduler/NewsScheduler.java b/src/main/java/com/kkokkomu/short_news/core/scheduler/NewsScheduler.java index dc80294..9d66cba 100644 --- a/src/main/java/com/kkokkomu/short_news/core/scheduler/NewsScheduler.java +++ b/src/main/java/com/kkokkomu/short_news/core/scheduler/NewsScheduler.java @@ -1,9 +1,15 @@ package com.kkokkomu.short_news.core.scheduler; +import com.kkokkomu.short_news.core.config.service.RedisService; +import com.kkokkomu.short_news.news.domain.News; import com.kkokkomu.short_news.news.dto.news.request.CreateGenerateNewsDto; import com.kkokkomu.short_news.news.dto.news.response.GenerateNewsDto; import com.kkokkomu.short_news.core.config.service.MailService; import com.kkokkomu.short_news.news.service.AdminNewsService; +import com.kkokkomu.short_news.news.service.HomeNewsService; +import com.kkokkomu.short_news.news.service.NewsViewHistService; +import com.kkokkomu.short_news.user.domain.User; +import com.kkokkomu.short_news.user.service.UserLookupService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -17,7 +23,10 @@ @RequiredArgsConstructor public class NewsScheduler { private final AdminNewsService adminNewsService; + private final HomeNewsService homeNewsService; private final MailService mailService; + private final UserLookupService userLookupService; + private final NewsViewHistService newsViewHistService; @Scheduled(cron = "0 0 8 * * *") // 매일 아침 8시 public void generateNewsAt8AM() { @@ -59,5 +68,20 @@ public void generateNewsAt8AM() { log.info("send leesk9663@gmail.com"); mailService.sendEmail("leesk9663@gmail.com", LocalDate.now().toString() + " kkm 뉴스", content.toString()); - } + } // 뉴스 생성 + + @Scheduled(fixedRate = 600000) // 10분 마다 + public void syncViewCountToDatabase() { + log.info("syncViewCountToDatabase"); + homeNewsService.updateViewCnt(); + } // 뉴스 조회수 동기화 + + @Scheduled(cron = "0 0 4 * * ?") // 매일 새벽 4시에 실행 + public void syncAllUsersViewHistory() { + log.info("syncAllUsersViewHistory"); + List users = userLookupService.findAll(); + for (User user : users) { + newsViewHistService.updateNewsHist(user.getId()); + } + } // 모든 유저에 대해 시청기록 동기화 } diff --git a/src/main/java/com/kkokkomu/short_news/news/controller/HomeNewsController.java b/src/main/java/com/kkokkomu/short_news/news/controller/HomeNewsController.java index 4d4771e..03b6eef 100644 --- a/src/main/java/com/kkokkomu/short_news/news/controller/HomeNewsController.java +++ b/src/main/java/com/kkokkomu/short_news/news/controller/HomeNewsController.java @@ -29,23 +29,30 @@ public class HomeNewsController { @Operation(summary = "뉴스 리스트 조회") @GetMapping("/list") public ResponseDto>> readNewsList(@Parameter(hidden = true) @UserId Long userId, - @Parameter(description = "politics, economy, social, entertain, sports, living, world, it를 , 로 구분", example = "social,world") @RequestParam String category, + @RequestParam(required = false) Long cursorId, @RequestParam EHomeFilter filter, - @RequestParam int page, @RequestParam int size) { + @RequestParam int size) { log.info("readNewsList controller"); - return ResponseDto.ok(homeNewsService.readNewsList(userId, category, filter, page, size)); + + if (filter.equals(EHomeFilter.RECOMMEND)) { + return ResponseDto.ok(homeNewsService.readNewsList(userId, cursorId, size)); + } else { + return ResponseDto.ok(homeNewsService.readNewsList(userId, cursorId, size)); + } } @Operation(summary = "비로그인 뉴스 리스트 조회") @GetMapping("/list/guest") - public ResponseDto>> guestReadNewsList(@RequestParam int page, @RequestParam int size) { + public ResponseDto>> guestReadNewsList(@RequestParam(required = false) Long cursorId, + @RequestParam int size) { log.info("guestReadNewsList controller"); - return ResponseDto.ok(homeNewsService.guestReadNewsList(page, size)); + + return ResponseDto.ok(homeNewsService.guestReadNewsList(cursorId, size)); } - @Operation(summary = "뉴스 공유 수 증가") + @Operation(summary = "뉴스 공유수 증가") @PostMapping("/shared") public ResponseDto updateSharedCnt(@RequestBody SharedCntDto sharedCntDto) { log.info("updateSharedCnt controller"); @@ -58,4 +65,12 @@ public ResponseDto updateNotInterested(@RequestBody SharedCntDto shared log.info("updateNotInterested controller"); return ResponseDto.ok(homeNewsService.updateNotInterested(sharedCntDto)); } + + @Operation(summary = "뉴스 조회수 증가") + @PostMapping("/view") + public ResponseDto increaseViewCnt(@RequestBody SharedCntDto sharedCntDto, + @Parameter(hidden = true) @UserId Long userId) { + log.info("increaseViewCnt controller"); + return ResponseDto.ok(homeNewsService.increaseNewsView(sharedCntDto, userId)); + } } diff --git a/src/main/java/com/kkokkomu/short_news/news/domain/News.java b/src/main/java/com/kkokkomu/short_news/news/domain/News.java index 6f62a65..ff05c83 100644 --- a/src/main/java/com/kkokkomu/short_news/news/domain/News.java +++ b/src/main/java/com/kkokkomu/short_news/news/domain/News.java @@ -106,4 +106,8 @@ public void incrementViewCnt() { public void updateSharedCnt() { this.sharedCnt++; } + + public void updateViewCnt(int viewCnt) { + this.viewCnt += viewCnt; + } } diff --git a/src/main/java/com/kkokkomu/short_news/news/domain/NewsViewHist.java b/src/main/java/com/kkokkomu/short_news/news/domain/NewsViewHist.java index 5199a18..680a189 100644 --- a/src/main/java/com/kkokkomu/short_news/news/domain/NewsViewHist.java +++ b/src/main/java/com/kkokkomu/short_news/news/domain/NewsViewHist.java @@ -1,6 +1,5 @@ package com.kkokkomu.short_news.news.domain; -import com.kkokkomu.short_news.core.event.NewsViewHistListener; import com.kkokkomu.short_news.user.domain.User; import jakarta.persistence.*; import lombok.*; @@ -12,7 +11,6 @@ @Table(name = "news_view_hist", indexes = { @Index(name = "idx_user_id", columnList = "user_id") }) -@EntityListeners(NewsViewHistListener.class) public class NewsViewHist { @Id @@ -31,9 +29,9 @@ public class NewsViewHist { private LocalDateTime viewDate; // 시청 일자 @Builder - public NewsViewHist(User user, News news, LocalDateTime viewDate) { + public NewsViewHist(User user, News news) { this.user = user; this.news = news; - this.viewDate = viewDate; + this.viewDate = LocalDateTime.now(); } } diff --git a/src/main/java/com/kkokkomu/short_news/news/repository/NewsRepository.java b/src/main/java/com/kkokkomu/short_news/news/repository/NewsRepository.java index d149eb4..7698087 100644 --- a/src/main/java/com/kkokkomu/short_news/news/repository/NewsRepository.java +++ b/src/main/java/com/kkokkomu/short_news/news/repository/NewsRepository.java @@ -14,8 +14,49 @@ @Repository public interface NewsRepository extends JpaRepository { - @Query("select n from News n order by n.createdAt desc ") - Page findAllCreatedAtDesc(Pageable pageable); + /****************** 뉴스 홈화면 *************************/ +// @Query("select n from News n order by n.createdAt desc ") +// Page findAllCreatedAtDesc(Pageable pageable); + + // 카테고리별 최신순 홈화면 뉴스 조회 + @Query("SELECT n FROM News n " + + "WHERE n.category IN :categories " + + "AND n.id < :cursorId " + + "AND n.id NOT IN (SELECT h.news.id FROM NewsViewHist h WHERE h.user.id = :userId) " + + "ORDER BY n.id DESC") + Page findByCategoryAndIdLessThanAndNotViewedByUser( + @Param("categories") List category, + @Param("cursorId") Long cursorId, + @Param("userId") Long userId, + Pageable pageable + ); + + // 카테고리별 최신순 홈화면 뉴스 초기 페이지 조회 + @Query("SELECT n FROM News n " + + "WHERE n.category IN :categories " + + "AND n.id NOT IN (SELECT h.news.id FROM NewsViewHist h WHERE h.user.id = :userId) " + + "ORDER BY n.id DESC") + Page findFirstPageByCategoryAndNotViewedByUser( + @Param("categories") List category, + @Param("userId") Long userId, + Pageable pageable + ); + + // 비로그인 카테고리별 최신순 홈화면 뉴스 조회 + @Query("SELECT n FROM News n " + + "WHERE n.id < :cursorId " + + "ORDER BY n.id DESC") + Page guestFindByCategoryAndIdLessThanAndNotViewedByUser( + @Param("cursorId") Long cursorId, + Pageable pageable + ); + + // 비로그인 카테고리별 최신순 홈화면 뉴스 초기 페이지 조회 + @Query("SELECT n FROM News n " + + "ORDER BY n.id DESC") + Page guestFindFirstPageByCategoryAndNotViewedByUser( + Pageable pageable + ); /****************** 뉴스 시청 기록 *************************/ diff --git a/src/main/java/com/kkokkomu/short_news/news/repository/NewsViewHistRepository.java b/src/main/java/com/kkokkomu/short_news/news/repository/NewsViewHistRepository.java index 9d62231..3a66845 100644 --- a/src/main/java/com/kkokkomu/short_news/news/repository/NewsViewHistRepository.java +++ b/src/main/java/com/kkokkomu/short_news/news/repository/NewsViewHistRepository.java @@ -8,4 +8,7 @@ @Repository public interface NewsViewHistRepository extends JpaRepository { void deleteAllByUser(User user); + + // 중복 체크를 위한 쿼리 + boolean existsByUserIdAndNewsId(Long userId, Long newsId); } diff --git a/src/main/java/com/kkokkomu/short_news/news/service/HomeNewsService.java b/src/main/java/com/kkokkomu/short_news/news/service/HomeNewsService.java index b1a1b60..92e9d78 100644 --- a/src/main/java/com/kkokkomu/short_news/news/service/HomeNewsService.java +++ b/src/main/java/com/kkokkomu/short_news/news/service/HomeNewsService.java @@ -1,15 +1,21 @@ package com.kkokkomu.short_news.news.service; +import com.kkokkomu.short_news.core.config.service.RedisService; import com.kkokkomu.short_news.core.dto.PageInfoDto; import com.kkokkomu.short_news.core.dto.PagingResponseDto; +import com.kkokkomu.short_news.core.exception.CommonException; +import com.kkokkomu.short_news.core.exception.ErrorCode; +import com.kkokkomu.short_news.core.type.ECategory; import com.kkokkomu.short_news.core.type.EHomeFilter; import com.kkokkomu.short_news.news.domain.News; +import com.kkokkomu.short_news.news.domain.NewsViewHist; import com.kkokkomu.short_news.news.dto.news.request.SharedCntDto; import com.kkokkomu.short_news.news.dto.news.response.*; import com.kkokkomu.short_news.news.dto.newsReaction.response.NewReactionByUserDto; import com.kkokkomu.short_news.news.dto.newsReaction.response.ReactionCntDto; import com.kkokkomu.short_news.news.repository.NewsRepository; import com.kkokkomu.short_news.user.domain.User; +import com.kkokkomu.short_news.user.service.UserCategoryService; import com.kkokkomu.short_news.user.service.UserLookupService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,61 +36,71 @@ public class HomeNewsService { private final UserLookupService userLookupService; private final NewsReactionService newsReactionService; private final NewsLookupService newsLookupService; + private final NewsViewHistService newsViewHistService; + private final SearchNewsService searchNewsService; + private final UserCategoryService userCategoryService; + + private final RedisService redisService; /* 홈화면 */ @Transactional(readOnly = true) - public PagingResponseDto> readNewsList(Long userId, String category, EHomeFilter filter, int page, int size) { + public PagingResponseDto> readNewsList(Long userId, Long cursorId, int size) { User user = userLookupService.findUserById(userId); - // 일단 최신순으로 조회 - Page results = newsRepository.findAllCreatedAtDesc(PageRequest.of(page, size)); + // 커서 아이디에 해당하는 뉴스가 있는지 검사 + if (cursorId != null && !newsRepository.existsById(cursorId)) { + throw new CommonException(ErrorCode.NOT_FOUND_CURSOR); + } - List news = results.getContent(); - PageInfoDto pageInfo = PageInfoDto.fromPageInfo(results); + PageRequest pageRequest = PageRequest.of(0, size); + + // 유저가 설정했던 카테고리 조회 + List categories = userCategoryService.findAllCategoriesByUserId(userId); + + // 최신순으로 조회 + List news; + Page results; + if (cursorId == null) { + // 뉴스 조회 기록 캐싱 동기화 + newsViewHistService.updateNewsHist(userId); - List newsListDtos = new ArrayList<>(); - for (News newsItem : news) { - // 각 감정표현 별 갯수 - ReactionCntDto reactionCntDto = newsReactionService.countNewsReaction(newsItem.getId()); - - // 유저 감정표현 여부 - NewReactionByUserDto newReactionByUserDto = newsReactionService.checkNewsReaction(userId, newsItem.getId()); - - // dto 생성 - newsListDtos.add( - NewsInfoDto.builder() - .info(NewsWithKeywordDto.of(newsItem)) - .reactionCnt(reactionCntDto) - .userReaction(newReactionByUserDto) - .build() - ); + results = newsRepository.findFirstPageByCategoryAndNotViewedByUser(categories, userId, pageRequest); + } else { + results = newsRepository.findByCategoryAndIdLessThanAndNotViewedByUser(categories, cursorId, userId, pageRequest); } + // 뉴스 결과물 기반으로 반환 + news = results.getContent(); + PageInfoDto pageInfo = PageInfoDto.fromPageInfo(results); + + List newsListDtos = searchNewsService.getNewsInfo(news, userId); + return PagingResponseDto.fromEntityAndPageInfo(newsListDtos, pageInfo); } // 숏폼 리스트 조회 @Transactional(readOnly = true) - public PagingResponseDto> guestReadNewsList(int page, int size) { - // 일단 최신순으로 조회 - Page results = newsRepository.findAllCreatedAtDesc(PageRequest.of(page, size)); + public PagingResponseDto> guestReadNewsList(Long cursorId, int size) { + // 커서 아이디에 해당하는 뉴스가 있는지 검사 + if (cursorId != null && !newsRepository.existsById(cursorId)) { + throw new CommonException(ErrorCode.NOT_FOUND_CURSOR); + } - List news = results.getContent(); - PageInfoDto pageInfo = PageInfoDto.fromPageInfo(results); + PageRequest pageRequest = PageRequest.of(0, size); - List newsListDtos = new ArrayList<>(); - for (News newsItem : news) { - // 각 감정표현 별 갯수 - ReactionCntDto reactionCntDto = newsReactionService.countNewsReaction(newsItem.getId()); - - // dto 생성 - newsListDtos.add( - GuestNewsInfoDto.builder() - .info(NewsWithKeywordDto.of(newsItem)) - .reactionCnt(reactionCntDto) - .build() - ); + // 최신순으로 조회 + List news; + Page results; + if (cursorId == null) { + results = newsRepository.guestFindFirstPageByCategoryAndNotViewedByUser(pageRequest); + } else { + results = newsRepository.guestFindByCategoryAndIdLessThanAndNotViewedByUser(cursorId, pageRequest); } + news = results.getContent(); + PageInfoDto pageInfo = PageInfoDto.fromPageInfo(results); + + List newsListDtos = searchNewsService.getGuestNewsInfo(news); + return PagingResponseDto.fromEntityAndPageInfo(newsListDtos, pageInfo); } // 비로그인 숏폼 리스트 조회 @@ -104,4 +120,27 @@ public NewsDto updateNotInterested(SharedCntDto sharedCntDto) { return NewsDto.of(news); } // 관심없음 표시 + + public String increaseNewsView(SharedCntDto sharedCntDto, Long userId) { + + // 레디스 조회수 ++ + redisService.incrementViewCount(sharedCntDto.newsId()); + Integer viewCount = redisService.getViewCount(sharedCntDto.newsId()); + + // 시청기록 저장 + redisService.saveNewsViewHistory(userId, sharedCntDto.newsId()); + + return "조회수: " + viewCount; + } // 뉴스 조회 + + public void updateViewCnt() { + log.info("updateViewCnt"); + List newsList = newsRepository.findAll(); + for (News news : newsList) { + Long newsId = news.getId(); + int redisViewCount = redisService.getViewCount(newsId); + news.updateViewCnt(redisViewCount); // DB의 조회수 업데이트 + newsRepository.save(news); + } + } // 조회수 DB 동기화 } diff --git a/src/main/java/com/kkokkomu/short_news/news/service/NewsLogService.java b/src/main/java/com/kkokkomu/short_news/news/service/NewsLogService.java index 1897ca4..689eb24 100644 --- a/src/main/java/com/kkokkomu/short_news/news/service/NewsLogService.java +++ b/src/main/java/com/kkokkomu/short_news/news/service/NewsLogService.java @@ -22,6 +22,7 @@ @Slf4j public class NewsLogService { private final NewsRepository newsRepository; + private final NewsViewHistService newsViewHistService; private final UserLookupService userLookupService; public CursorResponseDto> getNewsWithComment(Long userId, Long cursorId, int size) { @@ -36,6 +37,9 @@ public CursorResponseDto> getNewsWithComment(Long userId, Lo PageRequest pageRequest = PageRequest.of(0, size); + // 캐싱 히스토리 db 동기화 + newsViewHistService.updateNewsHist(userId); + List news; Page results; if (cursorId == null) { @@ -66,6 +70,9 @@ public CursorResponseDto> getNewsWithReaction(Long userId, L PageRequest pageRequest = PageRequest.of(0, size); + // 캐싱 히스토리 db 동기화 + newsViewHistService.updateNewsHist(userId); + List news; Page results; if (cursorId == null) { @@ -96,6 +103,9 @@ public CursorResponseDto> getNewsWithHist(Long userId, Long PageRequest pageRequest = PageRequest.of(0, size); + // 캐싱 히스토리 db 동기화 + newsViewHistService.updateNewsHist(userId); + List news; Page results; if (cursorId == null) { diff --git a/src/main/java/com/kkokkomu/short_news/news/service/NewsViewHistService.java b/src/main/java/com/kkokkomu/short_news/news/service/NewsViewHistService.java index 5ba561c..313c1fb 100644 --- a/src/main/java/com/kkokkomu/short_news/news/service/NewsViewHistService.java +++ b/src/main/java/com/kkokkomu/short_news/news/service/NewsViewHistService.java @@ -1,21 +1,58 @@ package com.kkokkomu.short_news.news.service; +import com.kkokkomu.short_news.core.config.service.RedisService; +import com.kkokkomu.short_news.core.exception.CommonException; +import com.kkokkomu.short_news.core.exception.ErrorCode; +import com.kkokkomu.short_news.news.domain.News; +import com.kkokkomu.short_news.news.domain.NewsViewHist; import com.kkokkomu.short_news.news.repository.NewsViewHistRepository; import com.kkokkomu.short_news.user.domain.User; +import com.kkokkomu.short_news.user.service.UserLookupService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.Set; + @Service @RequiredArgsConstructor @Slf4j public class NewsViewHistService { private final NewsViewHistRepository newsViewHistRepository; + private final RedisService redisService; + private final NewsLookupService newsLookupService; + private final UserLookupService userLookupService; + @Transactional public void deleteAllByUser(User user) { log.info("NewsViewHistService deleteAllByUser start"); newsViewHistRepository.deleteAllByUser(user); } + + @Transactional + public void updateNewsHist(Long userId) { + Set newsIds = redisService.getNewsViewHistory(userId); + if (newsIds != null && !newsIds.isEmpty()) { + User user = userLookupService.findUserById(userId); + for (Object newsIdObj : newsIds) { + Long newsId = Long.valueOf(newsIdObj.toString()); + + // 이미 저장된 기록이 없다면 저장 + if (!newsViewHistRepository.existsByUserIdAndNewsId(userId, newsId)) { + News news = newsLookupService.findNewsById(newsId); + NewsViewHist newsViewHist = NewsViewHist.builder() + .news(news) + .user(user) + .build(); + newsViewHistRepository.save(newsViewHist); + } + } + + // Redis에서 시청 기록 삭제 + redisService.deleteNewsViewHistory(userId); + } + } } diff --git a/src/main/java/com/kkokkomu/short_news/user/service/UserCategoryService.java b/src/main/java/com/kkokkomu/short_news/user/service/UserCategoryService.java index 06ebda6..903d6d2 100644 --- a/src/main/java/com/kkokkomu/short_news/user/service/UserCategoryService.java +++ b/src/main/java/com/kkokkomu/short_news/user/service/UserCategoryService.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @RequiredArgsConstructor @Slf4j @@ -24,7 +25,6 @@ public class UserCategoryService { private final UserCategoryRepository userCategoryRepository; private final UserLookupService userLookupService; - private final UserRepository userRepository; @Transactional public String updateUserCategory(Long userId, UpdateUserCategoryDto updateUserCategoryDto) { @@ -146,6 +146,14 @@ public CategoryByUserDto findUserCategoryByUserId(Long userId) { .it(it) .sports(sports) .build(); + } // 유저 카테고리 조회 + + public List findAllCategoriesByUserId(Long userId) { + List categoryList = userCategoryRepository.findAllByUserId(userId); + + return categoryList.stream() + .map(UserCategory::getCategory) + .collect(Collectors.toList()); } private UserCategory createUserCategory(User user, ECategory category) { diff --git a/src/main/java/com/kkokkomu/short_news/user/service/UserLookupService.java b/src/main/java/com/kkokkomu/short_news/user/service/UserLookupService.java index 2259dff..d965b22 100644 --- a/src/main/java/com/kkokkomu/short_news/user/service/UserLookupService.java +++ b/src/main/java/com/kkokkomu/short_news/user/service/UserLookupService.java @@ -6,8 +6,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + @Service public interface UserLookupService { + List findAll(); + User findUserById(Long userId); User findAdminUser(Long userId); diff --git a/src/main/java/com/kkokkomu/short_news/user/service/UserLookupServiceImpl.java b/src/main/java/com/kkokkomu/short_news/user/service/UserLookupServiceImpl.java index 1c0ddd2..bc2b790 100644 --- a/src/main/java/com/kkokkomu/short_news/user/service/UserLookupServiceImpl.java +++ b/src/main/java/com/kkokkomu/short_news/user/service/UserLookupServiceImpl.java @@ -9,6 +9,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -28,4 +30,9 @@ public User findAdminUser(Long userId) { return userRepository.findByIdAndRole(userId, EUserRole.ADMIN) .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_ADMIN)); } + + @Override + public List findAll() { + return userRepository.findAll(); + } }