diff --git a/src/main/java/com/adoonge/seedzip/seed/controller/SeedController.java b/src/main/java/com/adoonge/seedzip/seed/controller/SeedController.java index 1694043..17fbc49 100644 --- a/src/main/java/com/adoonge/seedzip/seed/controller/SeedController.java +++ b/src/main/java/com/adoonge/seedzip/seed/controller/SeedController.java @@ -96,10 +96,9 @@ public ApiResponse getCategorySeeds( schema = @Schema(implementation = SeedResponse.SeedDetail.class))), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류") }) - public ApiResponse getSeedDetail(@PathVariable("seedId") Long seedId, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { - - Member member = customUserDetails.getMember(); + public ApiResponse getSeedDetail( + @PathVariable("seedId") Long seedId, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { return new ApiResponse<>(seedService.getSeedDetail(seedId)); } @@ -300,10 +299,11 @@ public ApiResponse getUnreadSeeds( @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류") }) public ApiResponse deleteSeedList( - @RequestBody @Valid SeedDeleteListRequest request, + @RequestParam List ids, @AuthenticationPrincipal CustomUserDetails customUserDetails) { + Member member = customUserDetails.getMember(); - seedService.deleteSeedList(request, member); + seedService.deleteSeedList(ids, member); return new ApiResponse<>(ErrorCode.REQUEST_OK); } diff --git a/src/main/java/com/adoonge/seedzip/seed/domain/Seed.java b/src/main/java/com/adoonge/seedzip/seed/domain/Seed.java index 96a0ab7..424bc6d 100644 --- a/src/main/java/com/adoonge/seedzip/seed/domain/Seed.java +++ b/src/main/java/com/adoonge/seedzip/seed/domain/Seed.java @@ -17,6 +17,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Version; import java.time.LocalDate; import java.util.HashSet; import java.util.Set; @@ -59,6 +60,9 @@ public class Seed extends BaseEntity { @Column(name = "view_count", nullable = false) private long viewCount = 0L; // 조회수 + @Version + private Long version; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") @OnDelete(action = OnDeleteAction.CASCADE) // Member 삭제 시 Content도 삭제 @@ -73,7 +77,6 @@ public class Seed extends BaseEntity { @OneToMany(mappedBy = "seed", fetch = FetchType.LAZY) private Set categorySeeds = new HashSet<>(); - public void updateSeedName(String seedName) { this.seedName = seedName; } diff --git a/src/main/java/com/adoonge/seedzip/seed/repository/SeedRepository.java b/src/main/java/com/adoonge/seedzip/seed/repository/SeedRepository.java index 0165545..97795b5 100644 --- a/src/main/java/com/adoonge/seedzip/seed/repository/SeedRepository.java +++ b/src/main/java/com/adoonge/seedzip/seed/repository/SeedRepository.java @@ -48,4 +48,13 @@ default void deleteUnreferencedContents() {} List findAllByIdIn(List seedIds); void deleteAllByIdIn(List seedIds); + + @Modifying + @Query(""" + UPDATE Seed s + SET s.viewCount = s.viewCount + 1, + s.version = s.version + 1 + WHERE s.id = :id AND s.version = :version + """) + int increaseViewCount(@Param("id") Long id, @Param("version") Long version); } diff --git a/src/main/java/com/adoonge/seedzip/seed/service/SeedCacheService.java b/src/main/java/com/adoonge/seedzip/seed/service/SeedCacheService.java index 2e4be0c..2aeb1e4 100644 --- a/src/main/java/com/adoonge/seedzip/seed/service/SeedCacheService.java +++ b/src/main/java/com/adoonge/seedzip/seed/service/SeedCacheService.java @@ -17,10 +17,6 @@ public SeedCacheService(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } - public void increaseViewCounts(Long seedId) { - redisTemplate.opsForHash().increment(SEED_VIEWS_KEY, seedId.toString(), 1); - } - public Map getAllViewCounts() { return redisTemplate.opsForHash().entries(SEED_VIEWS_KEY); } diff --git a/src/main/java/com/adoonge/seedzip/seed/service/SeedScheduledService.java b/src/main/java/com/adoonge/seedzip/seed/service/SeedScheduledService.java deleted file mode 100644 index 6b13b3a..0000000 --- a/src/main/java/com/adoonge/seedzip/seed/service/SeedScheduledService.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.adoonge.seedzip.seed.service; - -import java.util.Map; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.adoonge.seedzip.seed.repository.SeedRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class SeedScheduledService { - - private final SeedCacheService seedCacheService; - private final SeedRepository seedRepository; - - @Scheduled(cron = "* */5 * * * *") // 5분마다 실행 - @Transactional - public void syncSeedViewCount() { - // 캐시에서 시드 조회수 가져오기 - Map allViewCounts = seedCacheService.getAllViewCounts(); - if (allViewCounts == null || allViewCounts.isEmpty()) { - return; // 캐시에 조회수가 없으면 종료 - } - - // 시드 조회수 업데이트 - allViewCounts.forEach((k, v) -> { - Long seedId = Long.parseLong(k.toString()); - Long viewCount = v == null ? 0L : Long.parseLong(v.toString()); - seedRepository.incrementViews(seedId, viewCount); - }); - - // 캐시 초기화 - seedCacheService.clearAllViewCounts(); - } - -} diff --git a/src/main/java/com/adoonge/seedzip/seed/service/SeedService.java b/src/main/java/com/adoonge/seedzip/seed/service/SeedService.java index cc31763..6e2395a 100644 --- a/src/main/java/com/adoonge/seedzip/seed/service/SeedService.java +++ b/src/main/java/com/adoonge/seedzip/seed/service/SeedService.java @@ -64,7 +64,6 @@ public class SeedService { private final FileService fileService; - private final SeedCacheService seedCacheService; private final SeedRepository seedRepository; private final FileRepository fileRepository; @@ -172,10 +171,18 @@ public void uploadFiles(Long seedId, List files) { fileService.saveFiles(files, seed); } - @Transactional(readOnly = true) + @Transactional public SeedResponse.SeedDetail getSeedDetail(Long seedId) { Seed seed = getSeedOrThrow(seedId); - seedCacheService.increaseViewCounts(seedId); + + // 최대 3회 재시도 + for (int i = 0; i < 3; i++) { + int updated = seedRepository.increaseViewCount(seedId, seed.getVersion()); + if (updated > 0) break; // 성공 + // 실패면 최신 버전 재조회 후 재시도 + seed = getSeedOrThrow(seedId); + } + List files = fileRepository.findAllBySeed(seed) .orElseThrow(() -> SeedzipException.from(ErrorCode.FILE_NOT_FOUND)); @@ -449,12 +456,11 @@ public SeedResponse.GetAllSeeds getUnreadSeeds(Member member, int page, int size } @Transactional - public void deleteSeedList(SeedDeleteListRequest request, Member member) { - List seedIds = request.seedIdList(); - if (seedIds.isEmpty()) return; + public void deleteSeedList(List ids, Member member) { + if (ids.isEmpty()) return; - List seeds = seedRepository.findAllByIdIn(seedIds); - if(seedIds.size() != seeds.size()) { + List seeds = seedRepository.findAllByIdIn(ids); + if(ids.size() != seeds.size()) { throw SeedzipException.from(ErrorCode.SEED_ACCESS_DENIED); } @@ -466,10 +472,10 @@ public void deleteSeedList(SeedDeleteListRequest request, Member member) { deleteFileFromS3(seed.getId(), seed); } - categorySeedRepository.deleteAllBySeedIdIn(seedIds); - fileRepository.deleteAllBySeedIdIn(seedIds); - seedTagRepository.deleteAllBySeedIdIn(seedIds); - seedRepository.deleteAllByIdIn(seedIds); + categorySeedRepository.deleteAllBySeedIdIn(ids); + fileRepository.deleteAllBySeedIdIn(ids); + seedTagRepository.deleteAllBySeedIdIn(ids); + seedRepository.deleteAllByIdIn(ids); } private void deleteFileFromS3(Long id, Seed seed) {