diff --git a/src/main/java/org/recordy/server/external/config/S3Config.java b/src/main/java/org/recordy/server/common/config/S3Config.java similarity index 96% rename from src/main/java/org/recordy/server/external/config/S3Config.java rename to src/main/java/org/recordy/server/common/config/S3Config.java index 633809ea..129157ff 100644 --- a/src/main/java/org/recordy/server/external/config/S3Config.java +++ b/src/main/java/org/recordy/server/common/config/S3Config.java @@ -1,4 +1,4 @@ -package org.recordy.server.external.config; +package org.recordy.server.common.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/org/recordy/server/external/controller/S3TestApi.java b/src/main/java/org/recordy/server/common/controller/S3TestApi.java similarity index 96% rename from src/main/java/org/recordy/server/external/controller/S3TestApi.java rename to src/main/java/org/recordy/server/common/controller/S3TestApi.java index 7db402e8..989e1b22 100644 --- a/src/main/java/org/recordy/server/external/controller/S3TestApi.java +++ b/src/main/java/org/recordy/server/common/controller/S3TestApi.java @@ -1,4 +1,4 @@ -package org.recordy.server.external.controller; +package org.recordy.server.common.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/org/recordy/server/external/controller/S3TestController.java b/src/main/java/org/recordy/server/common/controller/S3TestController.java similarity index 88% rename from src/main/java/org/recordy/server/external/controller/S3TestController.java rename to src/main/java/org/recordy/server/common/controller/S3TestController.java index a4beb1d3..24ef03fb 100644 --- a/src/main/java/org/recordy/server/external/controller/S3TestController.java +++ b/src/main/java/org/recordy/server/common/controller/S3TestController.java @@ -1,7 +1,7 @@ -package org.recordy.server.external.controller; +package org.recordy.server.common.controller; import lombok.RequiredArgsConstructor; -import org.recordy.server.external.service.impl.S3ServiceImpl; +import org.recordy.server.common.service.impl.S3ServiceImpl; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/org/recordy/server/external/exception/ExternalException.java b/src/main/java/org/recordy/server/common/exception/ExternalException.java similarity index 85% rename from src/main/java/org/recordy/server/external/exception/ExternalException.java rename to src/main/java/org/recordy/server/common/exception/ExternalException.java index 38a0f2ad..ab3b3cb5 100644 --- a/src/main/java/org/recordy/server/external/exception/ExternalException.java +++ b/src/main/java/org/recordy/server/common/exception/ExternalException.java @@ -1,4 +1,4 @@ -package org.recordy.server.external.exception; +package org.recordy.server.common.exception; import org.recordy.server.common.exception.RecordyException; import org.recordy.server.common.message.ErrorMessage; diff --git a/src/main/java/org/recordy/server/external/service/S3Service.java b/src/main/java/org/recordy/server/common/service/S3Service.java similarity index 85% rename from src/main/java/org/recordy/server/external/service/S3Service.java rename to src/main/java/org/recordy/server/common/service/S3Service.java index 7e69ccfe..b2e7cc8e 100644 --- a/src/main/java/org/recordy/server/external/service/S3Service.java +++ b/src/main/java/org/recordy/server/common/service/S3Service.java @@ -1,4 +1,4 @@ -package org.recordy.server.external.service; +package org.recordy.server.common.service; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/org/recordy/server/external/service/impl/S3ServiceImpl.java b/src/main/java/org/recordy/server/common/service/impl/S3ServiceImpl.java similarity index 93% rename from src/main/java/org/recordy/server/external/service/impl/S3ServiceImpl.java rename to src/main/java/org/recordy/server/common/service/impl/S3ServiceImpl.java index 3a55f8dd..f34c8d0d 100644 --- a/src/main/java/org/recordy/server/external/service/impl/S3ServiceImpl.java +++ b/src/main/java/org/recordy/server/common/service/impl/S3ServiceImpl.java @@ -1,9 +1,9 @@ -package org.recordy.server.external.service.impl; +package org.recordy.server.common.service.impl; +import org.recordy.server.common.config.S3Config; import org.recordy.server.common.message.ErrorMessage; -import org.recordy.server.external.config.S3Config; -import org.recordy.server.external.exception.ExternalException; -import org.recordy.server.external.service.S3Service; +import org.recordy.server.common.exception.ExternalException; +import org.recordy.server.common.service.S3Service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/org/recordy/server/keyword/domain/Keyword.java b/src/main/java/org/recordy/server/keyword/domain/Keyword.java index cdec1561..be49985a 100644 --- a/src/main/java/org/recordy/server/keyword/domain/Keyword.java +++ b/src/main/java/org/recordy/server/keyword/domain/Keyword.java @@ -1,5 +1,7 @@ package org.recordy.server.keyword.domain; +import java.util.List; + public enum Keyword { EXOTIC("이색적인"), @@ -23,7 +25,9 @@ public enum Keyword { this.name = name; } - public static Keyword fromString(String keyword) { - return Keyword.valueOf(keyword.toUpperCase()); + public static List from(List keywords) { + return keywords.stream() + .map(Keyword::valueOf) + .toList(); } } diff --git a/src/main/java/org/recordy/server/keyword/domain/KeywordEntity.java b/src/main/java/org/recordy/server/keyword/domain/KeywordEntity.java index 278ca36d..cfda0808 100644 --- a/src/main/java/org/recordy/server/keyword/domain/KeywordEntity.java +++ b/src/main/java/org/recordy/server/keyword/domain/KeywordEntity.java @@ -4,12 +4,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.recordy.server.common.domain.JpaMetaInfoEntity; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "keywords") @Entity -public class KeywordEntity { +public class KeywordEntity extends JpaMetaInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/recordy/server/record/controller/RecordController.java b/src/main/java/org/recordy/server/record/controller/RecordController.java index 7c77b95d..a031cc2b 100644 --- a/src/main/java/org/recordy/server/record/controller/RecordController.java +++ b/src/main/java/org/recordy/server/record/controller/RecordController.java @@ -3,8 +3,7 @@ import lombok.RequiredArgsConstructor; import org.recordy.server.auth.security.UserId; -import org.recordy.server.keyword.domain.Keyword; -import org.recordy.server.record.controller.dto.RecordCreateRequest; +import org.recordy.server.record.controller.dto.request.RecordCreateRequest; import org.recordy.server.record.domain.File; import org.recordy.server.record.domain.Record; @@ -12,12 +11,10 @@ import org.recordy.server.record.service.RecordService; import org.springframework.data.domain.Slice; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.stream.Collectors; @RequiredArgsConstructor @RequestMapping("/api/v1/records") @@ -55,8 +52,8 @@ public ResponseEntity deleteRecord( @GetMapping("/recent") public ResponseEntity> getRecentRecords( @RequestParam(required = false) List keywords, - @RequestParam long cursorId, - @RequestParam int size + @RequestParam(required = false, defaultValue = "0L") long cursorId, + @RequestParam(required = false, defaultValue = "10") int size ) { Slice records = recordService.getRecentRecords(keywords, cursorId, size); @@ -65,11 +62,33 @@ public ResponseEntity> getRecentRecords( .body(records); } + @GetMapping("/famous") + public ResponseEntity> getFamousRecords( + @RequestParam(required = false) List keywords, + @RequestParam(required = false, defaultValue = "0") int pageNumber, + @RequestParam(required = false, defaultValue = "10") int pageSize + ){ + return ResponseEntity + .ok() + .body(recordService.getFamousRecords(keywords, pageNumber, pageSize)); + } + + @PostMapping("/watch") + public ResponseEntity watch( + @UserId long userId, + @RequestParam long recordId + ) { + recordService.watch(userId, recordId); + return ResponseEntity + .ok() + .build(); + } + @GetMapping("/user") public ResponseEntity> getRecentRecordsByUser( @UserId long userId, - @RequestParam long cursorId, - @RequestParam int size + @RequestParam(required = false, defaultValue = "0L") long cursorId, + @RequestParam(required = false, defaultValue = "10") int size ) { Slice records = recordService.getRecentRecordsByUser(userId, cursorId, size); diff --git a/src/main/java/org/recordy/server/record/controller/dto/RecordCreateRequest.java b/src/main/java/org/recordy/server/record/controller/dto/request/RecordCreateRequest.java similarity index 71% rename from src/main/java/org/recordy/server/record/controller/dto/RecordCreateRequest.java rename to src/main/java/org/recordy/server/record/controller/dto/request/RecordCreateRequest.java index e949b438..804cecf9 100644 --- a/src/main/java/org/recordy/server/record/controller/dto/RecordCreateRequest.java +++ b/src/main/java/org/recordy/server/record/controller/dto/request/RecordCreateRequest.java @@ -1,4 +1,4 @@ -package org.recordy.server.record.controller.dto; +package org.recordy.server.record.controller.dto.request; import java.util.List; diff --git a/src/main/java/org/recordy/server/record/domain/Record.java b/src/main/java/org/recordy/server/record/domain/Record.java index d0de6e0b..2a260f04 100644 --- a/src/main/java/org/recordy/server/record/domain/Record.java +++ b/src/main/java/org/recordy/server/record/domain/Record.java @@ -1,10 +1,11 @@ package org.recordy.server.record.domain; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import org.recordy.server.keyword.domain.Keyword; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; import org.recordy.server.user.domain.User; import java.util.List; @@ -20,11 +21,10 @@ public class Record { String content; List keywords; User uploader; + private LocalDateTime createdAt; + long bookmarkCount; public boolean isUploader(long userId) { - if (uploader.getId() == userId) { - return true; - } - return false; + return uploader.getId() == userId; } } diff --git a/src/main/java/org/recordy/server/record/domain/RecordEntity.java b/src/main/java/org/recordy/server/record/domain/RecordEntity.java index 4c63fb93..81ae0e1e 100644 --- a/src/main/java/org/recordy/server/record/domain/RecordEntity.java +++ b/src/main/java/org/recordy/server/record/domain/RecordEntity.java @@ -1,12 +1,16 @@ package org.recordy.server.record.domain; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.recordy.server.common.domain.JpaMetaInfoEntity; import org.recordy.server.keyword.domain.KeywordEntity; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; +import org.recordy.server.record_stat.domain.BookmarkEntity; +import org.recordy.server.record_stat.domain.ViewEntity; import org.recordy.server.user.domain.UserEntity; import java.util.ArrayList; @@ -16,7 +20,7 @@ @Getter @Table(name = "records") @Entity -public class RecordEntity { +public class RecordEntity extends JpaMetaInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,11 +35,17 @@ public class RecordEntity { @JoinColumn(name = "user_id") private UserEntity user; - @OneToMany(mappedBy = "record") + @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, orphanRemoval = true) private List uploads = new ArrayList<>(); + @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, orphanRemoval = true) + private List views = new ArrayList<>(); + + @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, orphanRemoval = true) + private List bookmarks = new ArrayList<>(); + @Builder - public RecordEntity(Long id, String videoUrl, String thumbnailUrl, String location, String content, UserEntity user) { + public RecordEntity(Long id, String videoUrl, String thumbnailUrl, String location, String content, UserEntity user, LocalDateTime createdAt) { this.id = id; this.videoUrl = videoUrl; this.thumbnailUrl = thumbnailUrl; @@ -51,14 +61,11 @@ public static RecordEntity from(Record record) { record.getFileUrl().thumbnailUrl(), record.getLocation(), record.getContent(), - UserEntity.from(record.getUploader()) + UserEntity.from(record.getUploader()), + record.getCreatedAt() ); } - private void addUpload(UploadEntity upload) { - uploads.add(upload); - } - public Record toDomain() { return Record.builder() .id(id) @@ -73,6 +80,19 @@ public Record toDomain() { .map(KeywordEntity::toDomain) .toList()) .uploader(user.toDomain()) + .bookmarkCount(bookmarks.size()) .build(); } + + public void addUpload(UploadEntity upload) { + uploads.add(upload); + } + + public void addView(ViewEntity view) { + views.add(view); + } + + public void addBookmark(BookmarkEntity bookmark) { + bookmarks.add(bookmark); + } } diff --git a/src/main/java/org/recordy/server/record/domain/UploadEntity.java b/src/main/java/org/recordy/server/record/domain/UploadEntity.java index 186a7144..5163ee2f 100644 --- a/src/main/java/org/recordy/server/record/domain/UploadEntity.java +++ b/src/main/java/org/recordy/server/record/domain/UploadEntity.java @@ -29,9 +29,12 @@ public UploadEntity(Long id, RecordEntity record, KeywordEntity keyword) { } public static UploadEntity of(RecordEntity record, KeywordEntity keyword) { - return UploadEntity.builder() + UploadEntity upload = UploadEntity.builder() .record(record) .keyword(keyword) .build(); + record.addUpload(upload); + + return upload; } } diff --git a/src/main/java/org/recordy/server/record/domain/usecase/RecordCreate.java b/src/main/java/org/recordy/server/record/domain/usecase/RecordCreate.java index e15bd4a2..6eab245e 100644 --- a/src/main/java/org/recordy/server/record/domain/usecase/RecordCreate.java +++ b/src/main/java/org/recordy/server/record/domain/usecase/RecordCreate.java @@ -1,10 +1,9 @@ package org.recordy.server.record.domain.usecase; import org.recordy.server.keyword.domain.Keyword; -import org.recordy.server.record.controller.dto.RecordCreateRequest; +import org.recordy.server.record.controller.dto.request.RecordCreateRequest; import java.util.List; -import java.util.stream.Collectors; public record RecordCreate( long uploaderId, @@ -13,17 +12,11 @@ public record RecordCreate( List keywords ) { - public static RecordCreate of(Long uploaderId, String location, String content, List keywords){ - return new RecordCreate(uploaderId, location, content, keywords); - } - public static RecordCreate from(Long uploaderId, RecordCreateRequest recordCreateRequest) { - List keywords = recordCreateRequest.keywords().stream() - .map(Keyword::valueOf) - .collect(Collectors.toList()); - return new RecordCreate(uploaderId, recordCreateRequest.location(), recordCreateRequest.content(), keywords); + return new RecordCreate( + uploaderId, + recordCreateRequest.location(), + recordCreateRequest.content(), + Keyword.from(recordCreateRequest.keywords())); } - - - } diff --git a/src/main/java/org/recordy/server/record/repository/RecordRepository.java b/src/main/java/org/recordy/server/record/repository/RecordRepository.java index 592be512..723535c8 100644 --- a/src/main/java/org/recordy/server/record/repository/RecordRepository.java +++ b/src/main/java/org/recordy/server/record/repository/RecordRepository.java @@ -1,12 +1,13 @@ package org.recordy.server.record.repository; -import java.util.Optional; import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record.domain.Record; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import java.util.List; +import java.util.Map; import java.util.Optional; public interface RecordRepository { @@ -16,9 +17,11 @@ public interface RecordRepository { void deleteById(long recordId); // query - Optional findById(long recordId); - Slice findAllOrderByPopularity(long cursor, Pageable pageable); + Optional findById(long id); + Slice findAllOrderByPopularity(Pageable pageable); + Slice findAllByKeywordsOrderByPopularity(List keywords, Pageable pageable); Slice findAllByIdAfterOrderByIdDesc(long cursor, Pageable pageable); Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keywords, long cursor, Pageable pageable); Slice findAllByUserIdOrderByIdDesc(long userId, long cursor, Pageable pageable); + Map countAllByUserIdGroupByKeyword(long userId); } diff --git a/src/main/java/org/recordy/server/record/repository/impl/RecordQueryDslRepository.java b/src/main/java/org/recordy/server/record/repository/impl/RecordQueryDslRepository.java index d85b5a82..bc1eabce 100644 --- a/src/main/java/org/recordy/server/record/repository/impl/RecordQueryDslRepository.java +++ b/src/main/java/org/recordy/server/record/repository/impl/RecordQueryDslRepository.java @@ -1,22 +1,29 @@ package org.recordy.server.record.repository.impl; +import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.recordy.server.common.util.QueryDslUtils; import org.recordy.server.keyword.domain.KeywordEntity; -import org.recordy.server.keyword.domain.QKeywordEntity; -import org.recordy.server.record.domain.QRecordEntity; -import org.recordy.server.record.domain.QUploadEntity; import org.recordy.server.record.domain.RecordEntity; -import org.recordy.server.user.domain.QUserEntity; -import org.recordy.server.user.domain.UserEntity; -import org.recordy.server.user.repository.UserRepository; 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.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.recordy.server.keyword.domain.QKeywordEntity.keywordEntity; +import static org.recordy.server.record.domain.QRecordEntity.recordEntity; +import static org.recordy.server.record.domain.QUploadEntity.uploadEntity; +import static org.recordy.server.record_stat.domain.QBookmarkEntity.bookmarkEntity; +import static org.recordy.server.record_stat.domain.QViewEntity.viewEntity; +import static org.recordy.server.user.domain.QUserEntity.userEntity; @RequiredArgsConstructor @Repository @@ -24,51 +31,145 @@ public class RecordQueryDslRepository { private final JPAQueryFactory jpaQueryFactory; - public Slice findAllByIdAfterOrderByIdDesc(long cursor, Pageable pageable) { - // TODO: 0을 여기서 대체하지 말고, 서비스나 컨트롤러에서 처리하도록 수정 + public Slice findAllOrderByPopularity(Pageable pageable) { + LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); + + List recordEntities = jpaQueryFactory + .selectFrom(recordEntity) + .leftJoin(recordEntity.bookmarks, bookmarkEntity) + .leftJoin(recordEntity.views, viewEntity) + .where( + bookmarkEntity.createdAt.after(sevenDaysAgo) + .or(viewEntity.createdAt.after(sevenDaysAgo)) + ) + .groupBy(recordEntity.id) + .orderBy(bookmarkEntity.count().multiply(2).add(viewEntity.count()).desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable, recordEntities)); + } + + public Slice findAllByKeywordsOrderByPopularity(List keywords, Pageable pageable) { + LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); + + List recordEntities = jpaQueryFactory + .selectFrom(recordEntity) + .leftJoin(recordEntity.bookmarks, bookmarkEntity) + .leftJoin(recordEntity.views, viewEntity) + .join(recordEntity.uploads, uploadEntity) + .join(uploadEntity.keyword, keywordEntity) + .where( + bookmarkEntity.createdAt.after(sevenDaysAgo) + .or(viewEntity.createdAt.after(sevenDaysAgo)), + keywordEntity.in(keywords) + ) + .groupBy(recordEntity.id) + .orderBy(bookmarkEntity.count().multiply(2).add(viewEntity.count()).desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable, recordEntities)); + } + + public Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keywords, long cursor, Pageable pageable) { if (cursor == 0) cursor = Long.MAX_VALUE; List recordEntities = jpaQueryFactory - .selectFrom(QRecordEntity.recordEntity) + .selectFrom(recordEntity) + .join(recordEntity.uploads, uploadEntity) + .join(uploadEntity.keyword, keywordEntity) .where( - QueryDslUtils.ltCursorId(cursor, QRecordEntity.recordEntity.id) + QueryDslUtils.ltCursorId(cursor, recordEntity.id), + keywordEntity.in(keywords) ) - .orderBy(QRecordEntity.recordEntity.id.desc()) + .orderBy(recordEntity.id.desc()) .limit(pageable.getPageSize() + 1) .fetch(); return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable, recordEntities)); } - public Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keywords, long cursor, Pageable pageable) { + public Slice findAllByIdAfterOrderByIdDesc(long cursor, Pageable pageable) { + // TODO: 0을 여기서 대체하지 말고, 서비스나 컨트롤러에서 처리하도록 수정 + if (cursor == 0) + cursor = Long.MAX_VALUE; + List recordEntities = jpaQueryFactory - .selectFrom(QRecordEntity.recordEntity) - .join(QRecordEntity.recordEntity.uploads, QUploadEntity.uploadEntity) - .join(QUploadEntity.uploadEntity.keyword, QKeywordEntity.keywordEntity) + .selectFrom(recordEntity) .where( - QueryDslUtils.ltCursorId(cursor, QRecordEntity.recordEntity.id), - QKeywordEntity.keywordEntity.in(keywords) + QueryDslUtils.ltCursorId(cursor, recordEntity.id) ) - .orderBy(QRecordEntity.recordEntity.id.desc()) + .orderBy(recordEntity.id.desc()) .limit(pageable.getPageSize() + 1) .fetch(); return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable, recordEntities)); } - public Slice findAllByUserIdOrderByIdDesc(UserEntity userEntity, long cursor, Pageable pageable) { + public Slice findAllByUserIdOrderByIdDesc(long userId, long cursor, Pageable pageable) { + if (cursor == 0) + cursor = Long.MAX_VALUE; + List recordEntities = jpaQueryFactory - .selectFrom((QRecordEntity.recordEntity)) - .join(QRecordEntity.recordEntity.user, QUserEntity.userEntity) + .selectFrom((recordEntity)) + .join(recordEntity.user, userEntity) .where( - QueryDslUtils.ltCursorId(cursor,QRecordEntity.recordEntity.id), - QUserEntity.userEntity.eq(userEntity) + QueryDslUtils.ltCursorId(cursor, recordEntity.id), + userEntity.id.eq(userId) ) - .orderBy(QRecordEntity.recordEntity.id.desc()) + .orderBy(recordEntity.id.desc()) .limit(pageable.getPageSize() + 1) .fetch(); - return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable,recordEntities)); + return new SliceImpl<>(recordEntities, pageable, QueryDslUtils.hasNext(pageable, recordEntities)); + } + + public Map countAllByUserIdGroupByKeyword(long userId) { + List uploadResults = jpaQueryFactory + .select(uploadEntity.keyword, uploadEntity.count()) + .from(uploadEntity) + .join(uploadEntity.record, recordEntity) + .where(recordEntity.user.id.eq(userId)) + .groupBy(uploadEntity.keyword) + .fetch(); + + List viewResults = jpaQueryFactory + .select(uploadEntity.keyword, viewEntity.count()) + .from(viewEntity) + .join(viewEntity.record, recordEntity) + .join(recordEntity.uploads, uploadEntity) + .where(viewEntity.user.id.eq(userId)) + .groupBy(uploadEntity.keyword) + .fetch(); + + List bookmarkResults = jpaQueryFactory + .select(uploadEntity.keyword, bookmarkEntity.count()) + .from(bookmarkEntity) + .join(bookmarkEntity.record, recordEntity) + .join(recordEntity.uploads, uploadEntity) + .where(bookmarkEntity.user.id.eq(userId)) + .groupBy(uploadEntity.keyword) + .fetch(); + + Map preference = uploadResults.stream() + .collect(Collectors.toMap( + tuple -> tuple.get(uploadEntity.keyword), + tuple -> tuple.get(uploadEntity.count()))); + + viewResults.forEach(tuple -> preference.merge( + tuple.get(uploadEntity.keyword), + tuple.get(viewEntity.count()), + Long::sum)); + + bookmarkResults.forEach(tuple -> preference.merge( + tuple.get(uploadEntity.keyword), + tuple.get(bookmarkEntity.count()), + Long::sum)); + + return preference; } } diff --git a/src/main/java/org/recordy/server/record/repository/impl/RecordRepositoryImpl.java b/src/main/java/org/recordy/server/record/repository/impl/RecordRepositoryImpl.java index d275d603..2d46bfca 100644 --- a/src/main/java/org/recordy/server/record/repository/impl/RecordRepositoryImpl.java +++ b/src/main/java/org/recordy/server/record/repository/impl/RecordRepositoryImpl.java @@ -1,6 +1,7 @@ package org.recordy.server.record.repository.impl; import java.util.Optional; + import lombok.RequiredArgsConstructor; import org.recordy.server.common.message.ErrorMessage; import org.recordy.server.keyword.domain.Keyword; @@ -9,18 +10,19 @@ import org.recordy.server.record.domain.Record; import org.recordy.server.record.domain.RecordEntity; import org.recordy.server.record.domain.UploadEntity; +import org.recordy.server.record.exception.RecordException; import org.recordy.server.record.repository.RecordRepository; -import org.recordy.server.user.domain.UserEntity; -import org.recordy.server.user.exception.UserException; -import org.recordy.server.user.repository.impl.UserJpaRepository; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.stream.Collectors; @RequiredArgsConstructor +@Transactional(readOnly = true) @Repository public class RecordRepositoryImpl implements RecordRepository { @@ -28,25 +30,32 @@ public class RecordRepositoryImpl implements RecordRepository { private final RecordQueryDslRepository recordQueryDslRepository; private final KeywordJpaRepository keywordJpaRepository; private final UploadJpaRepository uploadJpaRepository; - private final UserJpaRepository userJpaRepository; + @Transactional @Override public Record save(Record record) { - List keywords = keywordJpaRepository.findAll(); RecordEntity recordEntity = recordJpaRepository.save(RecordEntity.from(record)); + saveUploads(recordEntity, record.getKeywords()); + + return recordEntity.toDomain(); + } - List uploadEntities = keywords.stream() - .filter(keyword -> record.getKeywords().contains(keyword.toDomain())) + private void saveUploads(RecordEntity recordEntity, List keywords) { + List keywordEntities = keywordJpaRepository.findAll(); + List uploadEntities = keywordEntities.stream() + .filter(keyword -> keywords.contains(keyword.toDomain())) .map(keyword -> UploadEntity.of(recordEntity, keyword)) .toList(); - uploadJpaRepository.saveAll(uploadEntities); - return recordEntity.toDomain(); + uploadJpaRepository.saveAll(uploadEntities); } @Override public void deleteById(long recordId) { - recordJpaRepository.deleteById(recordId); + RecordEntity recordEntity = recordJpaRepository.findById(recordId) + .orElseThrow(() -> new RecordException(ErrorMessage.RECORD_NOT_FOUND)); + + recordJpaRepository.delete(recordEntity); } @Override @@ -56,8 +65,19 @@ public Optional findById(long recordId) { } @Override - public Slice findAllOrderByPopularity(long cursor, Pageable pageable) { - return null; + public Slice findAllOrderByPopularity(Pageable pageable) { + return recordQueryDslRepository.findAllOrderByPopularity(pageable) + .map(RecordEntity::toDomain); + } + + @Override + public Slice findAllByKeywordsOrderByPopularity(List keywords, Pageable pageable) { + List keywordEntities = keywordJpaRepository.findAll().stream() + .filter(keyword -> keywords.contains(keyword.toDomain())) + .toList(); + + return recordQueryDslRepository.findAllByKeywordsOrderByPopularity(keywordEntities, pageable) + .map(RecordEntity::toDomain); } @Override @@ -68,8 +88,8 @@ public Slice findAllByIdAfterOrderByIdDesc(long cursor, Pageable pageabl @Override public Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keywords, long cursor, Pageable pageable) { - List keywordEntities = keywords.stream() - .map(KeywordEntity::from) + List keywordEntities = keywordJpaRepository.findAll().stream() + .filter(keyword -> keywords.contains(keyword.toDomain())) .toList(); return recordQueryDslRepository.findAllByIdAfterAndKeywordsOrderByIdDesc(keywordEntities, cursor, pageable) @@ -78,9 +98,18 @@ public Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keyw @Override public Slice findAllByUserIdOrderByIdDesc(long userId, long cursor, Pageable pageable) { - UserEntity userEntity = userJpaRepository.findById(userId) - .orElseThrow(() -> new UserException(ErrorMessage.USER_NOT_FOUND)); - return recordQueryDslRepository.findAllByUserIdOrderByIdDesc(userEntity,cursor, pageable) + return recordQueryDslRepository.findAllByUserIdOrderByIdDesc(userId, cursor, pageable) .map(RecordEntity::toDomain); } + + @Override + public Map countAllByUserIdGroupByKeyword(long userId) { + Map preference = recordQueryDslRepository.countAllByUserIdGroupByKeyword(userId); + + return preference.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().toDomain(), + Map.Entry::getValue + )); + } } diff --git a/src/main/java/org/recordy/server/record/service/FileService.java b/src/main/java/org/recordy/server/record/service/FileService.java index 7822338b..9dcfb491 100644 --- a/src/main/java/org/recordy/server/record/service/FileService.java +++ b/src/main/java/org/recordy/server/record/service/FileService.java @@ -1,7 +1,7 @@ package org.recordy.server.record.service; import org.recordy.server.record.domain.File; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; public interface FileService { diff --git a/src/main/java/org/recordy/server/record/service/RecordService.java b/src/main/java/org/recordy/server/record/service/RecordService.java index 8ef51523..4a1a46fc 100644 --- a/src/main/java/org/recordy/server/record/service/RecordService.java +++ b/src/main/java/org/recordy/server/record/service/RecordService.java @@ -1,6 +1,5 @@ package org.recordy.server.record.service; -import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record.domain.File; import org.recordy.server.record.domain.Record; import org.recordy.server.record.domain.usecase.RecordCreate; @@ -15,9 +14,8 @@ public interface RecordService { void delete(long userId, long recordId); // query - Slice getFamousRecords(long cursorId, int size); - Slice getRecentRecordsLaterThanCursor(long cursorId, int size); - Slice getRecentRecordsByKeywords(List keywords, long cursorId, int size); + void watch(long userId, long recordId); + Slice getFamousRecords(List keywords, int pageNumber, int size); Slice getRecentRecordsByUser(long userId, long cursorId, int size); Slice getRecentRecords(List keywords, Long cursorId, int size); } diff --git a/src/main/java/org/recordy/server/record/controller/dto/FileUrl.java b/src/main/java/org/recordy/server/record/service/dto/FileUrl.java similarity index 62% rename from src/main/java/org/recordy/server/record/controller/dto/FileUrl.java rename to src/main/java/org/recordy/server/record/service/dto/FileUrl.java index 3f73bd2a..2e792e00 100644 --- a/src/main/java/org/recordy/server/record/controller/dto/FileUrl.java +++ b/src/main/java/org/recordy/server/record/service/dto/FileUrl.java @@ -1,4 +1,4 @@ -package org.recordy.server.record.controller.dto; +package org.recordy.server.record.service.dto; public record FileUrl( String videoUrl, diff --git a/src/main/java/org/recordy/server/record/service/impl/FileServiceImpl.java b/src/main/java/org/recordy/server/record/service/impl/FileServiceImpl.java index 90588c95..a05d98f7 100644 --- a/src/main/java/org/recordy/server/record/service/impl/FileServiceImpl.java +++ b/src/main/java/org/recordy/server/record/service/impl/FileServiceImpl.java @@ -2,11 +2,11 @@ import lombok.RequiredArgsConstructor; import org.recordy.server.common.message.ErrorMessage; -import org.recordy.server.external.service.S3Service; +import org.recordy.server.common.service.S3Service; import org.recordy.server.record.exception.RecordException; import org.recordy.server.record.service.FileService; import org.recordy.server.record.domain.File; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; import org.springframework.stereotype.Service; import java.io.IOException; diff --git a/src/main/java/org/recordy/server/record/service/impl/RecordServiceImpl.java b/src/main/java/org/recordy/server/record/service/impl/RecordServiceImpl.java index 217403d0..d6363464 100644 --- a/src/main/java/org/recordy/server/record/service/impl/RecordServiceImpl.java +++ b/src/main/java/org/recordy/server/record/service/impl/RecordServiceImpl.java @@ -1,7 +1,6 @@ package org.recordy.server.record.service.impl; import lombok.RequiredArgsConstructor; -import org.recordy.server.common.exception.RecordyException; import org.recordy.server.common.message.ErrorMessage; import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record.domain.File; @@ -11,7 +10,9 @@ import org.recordy.server.record.repository.RecordRepository; import org.recordy.server.record.service.FileService; import org.recordy.server.record.service.RecordService; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.record_stat.repository.ViewRepository; import org.recordy.server.user.domain.User; import org.recordy.server.user.exception.UserException; import org.recordy.server.user.service.UserService; @@ -20,13 +21,14 @@ import org.springframework.stereotype.Service; import java.util.List; -import java.util.stream.Collectors; +import java.util.Objects; @RequiredArgsConstructor @Service public class RecordServiceImpl implements RecordService { private final RecordRepository recordRepository; + private final ViewRepository viewRepository; private final FileService fileService; private final UserService userService; @@ -49,25 +51,39 @@ public Record create(RecordCreate recordCreate, File file) { public void delete(long userId, long recordId) { Record record = recordRepository.findById(recordId) .orElseThrow(() -> new RecordException(ErrorMessage.RECORD_NOT_FOUND)); - if (!record.isUploader(recordId)) { - throw new RecordyException(ErrorMessage.FORBIDDEN_DELETE_RECORD); + if (!record.isUploader(userId)) { + throw new RecordException(ErrorMessage.FORBIDDEN_DELETE_RECORD); } recordRepository.deleteById(recordId); } @Override - public Slice getFamousRecords(long cursorId, int size) { - return null; + public void watch(long userId, long recordId) { + User user = userService.getById(userId) + .orElseThrow(() -> new UserException(ErrorMessage.USER_NOT_FOUND)); + Record record = recordRepository.findById(recordId) + .orElseThrow(() -> new RecordException(ErrorMessage.RECORD_NOT_FOUND)); + viewRepository.save(View.builder() + .record(record) + .user(user) + .build()); } @Override - public Slice getRecentRecordsLaterThanCursor(long cursorId, int size) { - return recordRepository.findAllByIdAfterOrderByIdDesc(cursorId, PageRequest.ofSize(size)); + public Slice getFamousRecords(List keywords, int pageNumber, int size) { + if (Objects.isNull(keywords) || keywords.isEmpty()) { + return getFamousRecords(pageNumber, size); + } + + return getFamousRecordsWithKeywords(Keyword.from(keywords), pageNumber, size); } - @Override - public Slice getRecentRecordsByKeywords(List keywords, long cursorId, int size) { - return recordRepository.findAllByIdAfterAndKeywordsOrderByIdDesc(keywords, cursorId, PageRequest.ofSize(size)); + private Slice getFamousRecords(int pageNumber, int size) { + return recordRepository.findAllOrderByPopularity(PageRequest.of(pageNumber, size)); + } + + private Slice getFamousRecordsWithKeywords(List keywords, int pageNumber, int size) { + return recordRepository.findAllByKeywordsOrderByPopularity(keywords, PageRequest.of(pageNumber, size)); } @Override @@ -77,12 +93,18 @@ public Slice getRecentRecordsByUser(long userId, long cursorId, int size @Override public Slice getRecentRecords(List keywords, Long cursorId, int size) { - if (keywords == null || keywords.isEmpty()) { - return getRecentRecordsLaterThanCursor(cursorId, size); + if (Objects.isNull(keywords) || keywords.isEmpty()) { + return getRecentRecords(cursorId, size); } - List keywordEnums = keywords.stream() - .map(Keyword::valueOf) - .collect(Collectors.toList()); - return getRecentRecordsByKeywords(keywordEnums, cursorId, size); + + return getRecentRecordsWithKeywords(Keyword.from(keywords), cursorId, size); + } + + private Slice getRecentRecords(long cursorId, int size) { + return recordRepository.findAllByIdAfterOrderByIdDesc(cursorId, PageRequest.ofSize(size)); + } + + private Slice getRecentRecordsWithKeywords(List keywords, long cursorId, int size) { + return recordRepository.findAllByIdAfterAndKeywordsOrderByIdDesc(keywords, cursorId, PageRequest.ofSize(size)); } } diff --git a/src/main/java/org/recordy/server/record_stat/domain/Bookmark.java b/src/main/java/org/recordy/server/record_stat/domain/Bookmark.java index b277cc92..eeca0279 100644 --- a/src/main/java/org/recordy/server/record_stat/domain/Bookmark.java +++ b/src/main/java/org/recordy/server/record_stat/domain/Bookmark.java @@ -1,5 +1,6 @@ package org.recordy.server.record_stat.domain; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -14,4 +15,5 @@ public class Bookmark { private Long id; private User user; private Record record; + private LocalDateTime createdAt; } \ No newline at end of file diff --git a/src/main/java/org/recordy/server/record_stat/domain/BookmarkEntity.java b/src/main/java/org/recordy/server/record_stat/domain/BookmarkEntity.java index 66f98859..925d39c6 100644 --- a/src/main/java/org/recordy/server/record_stat/domain/BookmarkEntity.java +++ b/src/main/java/org/recordy/server/record_stat/domain/BookmarkEntity.java @@ -1,10 +1,12 @@ package org.recordy.server.record_stat.domain; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.recordy.server.common.domain.JpaMetaInfoEntity; import org.recordy.server.record.domain.RecordEntity; import org.recordy.server.user.domain.UserEntity; @@ -12,7 +14,7 @@ @Getter @Table(name = "bookmarks") @Entity -public class BookmarkEntity { +public class BookmarkEntity extends JpaMetaInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -27,18 +29,22 @@ public class BookmarkEntity { private UserEntity user; @Builder - public BookmarkEntity(Long id, RecordEntity record, UserEntity user) { + public BookmarkEntity(Long id, RecordEntity record, UserEntity user, LocalDateTime createdAt) { this.id = id; this.record = record; this.user = user; } public static BookmarkEntity from(Bookmark bookmark) { - return BookmarkEntity.builder() + BookmarkEntity bookmarkEntity = BookmarkEntity.builder() .id(bookmark.getId()) .record(RecordEntity.from(bookmark.getRecord())) .user(UserEntity.from(bookmark.getUser())) + .createdAt(bookmark.getCreatedAt()) .build(); + bookmarkEntity.getRecord().addBookmark(bookmarkEntity); + + return bookmarkEntity; } public Bookmark toDomain() { @@ -46,6 +52,11 @@ public Bookmark toDomain() { .id(id) .record(record.toDomain()) .user(user.toDomain()) + .createdAt(createdAt) .build(); } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/src/main/java/org/recordy/server/record_stat/domain/View.java b/src/main/java/org/recordy/server/record_stat/domain/View.java index 42f719de..087b69ff 100644 --- a/src/main/java/org/recordy/server/record_stat/domain/View.java +++ b/src/main/java/org/recordy/server/record_stat/domain/View.java @@ -1,9 +1,19 @@ package org.recordy.server.record_stat.domain; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.recordy.server.user.domain.User; import org.recordy.server.record.domain.Record; -public record View( - long userId, - Record record -) { +@AllArgsConstructor +@Builder +@Getter +public class View { + + private Long id; + private User user; + private Record record; + private LocalDateTime createdAt; } diff --git a/src/main/java/org/recordy/server/record_stat/domain/ViewEntity.java b/src/main/java/org/recordy/server/record_stat/domain/ViewEntity.java new file mode 100644 index 00000000..57a790c0 --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/domain/ViewEntity.java @@ -0,0 +1,69 @@ +package org.recordy.server.record_stat.domain; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.*; +import org.recordy.server.common.domain.JpaMetaInfoEntity; +import org.recordy.server.record.domain.RecordEntity; +import org.recordy.server.user.domain.UserEntity; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "views") +@Entity +public class ViewEntity extends JpaMetaInfoEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "record_id") + private RecordEntity record; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserEntity user; + + @Builder + private ViewEntity(Long id, RecordEntity record, UserEntity user, LocalDateTime createdAt) { + this.id = id; + this.record = record; + this.user = user; + } + + public static ViewEntity of(RecordEntity record, UserEntity user) { + ViewEntity view = ViewEntity.builder() + .record(record) + .user(user) + .build(); + record.addView(view); + + return view; + } + + public static ViewEntity from(View view) { + ViewEntity viewEntity = ViewEntity.builder() + .id(view.getId()) + .record(RecordEntity.from(view.getRecord())) + .user(UserEntity.from(view.getUser())) + .createdAt(view.getCreatedAt()) + .build(); + viewEntity.getRecord().addView(viewEntity); + + return viewEntity; + } + + public View toDomain() { + return View.builder() + .id(id) + .record(record.toDomain()) + .user(user.toDomain()) + .createdAt(createdAt) + .build(); + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/src/main/java/org/recordy/server/record_stat/domain/usecase/Preference.java b/src/main/java/org/recordy/server/record_stat/domain/usecase/Preference.java index 9192f28b..0bddadee 100644 --- a/src/main/java/org/recordy/server/record_stat/domain/usecase/Preference.java +++ b/src/main/java/org/recordy/server/record_stat/domain/usecase/Preference.java @@ -2,10 +2,45 @@ import org.recordy.server.keyword.domain.Keyword; +import java.security.Key; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import java.util.stream.Stream; public record Preference( long userId, - Map preference + Map preference ) { + + public static Preference of(long userId, Map preference) { + Map topPreference = preference.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(3) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (a, b) -> a, + LinkedHashMap::new)); + + return new Preference(userId, normalize(topPreference)); + } + + private static Map normalize(Map preference) { + long sum = sum(preference); + + return preference.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue() * 100 / sum, + (a, b) -> a, + LinkedHashMap::new)); + } + + private static long sum(Map preference) { + return preference.values().stream() + .mapToLong(Long::longValue) + .sum(); + } } diff --git a/src/main/java/org/recordy/server/record_stat/repository/BookmarkQueryDslRepository.java b/src/main/java/org/recordy/server/record_stat/repository/BookmarkQueryDslRepository.java deleted file mode 100644 index f62dd813..00000000 --- a/src/main/java/org/recordy/server/record_stat/repository/BookmarkQueryDslRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.recordy.server.record_stat.repository; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.recordy.server.common.util.QueryDslUtils; -import org.recordy.server.keyword.domain.QKeywordEntity; -import org.recordy.server.record.domain.QRecordEntity; -import org.recordy.server.record.domain.QUploadEntity; -import org.recordy.server.record.domain.RecordEntity; -import org.recordy.server.record_stat.domain.BookmarkEntity; -import org.recordy.server.record_stat.domain.QBookmarkEntity; -import org.recordy.server.user.domain.QUserEntity; -import org.recordy.server.user.domain.UserEntity; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.stereotype.Repository; - -@RequiredArgsConstructor -@Repository -public class BookmarkQueryDslRepository { - - private final JPAQueryFactory jpaQueryFactory; - - public Slice findAllByUserOrderByIdDesc(UserEntity userEntity, long cursor, Pageable pageable) { - List bookmarkEntities = jpaQueryFactory - .selectFrom(QBookmarkEntity.bookmarkEntity) - .join(QBookmarkEntity.bookmarkEntity.record, QRecordEntity.recordEntity) - .join(QBookmarkEntity.bookmarkEntity.user, QUserEntity.userEntity) - .where( - QueryDslUtils.ltCursorId(cursor, QBookmarkEntity.bookmarkEntity.id), - QUserEntity.userEntity.eq(userEntity) - - ) - .orderBy(QBookmarkEntity.bookmarkEntity.id.desc()) - .limit(pageable.getPageSize() + 1) - .fetch(); - - return new SliceImpl<>(bookmarkEntities, pageable, QueryDslUtils.hasNext(pageable, bookmarkEntities)); - } - -} diff --git a/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepository.java b/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepository.java index 84eb2b57..18588469 100644 --- a/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepository.java +++ b/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepository.java @@ -1,15 +1,21 @@ package org.recordy.server.record_stat.repository; +import java.util.Optional; +import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record_stat.domain.Bookmark; +import org.recordy.server.user.domain.User; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import java.util.Map; + public interface BookmarkRepository { // command Bookmark save(Bookmark bookmark); + void deleteById(long bookmarkId); //query Slice findAllByBookmarksOrderByIdDesc(long userId, long cursor, Pageable pageable); - Long countAllByRecordId(long recordId); + Optional findByUserAndRecord(long userId, long recordId); } diff --git a/src/main/java/org/recordy/server/record_stat/repository/ViewRepository.java b/src/main/java/org/recordy/server/record_stat/repository/ViewRepository.java new file mode 100644 index 00000000..7d9f0b2b --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/repository/ViewRepository.java @@ -0,0 +1,16 @@ +package org.recordy.server.record_stat.repository; + +import org.recordy.server.keyword.domain.Keyword; +import org.recordy.server.record.domain.Record; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.user.domain.User; + +import java.util.Map; + +public interface ViewRepository { + + // command + View save(View view); + + // query +} diff --git a/src/main/java/org/recordy/server/record_stat/repository/BookmarkJpaRepository.java b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkJpaRepository.java similarity index 54% rename from src/main/java/org/recordy/server/record_stat/repository/BookmarkJpaRepository.java rename to src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkJpaRepository.java index 86fa923c..59fcfe91 100644 --- a/src/main/java/org/recordy/server/record_stat/repository/BookmarkJpaRepository.java +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkJpaRepository.java @@ -1,9 +1,11 @@ -package org.recordy.server.record_stat.repository; +package org.recordy.server.record_stat.repository.impl; +import java.util.Optional; import org.recordy.server.record_stat.domain.BookmarkEntity; import org.springframework.data.jpa.repository.JpaRepository; public interface BookmarkJpaRepository extends JpaRepository { - Long countAllByRecord_Id(long recordId); + //query + Optional findByUser_IdAndRecord_Id(long userId, long recordId); } diff --git a/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkQueryDslRepository.java b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkQueryDslRepository.java new file mode 100644 index 00000000..7fbb53e7 --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkQueryDslRepository.java @@ -0,0 +1,66 @@ +package org.recordy.server.record_stat.repository.impl; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import org.recordy.server.common.util.QueryDslUtils; +import org.recordy.server.keyword.domain.KeywordEntity; +import org.recordy.server.record.domain.QRecordEntity; +import org.recordy.server.record_stat.domain.BookmarkEntity; +import org.recordy.server.record_stat.domain.QBookmarkEntity; +import org.recordy.server.user.domain.QUserEntity; +import org.recordy.server.user.domain.UserEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import static org.recordy.server.record.domain.QRecordEntity.recordEntity; +import static org.recordy.server.record.domain.QUploadEntity.uploadEntity; +import static org.recordy.server.record_stat.domain.QBookmarkEntity.bookmarkEntity; +import static org.recordy.server.record_stat.domain.QViewEntity.viewEntity; + +@RequiredArgsConstructor +@Repository +public class BookmarkQueryDslRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Slice findAllByUserOrderByIdDesc(UserEntity userEntity, long cursor, Pageable pageable) { + List bookmarkEntities = jpaQueryFactory + .selectFrom(bookmarkEntity) + .join(bookmarkEntity.record, recordEntity) + .join(bookmarkEntity.user, QUserEntity.userEntity) + .where( + QueryDslUtils.ltCursorId(cursor, bookmarkEntity.id), + QUserEntity.userEntity.eq(userEntity) + + ) + .orderBy(bookmarkEntity.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(bookmarkEntities, pageable, QueryDslUtils.hasNext(pageable, bookmarkEntities)); + } + + public Map countAllByUserIdGroupByKeyword(long userId) { + List results = jpaQueryFactory + .select(uploadEntity.keyword, bookmarkEntity.count()) + .from(bookmarkEntity) + .join(bookmarkEntity.record, recordEntity) + .join(recordEntity.uploads, uploadEntity) + .where(bookmarkEntity.user.id.eq(userId)) + .groupBy(uploadEntity.keyword) + .fetch(); + + return results.stream() + .collect(Collectors.toMap( + tuple -> tuple.get(uploadEntity.keyword), + tuple -> tuple.get(viewEntity.count()))); + } +} diff --git a/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepositoryImpl.java b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkRepositoryImpl.java similarity index 68% rename from src/main/java/org/recordy/server/record_stat/repository/BookmarkRepositoryImpl.java rename to src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkRepositoryImpl.java index b918cf69..79e1630b 100644 --- a/src/main/java/org/recordy/server/record_stat/repository/BookmarkRepositoryImpl.java +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/BookmarkRepositoryImpl.java @@ -1,10 +1,13 @@ -package org.recordy.server.record_stat.repository; +package org.recordy.server.record_stat.repository.impl; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.recordy.server.common.message.ErrorMessage; +import org.recordy.server.keyword.domain.Keyword; +import org.recordy.server.keyword.domain.KeywordEntity; import org.recordy.server.record_stat.domain.Bookmark; import org.recordy.server.record_stat.domain.BookmarkEntity; -import org.recordy.server.user.domain.User; +import org.recordy.server.record_stat.repository.BookmarkRepository; import org.recordy.server.user.domain.UserEntity; import org.recordy.server.user.exception.UserException; import org.recordy.server.user.repository.impl.UserJpaRepository; @@ -12,6 +15,9 @@ import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; +import java.util.Map; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Repository public class BookmarkRepositoryImpl implements BookmarkRepository { @@ -26,6 +32,7 @@ public Bookmark save(Bookmark bookmark) { .toDomain(); } + @Override public Slice findAllByBookmarksOrderByIdDesc(long userId, long cursor, Pageable pageable) { UserEntity userEntity = userJpaRepository.findById(userId) @@ -35,7 +42,13 @@ public Slice findAllByBookmarksOrderByIdDesc(long userId, long cursor, } @Override - public Long countAllByRecordId(long recordId) { - return bookmarkJpaRepository.countAllByRecord_Id(recordId); + public void deleteById(long bookmarkId) { + bookmarkJpaRepository.deleteById(bookmarkId); + } + + @Override + public Optional findByUserAndRecord(long userId, long recordId) { + return bookmarkJpaRepository.findByUser_IdAndRecord_Id(userId, recordId) + .map(BookmarkEntity::toDomain); } } diff --git a/src/main/java/org/recordy/server/record_stat/repository/impl/ViewJpaRepository.java b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewJpaRepository.java new file mode 100644 index 00000000..96eabb92 --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewJpaRepository.java @@ -0,0 +1,7 @@ +package org.recordy.server.record_stat.repository.impl; + +import org.recordy.server.record_stat.domain.ViewEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ViewJpaRepository extends JpaRepository { +} diff --git a/src/main/java/org/recordy/server/record_stat/repository/impl/ViewQueryDslRepository.java b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewQueryDslRepository.java new file mode 100644 index 00000000..c794c289 --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewQueryDslRepository.java @@ -0,0 +1,38 @@ +package org.recordy.server.record_stat.repository.impl; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.recordy.server.keyword.domain.KeywordEntity; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.recordy.server.record.domain.QRecordEntity.recordEntity; +import static org.recordy.server.record.domain.QUploadEntity.uploadEntity; +import static org.recordy.server.record_stat.domain.QViewEntity.viewEntity; + +@RequiredArgsConstructor +@Repository +public class ViewQueryDslRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Map countAllByUserIdGroupByKeyword(long userId) { + List results = jpaQueryFactory + .select(uploadEntity.keyword, viewEntity.count()) + .from(viewEntity) + .join(viewEntity.record, recordEntity) + .join(recordEntity.uploads, uploadEntity) + .where(viewEntity.user.id.eq(userId)) + .groupBy(uploadEntity.keyword) + .fetch(); + + return results.stream() + .collect(Collectors.toMap( + tuple -> tuple.get(uploadEntity.keyword), + tuple -> tuple.get(viewEntity.count()))); + } +} diff --git a/src/main/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryImpl.java b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryImpl.java new file mode 100644 index 00000000..0bdb532b --- /dev/null +++ b/src/main/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryImpl.java @@ -0,0 +1,30 @@ +package org.recordy.server.record_stat.repository.impl; + +import lombok.RequiredArgsConstructor; +import org.recordy.server.keyword.domain.Keyword; +import org.recordy.server.keyword.domain.KeywordEntity; +import org.recordy.server.record.domain.Record; +import org.recordy.server.record.domain.RecordEntity; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.record_stat.domain.ViewEntity; +import org.recordy.server.record_stat.repository.ViewRepository; +import org.recordy.server.user.domain.User; +import org.recordy.server.user.domain.UserEntity; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Repository +public class ViewRepositoryImpl implements ViewRepository { + + private final ViewJpaRepository viewJpaRepository; + private final ViewQueryDslRepository viewQueryDslRepository; + + @Override + public View save(View view) { + return viewJpaRepository.save(ViewEntity.from(view)) + .toDomain(); + } +} diff --git a/src/main/java/org/recordy/server/record_stat/service/RecordStatService.java b/src/main/java/org/recordy/server/record_stat/service/RecordStatService.java index 5e829e3a..b9dedc88 100644 --- a/src/main/java/org/recordy/server/record_stat/service/RecordStatService.java +++ b/src/main/java/org/recordy/server/record_stat/service/RecordStatService.java @@ -9,9 +9,9 @@ public interface RecordStatService { // command Bookmark bookmark(long userId, long recordId); + void deleteBookmark(long userId, long recordId); // query Preference getPreference(long userId); Slice getBookmarkedRecords(long userId, long cursorId, int size); - long getBookmarkCount(long recordId); } diff --git a/src/main/java/org/recordy/server/record_stat/service/impl/RecordStatServiceImpl.java b/src/main/java/org/recordy/server/record_stat/service/impl/RecordStatServiceImpl.java index 49dd17b7..c071c613 100644 --- a/src/main/java/org/recordy/server/record_stat/service/impl/RecordStatServiceImpl.java +++ b/src/main/java/org/recordy/server/record_stat/service/impl/RecordStatServiceImpl.java @@ -1,5 +1,6 @@ package org.recordy.server.record_stat.service.impl; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.recordy.server.common.message.ErrorMessage; import org.recordy.server.record.domain.Record; @@ -37,9 +38,18 @@ public Bookmark bookmark(long userId, long recordId) { .build()); } + @Override + public void deleteBookmark(long userId, long recordId) { + Optional optionalBookmark = bookmarkRepository.findByUserAndRecord(userId, recordId); + if (optionalBookmark.isPresent()) { + Bookmark bookmark = optionalBookmark.get(); + bookmarkRepository.deleteById(bookmark.getId()); + } + } + @Override public Preference getPreference(long userId) { - return null; + return Preference.of(userId, recordRepository.countAllByUserIdGroupByKeyword(userId)); } @Override @@ -47,9 +57,4 @@ public Slice getBookmarkedRecords(long userId, long cursorId, int size) return bookmarkRepository.findAllByBookmarksOrderByIdDesc(userId, cursorId, PageRequest.ofSize(size)) .map(Bookmark::getRecord); } - - @Override - public long getBookmarkCount(long recordId) { - return bookmarkRepository.countAllByRecordId(recordId); - } } diff --git a/src/test/java/org/recordy/server/auth/service/impl/token/AuthTokenParserTest.java b/src/test/java/org/recordy/server/auth/service/impl/token/AuthTokenParserTest.java index 28e650dd..a05ffdc6 100644 --- a/src/test/java/org/recordy/server/auth/service/impl/token/AuthTokenParserTest.java +++ b/src/test/java/org/recordy/server/auth/service/impl/token/AuthTokenParserTest.java @@ -26,7 +26,7 @@ void init() { @Test void getBody를_통해_토큰의_내용을_읽을_수_있다() { // given - String token = authTokenGenerator.generate(Map.of("A", "a", "B", "b"), 1000L); + String token = authTokenGenerator.generate(Map.of("A", "a", "B", "b"), 10000000L); // when Claims body = authTokenParser.getBody(token); @@ -34,7 +34,7 @@ void init() { // then assertThat(body.get("A")).isEqualTo("a"); assertThat(body.get("B")).isEqualTo("b"); - assertThat(body.getExpiration().getTime() - body.getIssuedAt().getTime()).isEqualTo(1000L); + assertThat(body.getExpiration().getTime() - body.getIssuedAt().getTime()).isEqualTo(10000000L); } @Test diff --git a/src/test/java/org/recordy/server/external/controller/S3TestControllerTest.java b/src/test/java/org/recordy/server/external/controller/S3TestControllerTest.java index cdb28b50..e055994c 100644 --- a/src/test/java/org/recordy/server/external/controller/S3TestControllerTest.java +++ b/src/test/java/org/recordy/server/external/controller/S3TestControllerTest.java @@ -2,7 +2,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.recordy.server.external.service.impl.S3ServiceImpl; +import org.recordy.server.common.controller.S3TestController; +import org.recordy.server.common.service.impl.S3ServiceImpl; import org.recordy.server.mock.FakeContainer; import org.springframework.mock.web.MockMultipartFile; diff --git a/src/test/java/org/recordy/server/external/service/S3ServiceTest.java b/src/test/java/org/recordy/server/external/service/S3ServiceTest.java index 2aedffb8..27b7275b 100644 --- a/src/test/java/org/recordy/server/external/service/S3ServiceTest.java +++ b/src/test/java/org/recordy/server/external/service/S3ServiceTest.java @@ -2,11 +2,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.recordy.server.external.config.S3Config; -import org.recordy.server.external.exception.ExternalException; -import org.recordy.server.external.service.impl.S3ServiceImpl; -import org.recordy.server.mock.FakeContainer; -import org.recordy.server.user.exception.UserException; +import org.recordy.server.common.config.S3Config; +import org.recordy.server.common.exception.ExternalException; +import org.recordy.server.common.service.impl.S3ServiceImpl; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; diff --git a/src/test/java/org/recordy/server/keyword/repository/KeywordRepositoryIntegrationTest.java b/src/test/java/org/recordy/server/keyword/repository/KeywordRepositoryIntegrationTest.java index 4bb21eff..7ff263f2 100644 --- a/src/test/java/org/recordy/server/keyword/repository/KeywordRepositoryIntegrationTest.java +++ b/src/test/java/org/recordy/server/keyword/repository/KeywordRepositoryIntegrationTest.java @@ -15,8 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertAll; @SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), @Sql(value = "/sql/keyword-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/keyword-repository-test-clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) @SpringBootTest class KeywordRepositoryIntegrationTest extends IntegrationTest { diff --git a/src/test/java/org/recordy/server/mock/FakeContainer.java b/src/test/java/org/recordy/server/mock/FakeContainer.java index 4e20e807..b10f772b 100644 --- a/src/test/java/org/recordy/server/mock/FakeContainer.java +++ b/src/test/java/org/recordy/server/mock/FakeContainer.java @@ -12,7 +12,7 @@ import org.recordy.server.auth.service.impl.token.AuthTokenParser; import org.recordy.server.auth.service.impl.token.AuthTokenServiceImpl; import org.recordy.server.auth.service.impl.token.AuthTokenSigningKeyProvider; -import org.recordy.server.external.service.S3Service; +import org.recordy.server.common.service.S3Service; import org.recordy.server.keyword.repository.KeywordRepository; import org.recordy.server.keyword.service.KeywordService; import org.recordy.server.keyword.service.impl.KeywordServiceImpl; @@ -25,11 +25,13 @@ import org.recordy.server.mock.record.FakeFileService; import org.recordy.server.mock.record.FakeRecordRepository; import org.recordy.server.mock.user.FakeUserRepository; +import org.recordy.server.mock.view.FakeViewRepository; import org.recordy.server.record.repository.RecordRepository; import org.recordy.server.record.service.FileService; import org.recordy.server.record.service.RecordService; import org.recordy.server.record.service.impl.RecordServiceImpl; import org.recordy.server.record_stat.repository.BookmarkRepository; +import org.recordy.server.record_stat.repository.ViewRepository; import org.recordy.server.record_stat.service.RecordStatService; import org.recordy.server.record_stat.service.impl.RecordStatServiceImpl; import org.recordy.server.user.controller.UserController; @@ -50,6 +52,7 @@ public class FakeContainer { public final RecordRepository recordRepository; public final KeywordRepository keywordRepository; public final BookmarkRepository bookmarkRepository; + public final ViewRepository viewRepository; // infrastructure public final AuthTokenSigningKeyProvider authTokenSigningKeyProvider; @@ -83,6 +86,7 @@ public FakeContainer() { this.recordRepository = new FakeRecordRepository(); this.keywordRepository = new FakeKeywordRepository(); this.bookmarkRepository = new FakeBookmarkRepository(); + this.viewRepository = new FakeViewRepository(); this.authTokenSigningKeyProvider = new AuthTokenSigningKeyProvider(DomainFixture.TOKEN_SECRET); this.authTokenGenerator = new AuthTokenGenerator(authTokenSigningKeyProvider); @@ -108,7 +112,7 @@ public FakeContainer() { this.authService = new AuthServiceImpl(authRepository, authPlatformServiceFactory, authTokenService); this.userService = new UserServiceImpl(userRepository, authService, authTokenService); this.fileService = new FakeFileService(); - this.recordService = new RecordServiceImpl(recordRepository, fileService, userService); + this.recordService = new RecordServiceImpl(recordRepository, viewRepository, fileService, userService); this.keywordService = new KeywordServiceImpl(keywordRepository); this.recordStatService = new RecordStatServiceImpl(userRepository, recordRepository, bookmarkRepository); diff --git a/src/test/java/org/recordy/server/mock/bookmark/FakeBookmarkRepository.java b/src/test/java/org/recordy/server/mock/bookmark/FakeBookmarkRepository.java index 9ff3c21c..1d8c8bb6 100644 --- a/src/test/java/org/recordy/server/mock/bookmark/FakeBookmarkRepository.java +++ b/src/test/java/org/recordy/server/mock/bookmark/FakeBookmarkRepository.java @@ -4,9 +4,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.recordy.server.record.domain.Record; + +import java.util.Optional; +import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record_stat.domain.Bookmark; import org.recordy.server.record_stat.repository.BookmarkRepository; +import org.recordy.server.user.domain.User; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -28,6 +31,11 @@ public Bookmark save(Bookmark bookmark) { return realBookmark; } + @Override + public void deleteById(long bookmarkId) { + bookmarks.remove(bookmarkId); + } + @Override public Slice findAllByBookmarksOrderByIdDesc(long userId, long cursor, Pageable pageable) { List content = bookmarks.keySet().stream() @@ -43,10 +51,9 @@ public Slice findAllByBookmarksOrderByIdDesc(long userId, long cursor, } @Override - public Long countAllByRecordId(long recordId) { - return bookmarks.keySet().stream() - .filter(key -> bookmarks.get(key).getRecord().getId() == recordId) - .map(bookmarks::get) - .count(); + public Optional findByUserAndRecord(long userId, long recordId) { + return bookmarks.values().stream() + .filter(bookmark -> bookmark.getUser().getId() == userId && bookmark.getRecord().getId() == recordId) + .findFirst(); } } diff --git a/src/test/java/org/recordy/server/mock/record/FakeFileService.java b/src/test/java/org/recordy/server/mock/record/FakeFileService.java index 156851be..8690a71f 100644 --- a/src/test/java/org/recordy/server/mock/record/FakeFileService.java +++ b/src/test/java/org/recordy/server/mock/record/FakeFileService.java @@ -2,7 +2,7 @@ import org.recordy.server.record.domain.File; import org.recordy.server.record.service.FileService; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; import org.recordy.server.util.DomainFixture; public class FakeFileService implements FileService { diff --git a/src/test/java/org/recordy/server/mock/record/FakeRecordRepository.java b/src/test/java/org/recordy/server/mock/record/FakeRecordRepository.java index e868622d..a1573150 100644 --- a/src/test/java/org/recordy/server/mock/record/FakeRecordRepository.java +++ b/src/test/java/org/recordy/server/mock/record/FakeRecordRepository.java @@ -56,7 +56,12 @@ public Optional findById(long recordId) { } @Override - public Slice findAllOrderByPopularity(long cursor, Pageable pageable) { + public Slice findAllOrderByPopularity(Pageable pageable) { + return null; + } + + @Override + public Slice findAllByKeywordsOrderByPopularity(List keywords, Pageable pageable) { return null; } @@ -82,7 +87,7 @@ public Slice findAllByIdAfterAndKeywordsOrderByIdDesc(List keyw @Override public Slice findAllByUserIdOrderByIdDesc(long userId, long cursor, Pageable pageable) { List content = records.values().stream() - .filter(record -> record.getId() < cursor && record.getId() == userId) + .filter(record -> record.getId() < cursor && record.getUploader().getId() == userId) .sorted(Comparator.comparing(Record::getId).reversed()) .toList(); @@ -91,4 +96,9 @@ public Slice findAllByUserIdOrderByIdDesc(long userId, long cursor, Page return new SliceImpl<>(content.subList(0, pageable.getPageSize()), pageable, true); } + + @Override + public Map countAllByUserIdGroupByKeyword(long userId) { + return Map.of(); + } } diff --git a/src/test/java/org/recordy/server/mock/view/FakeViewRepository.java b/src/test/java/org/recordy/server/mock/view/FakeViewRepository.java new file mode 100644 index 00000000..da0259c8 --- /dev/null +++ b/src/test/java/org/recordy/server/mock/view/FakeViewRepository.java @@ -0,0 +1,34 @@ +package org.recordy.server.mock.view; + +import org.recordy.server.keyword.domain.Keyword; +import org.recordy.server.keyword.domain.KeywordEntity; +import org.recordy.server.record.domain.Record; +import org.recordy.server.record.domain.RecordEntity; +import org.recordy.server.record.domain.UploadEntity; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.record_stat.domain.ViewEntity; +import org.recordy.server.record_stat.repository.ViewRepository; +import org.recordy.server.user.domain.User; +import org.recordy.server.user.domain.UserEntity; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class FakeViewRepository implements ViewRepository { + + public long autoIncrementId = 1L; + public final Map viewEntities = new HashMap<>(); + + @Override + public View save(View view) { + View realView = View.builder() + .id(autoIncrementId) + .user(view.getUser()) + .record(view.getRecord()) + .build(); + + viewEntities.put(autoIncrementId++, realView); + return view; + } +} diff --git a/src/test/java/org/recordy/server/record/domain/RecordEntityTest.java b/src/test/java/org/recordy/server/record/domain/RecordEntityTest.java index f80bad7c..6ce9d9f2 100644 --- a/src/test/java/org/recordy/server/record/domain/RecordEntityTest.java +++ b/src/test/java/org/recordy/server/record/domain/RecordEntityTest.java @@ -1,7 +1,7 @@ package org.recordy.server.record.domain; import org.junit.jupiter.api.Test; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; import org.recordy.server.user.domain.UserEntity; import org.recordy.server.user.domain.UserStatus; import org.recordy.server.util.DomainFixture; diff --git a/src/test/java/org/recordy/server/record/repository/RecordRepositoryIntegrationTest.java b/src/test/java/org/recordy/server/record/repository/RecordRepositoryIntegrationTest.java index bda7130e..a33ffcf9 100644 --- a/src/test/java/org/recordy/server/record/repository/RecordRepositoryIntegrationTest.java +++ b/src/test/java/org/recordy/server/record/repository/RecordRepositoryIntegrationTest.java @@ -1,12 +1,25 @@ package org.recordy.server.record.repository; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.recordy.server.keyword.domain.KeywordEntity; -import org.recordy.server.keyword.repository.impl.KeywordJpaRepository; +import org.recordy.server.keyword.domain.Keyword; import org.recordy.server.record.domain.Record; import org.recordy.server.record.domain.RecordEntity; import org.recordy.server.record.domain.UploadEntity; import org.recordy.server.record.repository.impl.UploadJpaRepository; +import org.recordy.server.record.service.dto.FileUrl; +import org.recordy.server.record_stat.domain.Bookmark; +import org.recordy.server.record_stat.domain.BookmarkEntity; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.record_stat.domain.ViewEntity; +import org.recordy.server.record_stat.repository.BookmarkRepository; +import org.recordy.server.record_stat.repository.ViewRepository; +import org.recordy.server.record_stat.repository.impl.BookmarkJpaRepository; +import org.recordy.server.record_stat.repository.impl.ViewJpaRepository; +import org.recordy.server.user.domain.UserStatus; import org.recordy.server.util.DomainFixture; import org.recordy.server.util.db.IntegrationTest; import org.springframework.beans.factory.annotation.Autowired; @@ -18,13 +31,16 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.recordy.server.util.DomainFixture.*; @SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), @Sql(value = "/sql/record-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/record-repository-test-clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) @Transactional @SpringBootTest @@ -36,10 +52,31 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { @Autowired private UploadJpaRepository uploadRepository; + @Autowired + private BookmarkRepository bookmarkRepository; + @Autowired + private BookmarkJpaRepository bookmarkJpaRepository; + + @Autowired + private ViewRepository viewRepository; + @Autowired + private ViewJpaRepository viewJpaRepository; + + private static LocalDateTime now; + private static LocalDateTime sevenDaysAgo; + private static LocalDateTime eightDaysAgo; + + @BeforeAll + static void setup() { + now = LocalDateTime.now(); + sevenDaysAgo = now.minus(7, ChronoUnit.DAYS); + eightDaysAgo = now.minus(8, ChronoUnit.DAYS); + } + @Test void save를_통해_레코드_데이터를_저장할_수_있다() { // given - Record record = DomainFixture.createRecord(); + Record record = DomainFixture.createRecord(6); // when Record result = recordRepository.save(record); @@ -50,38 +87,31 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { () -> assertThat(result.getFileUrl().videoUrl()).isEqualTo(DomainFixture.VIDEO_URL), () -> assertThat(result.getFileUrl().thumbnailUrl()).isEqualTo(DomainFixture.THUMBNAIL_URL), () -> assertThat(result.getLocation()).isEqualTo(DomainFixture.LOCATION), - () -> assertThat(result.getContent()).isEqualTo(DomainFixture.CONTENT) + () -> assertThat(result.getContent()).isEqualTo(CONTENT) ); } @Test void save를_통해_레코드와_관련한_키워드로부터_업로드_데이터를_저장할_수_있다() { // given - Record record = DomainFixture.createRecord(); + Record record = recordRepository.save(createRecord(6)); // when - Record savedRecord = recordRepository.save(record); - List uploads = uploadRepository.findAllByRecord(RecordEntity.from(savedRecord)); + List uploads = uploadRepository.findAllByRecord(RecordEntity.from(record)); // then assertAll( () -> assertThat(uploads).hasSize(3), - () -> assertThat(uploads.get(0).getKeyword().toDomain()).isEqualTo(DomainFixture.KEYWORD_1), - () -> assertThat(uploads.get(1).getKeyword().toDomain()).isEqualTo(DomainFixture.KEYWORD_2), - () -> assertThat(uploads.get(2).getKeyword().toDomain()).isEqualTo(DomainFixture.KEYWORD_3) - ); - assertAll( - () -> assertThat(uploads.get(0).getRecord().getId()).isEqualTo(savedRecord.getId()), - () -> assertThat(uploads.get(1).getRecord().getId()).isEqualTo(savedRecord.getId()), - () -> assertThat(uploads.get(2).getRecord().getId()).isEqualTo(savedRecord.getId()) + () -> assertThat(uploads.get(0).getKeyword().toDomain()).isEqualTo(KEYWORD_1), + () -> assertThat(uploads.get(1).getKeyword().toDomain()).isEqualTo(KEYWORD_2), + () -> assertThat(uploads.get(2).getKeyword().toDomain()).isEqualTo(KEYWORD_3) ); } @Test void save를_통해_저장한_업로드는_관련된_레코드를_참조할_수_있다() { // given - Record record = DomainFixture.createRecord(); - Record savedRecord = recordRepository.save(record); + Record savedRecord = recordRepository.save(DomainFixture.createRecord(6)); // when List uploads = uploadRepository.findAllByRecord(RecordEntity.from(savedRecord)); @@ -97,13 +127,9 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { @Test void deleteById를_통해_레코드를_삭제할_수_있다() { - //given - Record record = DomainFixture.createRecord(); - Record savedRecord = recordRepository.save(record); - - //when - recordRepository.deleteById(savedRecord.getId()); - Slice result = recordRepository.findAllByIdAfterOrderByIdDesc(0, PageRequest.ofSize(1)); + // when + recordRepository.deleteById(1); + Slice result = recordRepository.findAllByIdAfterOrderByIdDesc(2, PageRequest.ofSize(1)); //then assertAll( @@ -114,20 +140,21 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { @Test void findAllByUserIdOrderByCreatedAtDesc를_통해_userId를_기반으로_레코드_데이터를_조회할_수_있다() { - //given + // given + // userId 순서 : {1, 1, 2, 2, 1} long userId = 1; long cursor = 4L; int size = 2; //when - Slice result = recordRepository.findAllByIdAfterOrderByIdDesc(cursor, PageRequest.ofSize(size)); + Slice result = recordRepository.findAllByUserIdOrderByIdDesc(userId, cursor, PageRequest.ofSize(size)); //then assertAll( - () -> assertThat(result.get()).hasSize(2), - () -> assertThat(result.getContent().get(0).getId()).isEqualTo(2L), - () -> assertThat(result.getContent().get(1).getId()).isEqualTo(1L), - () -> assertThat((result.hasNext())).isFalse() + () -> assertThat(result.get()).hasSize(2), + () -> assertThat(result.getContent().get(0).getId()).isEqualTo(2L), + () -> assertThat(result.getContent().get(1).getId()).isEqualTo(1L), + () -> assertThat((result.hasNext())).isFalse() ); } @@ -143,12 +170,13 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { // then assertAll( - () -> assertThat(result.getContent()).hasSize(5), - () -> assertThat(result.getContent().get(0).getId()).isEqualTo(5L), - () -> assertThat(result.getContent().get(1).getId()).isEqualTo(4L), - () -> assertThat(result.getContent().get(2).getId()).isEqualTo(3L), - () -> assertThat(result.getContent().get(3).getId()).isEqualTo(2L), - () -> assertThat(result.getContent().get(4).getId()).isEqualTo(1L), + () -> assertThat(result.getContent()).hasSize(6), + () -> assertThat(result.getContent().get(0).getId()).isEqualTo(6L), + () -> assertThat(result.getContent().get(1).getId()).isEqualTo(5L), + () -> assertThat(result.getContent().get(2).getId()).isEqualTo(4L), + () -> assertThat(result.getContent().get(3).getId()).isEqualTo(3L), + () -> assertThat(result.getContent().get(4).getId()).isEqualTo(2L), + () -> assertThat(result.getContent().get(5).getId()).isEqualTo(1L), () -> assertThat(result.hasNext()).isFalse() ); } @@ -186,4 +214,234 @@ class RecordRepositoryIntegrationTest extends IntegrationTest { () -> assertThat(result.hasNext()).isFalse() ); } + + @Test + void findAllByIdAfterAndKeywordsOrderByIdDesc를_통해_키워드로_필터링된_레코드_데이터를_최신순으로_조회할_수_있다() { + // given + List keywords = List.of(DomainFixture.KEYWORD_1, DomainFixture.KEYWORD_2); + long cursor = 3L; + int size = 2; + + // when + Slice result = recordRepository.findAllByIdAfterAndKeywordsOrderByIdDesc(keywords, cursor, PageRequest.ofSize(size)); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(2), + () -> assertThat(result.getContent().get(0).getId()).isEqualTo(2L), + () -> assertThat(result.getContent().get(1).getId()).isEqualTo(1L), + () -> assertThat(result.hasNext()).isFalse() + ); + } + + @Test + void findAllOrderByPopularity를_통해_인기순으로_레코드_데이터를_조회할_수_있다() { + // given + viewRepository.save(View.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(Record.builder() + .id(1L) + .fileUrl(new FileUrl(VIDEO_URL, THUMBNAIL_URL)) + .location(LOCATION) + .content(CONTENT) + .keywords(KEYWORDS) + .uploader(createUser(UserStatus.ACTIVE)) + .build()) + .createdAt(sevenDaysAgo) + .build()); + bookmarkRepository.save(Bookmark.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(Record.builder() + .id(2L) + .fileUrl(new FileUrl(VIDEO_URL, THUMBNAIL_URL)) + .location(LOCATION) + .content(CONTENT) + .keywords(KEYWORDS) + .uploader(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build()) + .build()); + + // when + List result = recordRepository.findAllOrderByPopularity(PageRequest.of(0, 2)) + .getContent(); + + // then + assertAll( + () -> assertThat(result).hasSize(2), + () -> assertThat(result.get(0).getId()).isEqualTo(2L) + ); + } + + @Test + void findAllOrderByPopularity를_통해_계산한_인기순은_7일간의_데이터만_반영한다() { + // given + // 8일 전에 1L,2L을 저장, 7일 전에 3L를 저장 + // 8일 전에 4L,5L을 시청, 7일 전에 6L를 시청 + saveBookmarkWithCreatedAt(1, eightDaysAgo); + saveBookmarkWithCreatedAt(2, eightDaysAgo); + saveBookmarkWithCreatedAt(3, sevenDaysAgo); + saveViewWithCreatedAt(4, eightDaysAgo); + saveViewWithCreatedAt(5, eightDaysAgo); + saveViewWithCreatedAt(6, sevenDaysAgo); + + // when + List result = recordRepository.findAllOrderByPopularity(PageRequest.of(0, 2)) + .getContent(); + + // then + assertAll( + () -> assertThat(result).hasSize(2), + () -> assertThat(result.get(0).getId()).isEqualTo(3L), + () -> assertThat(result.get(1).getId()).isEqualTo(6L) + ); + } + + private void saveBookmarkWithCreatedAt(long recordId, LocalDateTime createdAt) { + Bookmark bookmark = bookmarkRepository.save(Bookmark.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(createRecord(recordId)) + .build()); + BookmarkEntity bookmarkEntity = bookmarkJpaRepository.findById(bookmark.getId()) + .orElseThrow(); + bookmarkEntity.setCreatedAt(createdAt.plusMinutes(1)); + } + + private void saveViewWithCreatedAt(long recordId, LocalDateTime createdAt) { + View view = viewRepository.save(View.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(createRecord(recordId)) + .build()); + ViewEntity viewEntity = viewJpaRepository.findById(view.getId()) + .orElseThrow(); + viewEntity.setCreatedAt(createdAt.plusMinutes(1)); + } + + @Test + void findAllOrderByPopularity를_통해_조회한_레코드는_조회수보다_저장수에서_더_큰_가중치를_얻는다() { + // given + viewRepository.save(View.builder() + .record(createRecord(1)) + .user(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build() + ); + viewRepository.save(View.builder() + .record(createRecord(1)) + .user(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build() + ); + viewRepository.save(View.builder() + .record(createRecord(1)) + .user(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build() + ); + viewRepository.save(View.builder() + .record(createRecord(2)) + .user(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build() + ); + // 1번 레코드 3번 시청, 2번 레코드 1번 시청 + + bookmarkRepository.save(Bookmark.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(createRecord(3)) + .createdAt(sevenDaysAgo) + .build() + ); + bookmarkRepository.save(Bookmark.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(createRecord(4)) + .createdAt(sevenDaysAgo) + .build() + ); + // 3번 레코드 1번 저장, 4번 레코드 1번 저장 + + // when + List result = recordRepository.findAllOrderByPopularity(PageRequest.of(0, 4)) + .getContent(); + + // then + assertAll( + () -> assertThat(result).hasSize(4), + () -> assertThat(result.get(0).getId()).isEqualTo(1), + () -> assertThat(result.get(1).getId()).isIn(3L, 4L), + () -> assertThat(result.get(2).getId()).isIn(3L, 4L), + () -> assertThat(result.get(3).getId()).isEqualTo(2) + ); + } + + @Test + void findAllByKeywordsOrderByPopularity를_통해_조회한_레코드는_키워드_필터링이_적용된다() { + // given + viewRepository.save(View.builder() + .record(createRecord(1)) + .user(createUser(UserStatus.ACTIVE)) + .createdAt(sevenDaysAgo) + .build() + ); + + // when + List inclusiveResult = recordRepository.findAllByKeywordsOrderByPopularity(List.of(Keyword.EXOTIC), PageRequest.of(0, 4)) + .getContent(); + List exclusiveResult = recordRepository.findAllByKeywordsOrderByPopularity(List.of(Keyword.DUCKUMORI), PageRequest.of(0, 4)) + .getContent(); + + // then + assertAll( + () -> assertThat(inclusiveResult).hasSize(1), + () -> assertThat(inclusiveResult.get(0).getId()).isEqualTo(1), + () -> assertThat(exclusiveResult).isEmpty() + ); + } + + @Test + void countAllByUserIdGroupByKeyword를_통해_사용자가_시청_저장_업로드한_모든_영상과_관련된_키워드별로_카운트할_수_있다() { + // given + // 현재 EXOTIC 3개 업로드했고, QUITE 3개 업로드했음 + long userId = 1L; + viewRepository.save(View.builder() + .user(createUser(UserStatus.ACTIVE)) + .record(Record.builder() + .id(1L) + .fileUrl(new FileUrl(VIDEO_URL, THUMBNAIL_URL)) + .location(LOCATION) + .content(CONTENT) + .keywords(KEYWORDS) + .uploader(createUser(UserStatus.ACTIVE)) + .build()) + .createdAt(sevenDaysAgo) + .build()); + + // QUITE 키워드 영상 1번 시청함 + viewRepository.save( + View.builder() + .record(DomainFixture.createRecord(2)) + .user(DomainFixture.createUser(UserStatus.ACTIVE)) + .build() + ); + + // EXOTIC 키워드 영상 1번 저장함 + bookmarkRepository.save( + Bookmark.builder() + .user(DomainFixture.createUser(UserStatus.ACTIVE)) + .record(DomainFixture.createRecord(5)) + .build() + ); + // 따라서 결과적으로 {EXOTIC:4, QUITE:4} + + // when + Map preference = recordRepository.countAllByUserIdGroupByKeyword(userId); + System.out.println("preference = " + preference); + + // then + assertAll( + () -> assertThat(preference.size()).isEqualTo(2), + () -> assertThat(preference.get(Keyword.EXOTIC)).isEqualTo(4), + () -> assertThat(preference.get(Keyword.QUITE)).isEqualTo(4) + ); + } } \ No newline at end of file diff --git a/src/test/java/org/recordy/server/record/service/RecordServiceTest.java b/src/test/java/org/recordy/server/record/service/RecordServiceTest.java index 8d0efefe..ec372f3f 100644 --- a/src/test/java/org/recordy/server/record/service/RecordServiceTest.java +++ b/src/test/java/org/recordy/server/record/service/RecordServiceTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.recordy.server.auth.exception.AuthException; import org.recordy.server.common.message.ErrorMessage; import org.recordy.server.mock.FakeContainer; import org.recordy.server.record.domain.File; @@ -29,6 +28,7 @@ void init() { UserRepository userRepository = fakeContainer.userRepository; userRepository.save(DomainFixture.createUser(UserStatus.ACTIVE)); + userRepository.save(DomainFixture.createUser(UserStatus.ACTIVE)); } @Test @@ -58,10 +58,10 @@ void init() { Record record = recordService.create(recordCreate, file); // when - recordService.delete(1,record.getId()); + recordService.delete(1, record.getId()); // then - Slice result = recordService.getRecentRecordsLaterThanCursor(0,1); + Slice result = recordService.getRecentRecords(null, 0L, 1); assertAll( () -> assertThat(result.getContent()).hasSize(0), () -> assertThat(result.hasNext()).isFalse() @@ -77,7 +77,7 @@ void init() { // when // then - assertThatThrownBy(() -> recordService.delete(100,record.getId())) + assertThatThrownBy(() -> recordService.delete(100, record.getId())) .isInstanceOf(RecordException.class) .hasMessageContaining(ErrorMessage.FORBIDDEN_DELETE_RECORD.getMessage()); } @@ -92,7 +92,7 @@ void init() { recordService.create(DomainFixture.createRecordCreateByOtherUser(), DomainFixture.createFile()); //when - Slice result = recordService.getRecentRecordsByUser(1,0,10); + Slice result = recordService.getRecentRecordsByUser(1, Long.MAX_VALUE, 10); //then assertAll( @@ -104,7 +104,7 @@ void init() { } @Test - void getRecentRecordsLaterThanCursor를_통해_커서_이후의_레코드를_최신_순서로_읽을_수_있다() { + void getRecentRecords를_통해_커서_이후의_레코드를_최신_순서로_읽을_수_있다() { // given recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); @@ -113,7 +113,7 @@ void init() { recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); // when - Slice result = recordService.getRecentRecordsLaterThanCursor(6, 10); + Slice result = recordService.getRecentRecords(null, 6L, 10); // then assertAll( @@ -128,14 +128,14 @@ void init() { } @Test - void getRecentRecordsLaterThanCursorByUser를_통해_커서가_제일_오래된_값이라면_아무것도_반환되지_않는다() { + void getRecentRecords를_통해_커서가_제일_오래된_값이라면_아무것도_반환되지_않는다() { // given recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); recordService.create(DomainFixture.createRecordCreate(), DomainFixture.createFile()); // when - Slice result = recordService.getRecentRecordsLaterThanCursor(1, 3); + Slice result = recordService.getRecentRecords(null, 1L, 3); // then assertAll( diff --git a/src/test/java/org/recordy/server/record_stat/domain/usecase/PreferenceTest.java b/src/test/java/org/recordy/server/record_stat/domain/usecase/PreferenceTest.java new file mode 100644 index 00000000..9dc1ed0f --- /dev/null +++ b/src/test/java/org/recordy/server/record_stat/domain/usecase/PreferenceTest.java @@ -0,0 +1,5 @@ +package org.recordy.server.record_stat.domain.usecase; + +class PreferenceTest { + +} \ No newline at end of file diff --git a/src/test/java/org/recordy/server/record_stat/repository/BookmarkRepositoryIntegrationTest.java b/src/test/java/org/recordy/server/record_stat/repository/BookmarkRepositoryIntegrationTest.java index 59ea840d..7105d9e2 100644 --- a/src/test/java/org/recordy/server/record_stat/repository/BookmarkRepositoryIntegrationTest.java +++ b/src/test/java/org/recordy/server/record_stat/repository/BookmarkRepositoryIntegrationTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertAll; import org.junit.jupiter.api.Test; +import org.recordy.server.record.domain.Record; +import org.recordy.server.record.repository.RecordRepository; import org.recordy.server.record_stat.domain.Bookmark; import org.recordy.server.util.DomainFixture; import org.recordy.server.util.db.IntegrationTest; @@ -16,17 +18,21 @@ import org.springframework.transaction.annotation.Transactional; @SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), @Sql(value = "/sql/bookmark-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/bookmark-repository-test-clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) @Transactional @SpringBootTest public class BookmarkRepositoryIntegrationTest extends IntegrationTest { + @Autowired private BookmarkRepository bookmarkRepository; + @Autowired + private RecordRepository recordRepository; @Test - void delete를_통해_북마크_테이터를_생성할_수_있다() { + void save를_통해_북마크_테이터를_생성할_수_있다() { //given Bookmark bookmark = DomainFixture.createBookmark(); @@ -37,7 +43,41 @@ public class BookmarkRepositoryIntegrationTest extends IntegrationTest { assertAll( () -> assertThat(result.getId()).isNotNull(), () -> assertThat(result.getUser().getId()).isEqualTo(DomainFixture.USER_ID), - () -> assertThat(result.getRecord().getId()).isEqualTo(DomainFixture.RECORD_ID) + () -> assertThat(result.getRecord().getId()).isEqualTo(DomainFixture.RECORD_ID), + () -> assertThat(result.getRecord().getBookmarkCount()).isEqualTo(1) + ); + } + + @Test + void save를_통해_저장된_북마크_데이터는_레코드_조회_시_데이터_개수로_카운트될_수_있다() { + // given + // userId 1 <-> recordId 1 + // userId 1 <-> recordId 2 + // userId 2 <-> recordId 1 + // userId 2 <-> recordId 1 + + // when + Slice result = recordRepository.findAllByIdAfterOrderByIdDesc(0, PageRequest.ofSize(4)); + + // then + assertAll( + () -> assertThat(result.getContent().size()).isEqualTo(2), + () -> assertThat(result.getContent().get(0).getBookmarkCount()).isEqualTo(2), + () -> assertThat(result.getContent().get(1).getBookmarkCount()).isEqualTo(2) + ); + } + + @Test + void deleteById를_통해_북마크를_삭제할_수_있다() { + //given + //when + bookmarkRepository.deleteById(1); + + //then + //원래 user1의 북마크 2개 있음 -> 이중 1개 삭제 + Slice bookmarks = bookmarkRepository.findAllByBookmarksOrderByIdDesc(1L, 3L, PageRequest.ofSize(10)); + assertAll( + () -> assertThat(bookmarks.getContent()).hasSize(0) ); } @@ -76,4 +116,17 @@ public class BookmarkRepositoryIntegrationTest extends IntegrationTest { ); } + @Test + void findByUserAndRecord를_통해_북마크를_찾을_수_있다() { + //given + + //when + Bookmark bookmark = bookmarkRepository.findByUserAndRecord(1,1).get(); + + //then + assertAll( + () -> assertThat(bookmark.getUser().getId()).isEqualTo(1), + () -> assertThat(bookmark.getRecord().getId()).isEqualTo(1) + ); + } } diff --git a/src/test/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryTest.java b/src/test/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryTest.java new file mode 100644 index 00000000..744defa3 --- /dev/null +++ b/src/test/java/org/recordy/server/record_stat/repository/impl/ViewRepositoryTest.java @@ -0,0 +1,47 @@ +package org.recordy.server.record_stat.repository.impl; + +import org.junit.jupiter.api.Test; +import org.recordy.server.record.domain.Record; +import org.recordy.server.record_stat.domain.View; +import org.recordy.server.record_stat.repository.ViewRepository; +import org.recordy.server.user.domain.User; +import org.recordy.server.user.domain.UserStatus; +import org.recordy.server.util.DomainFixture; +import org.recordy.server.util.db.IntegrationTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), + @Sql(value = "/sql/view-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +class ViewRepositoryIntegrationTest extends IntegrationTest { + + @Autowired + private ViewRepository viewRepository; + + @Test + void save를_통해_조회_데이터를_저장할_수_있다() { + // given + User user = DomainFixture.createUser(UserStatus.ACTIVE); + Record record = DomainFixture.createRecord(); + + // when + View view = viewRepository.save(View.builder() + .record(record) + .user(user) + .build()); + + // then + assertAll( + () -> assertThat(view).isNotNull(), + () -> assertThat(view.getUser().getId()).isEqualTo(user.getId()), + () -> assertThat(view.getRecord().getId()).isEqualTo(record.getId()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/recordy/server/record_stat/service/RecordStatServiceTest.java b/src/test/java/org/recordy/server/record_stat/service/RecordStatServiceTest.java index f2c65cae..437cab98 100644 --- a/src/test/java/org/recordy/server/record_stat/service/RecordStatServiceTest.java +++ b/src/test/java/org/recordy/server/record_stat/service/RecordStatServiceTest.java @@ -47,30 +47,23 @@ void init() { } @Test - void getBookmarkedRecords를_통해_커서_이후의_북마크된_레코드를_최신_순서로_읽을_수_있다() { + void deleteBookmark를_통해_북마크를_삭제할_수_있다() { // given recordStatService.bookmark(1, 1); - recordStatService.bookmark(2, 1); - recordStatService.bookmark(1, 2); - recordStatService.bookmark(2, 2); - recordStatService.bookmark(1, 3); - recordStatService.bookmark(2, 3); // when - Slice result = recordStatService.getBookmarkedRecords(1, 7, 10); + recordStatService.deleteBookmark(1,1); // then + Slice result = recordStatService.getBookmarkedRecords(1, 7, 10); + assertAll( - () -> assertThat(result.getContent()).hasSize(3), - () -> assertThat(result.getContent().get(0).getId()).isEqualTo(3L), - () -> assertThat(result.getContent().get(1).getId()).isEqualTo(2L), - () -> assertThat(result.getContent().get(2).getId()).isEqualTo(1L), - () -> assertThat(result.hasNext()).isFalse() + () -> assertThat(result.getContent()).hasSize(0) ); } @Test - void getBookmarkedRecords를_통해_커서가_제일_오래된_값이라면_아무것도_반환되지_않는다() { + void getBookmarkedRecords를_통해_커서_이후의_북마크된_레코드를_최신_순서로_읽을_수_있다() { // given recordStatService.bookmark(1, 1); recordStatService.bookmark(2, 1); @@ -80,18 +73,21 @@ void init() { recordStatService.bookmark(2, 3); // when - Slice result = recordStatService.getBookmarkedRecords(1, 1, 10); + Slice result = recordStatService.getBookmarkedRecords(1, 7, 10); // then assertAll( - () -> assertThat(result.getContent()).hasSize(0), + () -> assertThat(result.getContent()).hasSize(3), + () -> assertThat(result.getContent().get(0).getId()).isEqualTo(3L), + () -> assertThat(result.getContent().get(1).getId()).isEqualTo(2L), + () -> assertThat(result.getContent().get(2).getId()).isEqualTo(1L), () -> assertThat(result.hasNext()).isFalse() ); } @Test - void getBookmarkCount를_통해_현재_기록의_북마크_수를_구할_수_있다() { - //given + void getBookmarkedRecords를_통해_커서가_제일_오래된_값이라면_아무것도_반환되지_않는다() { + // given recordStatService.bookmark(1, 1); recordStatService.bookmark(2, 1); recordStatService.bookmark(1, 2); @@ -100,24 +96,12 @@ void init() { recordStatService.bookmark(2, 3); // when - Long result = recordStatService.getBookmarkCount(1); - - // then - assertAll( - () -> assertThat(result).isEqualTo(2) - ); - } - - @Test - void getBookmarkCount를_통해_북마크_수를_구할_때_북마크가_없으면_0을_리턴한다() { - //given - // when - Long result = recordStatService.getBookmarkCount(1); + Slice result = recordStatService.getBookmarkedRecords(1, 1, 10); // then assertAll( - () -> assertThat(result).isEqualTo(0) + () -> assertThat(result.getContent()).hasSize(0), + () -> assertThat(result.hasNext()).isFalse() ); } - } diff --git a/src/test/java/org/recordy/server/user/controller/UserControllerIntegrationTest.java b/src/test/java/org/recordy/server/user/controller/UserControllerIntegrationTest.java index b592c933..0e04e88e 100644 --- a/src/test/java/org/recordy/server/user/controller/UserControllerIntegrationTest.java +++ b/src/test/java/org/recordy/server/user/controller/UserControllerIntegrationTest.java @@ -10,8 +10,9 @@ @SpringBootTest @SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), @Sql(value = "/sql/user-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/user-service-test-clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) @AutoConfigureMockMvc class UserControllerIntegrationTest { diff --git a/src/test/java/org/recordy/server/user/controller/UserControllerTest.java b/src/test/java/org/recordy/server/user/controller/UserControllerTest.java index 7894eb4e..4c2ab974 100644 --- a/src/test/java/org/recordy/server/user/controller/UserControllerTest.java +++ b/src/test/java/org/recordy/server/user/controller/UserControllerTest.java @@ -102,7 +102,7 @@ void init() { } @Test - void delete를_통해_사용자를_삭제하는_데_성공하면_200_OK를_받는다() { + void delete를_통해_사용자를_삭제하는_데_성공하면_204_NO_CONTENT를_받는다() { // given userService.signIn(DomainFixture.createUserSignIn(AuthPlatform.Type.KAKAO)); @@ -110,7 +110,7 @@ void init() { ResponseEntity result = userController.delete(DomainFixture.USER_ID); // then - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); } @Test diff --git a/src/test/java/org/recordy/server/user/repository/UserRepositoryIntegrationTest.java b/src/test/java/org/recordy/server/user/repository/UserRepositoryIntegrationTest.java index 4378ec3e..396ea961 100644 --- a/src/test/java/org/recordy/server/user/repository/UserRepositoryIntegrationTest.java +++ b/src/test/java/org/recordy/server/user/repository/UserRepositoryIntegrationTest.java @@ -18,8 +18,9 @@ import static org.recordy.server.util.DomainFixture.*; @SqlGroup({ + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS), @Sql(value = "/sql/user-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/user-repository-test-clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(value = "/sql/clean-database.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) @SpringBootTest class UserRepositoryIntegrationTest extends IntegrationTest { diff --git a/src/test/java/org/recordy/server/util/DomainFixture.java b/src/test/java/org/recordy/server/util/DomainFixture.java index f5975bc1..12d3f868 100644 --- a/src/test/java/org/recordy/server/util/DomainFixture.java +++ b/src/test/java/org/recordy/server/util/DomainFixture.java @@ -9,7 +9,7 @@ import org.recordy.server.record.domain.Record; import org.recordy.server.record.domain.RecordEntity; import org.recordy.server.record.domain.usecase.RecordCreate; -import org.recordy.server.record.controller.dto.FileUrl; +import org.recordy.server.record.service.dto.FileUrl; import org.recordy.server.record_stat.domain.Bookmark; import org.recordy.server.user.controller.dto.request.TermsAgreement; import org.recordy.server.user.domain.usecase.UserSignIn; @@ -170,26 +170,36 @@ public static File createFile() { } public static Record createRecord() { - return new Record( - RECORD_ID, - new FileUrl(VIDEO_URL, THUMBNAIL_URL), - LOCATION, - CONTENT, - KEYWORDS, - createUser(UserStatus.ACTIVE) - ); + return Record.builder() + .id(RECORD_ID) + .fileUrl(new FileUrl(VIDEO_URL, THUMBNAIL_URL)) + .location(LOCATION) + .content(CONTENT) + .keywords(KEYWORDS) + .uploader(createUser(UserStatus.ACTIVE)) + .build(); + } + + public static Record createRecord(long id) { + return Record.builder() + .id(id) + .fileUrl(new FileUrl(VIDEO_URL, THUMBNAIL_URL)) + .location(LOCATION) + .content(CONTENT) + .keywords(KEYWORDS) + .uploader(createUser(UserStatus.ACTIVE)) + .build(); } public static RecordEntity createRecordEntity() { - return new RecordEntity( - RECORD_ID, - VIDEO_URL, - THUMBNAIL_URL, - LOCATION, - CONTENT, - KEYWORDS, - createUserEntity() - ); + return RecordEntity.builder() + .id(RECORD_ID) + .videoUrl(VIDEO_URL) + .thumbnailUrl(THUMBNAIL_URL) + .location(LOCATION) + .content(CONTENT) + .user(createUserEntity()) + .build(); } public static Bookmark createBookmark() { diff --git a/src/test/resources/sql/bookmark-repository-test-data.sql b/src/test/resources/sql/bookmark-repository-test-data.sql index 73dee9b5..7d149fa2 100644 --- a/src/test/resources/sql/bookmark-repository-test-data.sql +++ b/src/test/resources/sql/bookmark-repository-test-data.sql @@ -4,6 +4,9 @@ values (1, 'abcdefg', 'KAKAO', 'ACTIVE', true, true, true, 'konu'); insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) values (2, 'abcdefgh', 'KAKAO', 'ACTIVE', true, true, true, 'subin'); +insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) +values (3, 'abcdefghi', 'KAKAO', 'ACTIVE', true, true, true, 'sebin'); + insert into `keywords` (`id`, `keyword`) values (1, 'EXOTIC'); diff --git a/src/test/resources/sql/bookmark-repository-test-clean.sql b/src/test/resources/sql/clean-database.sql similarity index 68% rename from src/test/resources/sql/bookmark-repository-test-clean.sql rename to src/test/resources/sql/clean-database.sql index 1ca3018d..21abb006 100644 --- a/src/test/resources/sql/bookmark-repository-test-clean.sql +++ b/src/test/resources/sql/clean-database.sql @@ -1,5 +1,6 @@ +delete from `views` where 1; delete from `bookmarks` where 1; delete from `uploads` where 1; delete from `records` where 1; delete from `keywords` where 1; -delete from `users` where 1; \ No newline at end of file +delete from `users` where 1; diff --git a/src/test/resources/sql/keyword-repository-test-clean.sql b/src/test/resources/sql/keyword-repository-test-clean.sql deleted file mode 100644 index fe81deb3..00000000 --- a/src/test/resources/sql/keyword-repository-test-clean.sql +++ /dev/null @@ -1 +0,0 @@ -delete from `keywords` where 1; \ No newline at end of file diff --git a/src/test/resources/sql/record-repository-test-clean.sql b/src/test/resources/sql/record-repository-test-clean.sql deleted file mode 100644 index 55088237..00000000 --- a/src/test/resources/sql/record-repository-test-clean.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from `uploads` where 1; -delete from `records` where 1; -delete from `keywords` where 1; -delete from `users` where 1; diff --git a/src/test/resources/sql/record-repository-test-data.sql b/src/test/resources/sql/record-repository-test-data.sql index dc67d819..3d6830b8 100644 --- a/src/test/resources/sql/record-repository-test-data.sql +++ b/src/test/resources/sql/record-repository-test-data.sql @@ -1,6 +1,9 @@ insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) values (1, 'abcdefg', 'KAKAO', 'ACTIVE', true, true, true, 'konu'); +insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) +values (2, 'abcdefgh', 'KAKAO', 'ACTIVE', true, true, true, 'subin'); + insert into `keywords` (`id`, `keyword`) values (1, 'EXOTIC'); @@ -23,4 +26,25 @@ insert into `records` (`id`, `user_id`, `content`, `location`,`thumbnail_url`, ` values (4, 2, 'content', 'location', 'thumbnail_url', 'video_url'); insert into `records` (`id`, `user_id`, `content`, `location`,`thumbnail_url`, `video_url`) -values (5, 2, 'content', 'location', 'thumbnail_url', 'video_url'); \ No newline at end of file +values (5, 1, 'content', 'location', 'thumbnail_url', 'video_url'); + +insert into `records` (`id`, `user_id`, `content`, `location`,`thumbnail_url`, `video_url`) +values (6, 2, 'content', 'location', 'thumbnail_url', 'video_url'); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(1, 1, 1); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(6, 1, 2); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(2, 2, 2); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(3, 3, 1); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(4, 4, 2); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(5, 5, 1); diff --git a/src/test/resources/sql/user-repository-test-clean.sql b/src/test/resources/sql/user-repository-test-clean.sql deleted file mode 100644 index c2a76179..00000000 --- a/src/test/resources/sql/user-repository-test-clean.sql +++ /dev/null @@ -1 +0,0 @@ -delete from `users` where 1; \ No newline at end of file diff --git a/src/test/resources/sql/user-service-test-clean.sql b/src/test/resources/sql/user-service-test-clean.sql deleted file mode 100644 index c2a76179..00000000 --- a/src/test/resources/sql/user-service-test-clean.sql +++ /dev/null @@ -1 +0,0 @@ -delete from `users` where 1; \ No newline at end of file diff --git a/src/test/resources/sql/view-repository-test-data.sql b/src/test/resources/sql/view-repository-test-data.sql new file mode 100644 index 00000000..b95e9f9b --- /dev/null +++ b/src/test/resources/sql/view-repository-test-data.sql @@ -0,0 +1,26 @@ +insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) +values (1, 'abcdefg', 'KAKAO', 'ACTIVE', true, true, true, 'konu'); + +insert into `users` (`id`, `platform_id`, `platform_type`, `status`, `age_term`, `personal_info_term`, `use_term`, `nickname`) +values (2, 'abcdefgh', 'KAKAO', 'ACTIVE', true, true, true, 'subin'); + +insert into `keywords` (`id`, `keyword`) +values (1, 'EXOTIC'); + +insert into `keywords` (`id`, `keyword`) +values (2, 'QUITE'); + +insert into `records` (`id`, `user_id`, `content`, `location`,`thumbnail_url`, `video_url`) +values (1, 1, 'content', 'location', 'thumbnail_url', 'video_url'); + +insert into `records` (`id`, `user_id`, `content`, `location`,`thumbnail_url`, `video_url`) +values (2, 1, 'content', 'location', 'thumbnail_url', 'video_url'); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(1, 1, 1); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(2, 1, 2); + +insert into `uploads` (`id`, `record_id`, `keyword_id`) +values(3, 2, 1); \ No newline at end of file