diff --git a/src/main/java/com/example/log4u/common/entity/BaseEntity.java b/src/main/java/com/example/log4u/common/entity/BaseEntity.java index cf6bf0a2..ff310046 100644 --- a/src/main/java/com/example/log4u/common/entity/BaseEntity.java +++ b/src/main/java/com/example/log4u/common/entity/BaseEntity.java @@ -9,8 +9,15 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PACKAGE) @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @@ -21,6 +28,5 @@ abstract public class BaseEntity { private LocalDateTime createdAt; @LastModifiedDate - @Column(nullable = false) private LocalDateTime updatedAt; } \ No newline at end of file diff --git a/src/main/java/com/example/log4u/common/util/PageableUtil.java b/src/main/java/com/example/log4u/common/util/PageableUtil.java new file mode 100644 index 00000000..1599d1d4 --- /dev/null +++ b/src/main/java/com/example/log4u/common/util/PageableUtil.java @@ -0,0 +1,20 @@ +package com.example.log4u.common.util; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +public class PageableUtil { + public static Slice checkAndCreateSlice(List content, Pageable pageable) { + boolean hasNext = content.size() > pageable.getPageSize(); + + // 다음 페이지가 있으면 마지막 항목 제거 + if (hasNext) { + content.remove(content.size() - 1); // removeLast() 대신 인덱스로 처리 + } + + return new SliceImpl<>(content, pageable, hasNext); + } +} diff --git a/src/main/java/com/example/log4u/domain/diary/dto/DiaryRequestDto.java b/src/main/java/com/example/log4u/domain/diary/dto/DiaryRequestDto.java index 51acb3f6..eb979839 100644 --- a/src/main/java/com/example/log4u/domain/diary/dto/DiaryRequestDto.java +++ b/src/main/java/com/example/log4u/domain/diary/dto/DiaryRequestDto.java @@ -8,6 +8,7 @@ import com.example.log4u.domain.media.dto.MediaRequestDto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record DiaryRequestDto( @NotBlank(message = "제목은 필수입니다.") @@ -17,7 +18,7 @@ public record DiaryRequestDto( Double latitude, Double longitude, WeatherInfo weatherInfo, - @NotBlank(message = "공개 범위는 필수입니다.") + @NotNull(message = "공개 범위는 필수입니다.") VisibilityType visibility, List mediaList ) { diff --git a/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepository.java b/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepository.java index 343daef6..61a060f8 100644 --- a/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepository.java +++ b/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepository.java @@ -24,4 +24,10 @@ Slice findByUserIdAndVisibilityInAndCursorId( Long cursorId, Pageable pageable ); + + Slice getLikeDiarySliceByUserId( + Long userId, + List visibilities, + Long cursorId, + Pageable pageable); } diff --git a/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepositoryImpl.java b/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepositoryImpl.java index 27af5de2..d201fe8f 100644 --- a/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepositoryImpl.java +++ b/src/main/java/com/example/log4u/domain/diary/repository/CustomDiaryRepositoryImpl.java @@ -13,6 +13,7 @@ import com.example.log4u.domain.diary.VisibilityType; import com.example.log4u.domain.diary.entity.Diary; import com.example.log4u.domain.diary.entity.QDiary; +import com.example.log4u.domain.like.entity.QLike; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; @@ -24,6 +25,9 @@ public class CustomDiaryRepositoryImpl implements CustomDiaryRepository { private final JPAQueryFactory queryFactory; + private final QDiary diary = QDiary.diary; + private final QLike like = QLike.like; + @Override public Page searchDiaries( String keyword, @@ -31,7 +35,7 @@ public Page searchDiaries( SortType sort, Pageable pageable ) { - QDiary diary = QDiary.diary; + // QDiary diary = QDiary.diary; // 조건 생성 BooleanExpression condition = createCondition(diary, keyword, visibilities, null); @@ -132,4 +136,31 @@ private Slice checkAndCreateSlice(List content, Pageable pageable) return new SliceImpl<>(content, pageable, hasNext); } + + @Override + public Slice getLikeDiarySliceByUserId( + Long userId, + List visibilities, + Long cursorId, + Pageable pageable) { + QDiary diary = QDiary.diary; + + // 조건 생성 + BooleanExpression condition = createCondition(diary, null, visibilities, userId); + + // limit + 1로 다음 페이지 존재 여부 확인 + List content = queryFactory + .selectFrom(diary) + .innerJoin(like) + .on(like.diaryId.eq(diary.diaryId)) + .where(like.userId.eq(userId) + .and(condition) + .and(like.likeId.lt(cursorId))) + .orderBy(like.createdAt.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + // 다음 페이지 여부를 계산하여 반환 + return checkAndCreateSlice(content, pageable); + } } diff --git a/src/main/java/com/example/log4u/domain/diary/service/DiaryService.java b/src/main/java/com/example/log4u/domain/diary/service/DiaryService.java index 8a7891a8..d36f4de0 100644 --- a/src/main/java/com/example/log4u/domain/diary/service/DiaryService.java +++ b/src/main/java/com/example/log4u/domain/diary/service/DiaryService.java @@ -217,4 +217,44 @@ public void checkDiaryExists(Long diaryId) { throw new NotFoundDiaryException(); } } + + @Transactional(readOnly = true) + public PageResponse getMyDiariesByCursor(Long userId, VisibilityType visibilityType, + Long cursorId, int size) { + List visibilities = + visibilityType == null ? List.of(VisibilityType.PUBLIC, VisibilityType.PRIVATE, VisibilityType.FOLLOWER) : + List.of(visibilityType); + + Slice diaries = diaryRepository.findByUserIdAndVisibilityInAndCursorId( + userId, + visibilities, + cursorId != null ? cursorId : Long.MAX_VALUE, + PageRequest.of(0, size) + ); + + Slice dtoSlice = mapToDtoSlice(diaries); + + Long nextCursor = !dtoSlice.isEmpty() ? dtoSlice.getContent().getLast().diaryId() : null; + + return PageResponse.of(dtoSlice, nextCursor); + } + + @Transactional(readOnly = true) + public PageResponse getLikeDiariesByCursor(Long userId, Long targetUserId, Long cursorId, + int size) { + List visibilities = determineAccessibleVisibilities(userId, targetUserId); + + Slice diaries = diaryRepository.getLikeDiarySliceByUserId( + targetUserId, + visibilities, + cursorId != null ? cursorId : Long.MAX_VALUE, + PageRequest.of(0, size) + ); + + Slice dtoSlice = mapToDtoSlice(diaries); + + Long nextCursor = !dtoSlice.isEmpty() ? dtoSlice.getContent().getLast().diaryId() : null; + + return PageResponse.of(dtoSlice, nextCursor); + } } diff --git a/src/main/java/com/example/log4u/domain/follow/repository/FollowQuerydsl.java b/src/main/java/com/example/log4u/domain/follow/repository/FollowQuerydsl.java new file mode 100644 index 00000000..2bfb9527 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/follow/repository/FollowQuerydsl.java @@ -0,0 +1,69 @@ +package com.example.log4u.domain.follow.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.stereotype.Repository; + +import com.example.log4u.common.util.PageableUtil; +import com.example.log4u.domain.follow.entitiy.Follow; +import com.example.log4u.domain.follow.entitiy.QFollow; +import com.example.log4u.domain.user.dto.UserThumbnailResponseDto; +import com.example.log4u.domain.user.entity.QUser; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.NumberPath; + +@Repository +public class FollowQuerydsl extends QuerydslRepositorySupport { + private final QFollow follow = QFollow.follow; + private final QUser user = QUser.user; + + public FollowQuerydsl() { + super(Follow.class); + } + + private NumberPath getNumberPath(boolean isFollowerQuery) { + return isFollowerQuery ? follow.followerId : follow.followingId; + } + + private BooleanBuilder getBooleanBuilder(boolean isFollowerQuery, Long userId, Long cursorId) { + BooleanBuilder builder = new BooleanBuilder(); + builder.and(getNumberPath(isFollowerQuery).eq(userId)); + + if (cursorId != null) { + builder.and(follow.id.lt(cursorId)); + } + + return builder; + } + + private List getContent(boolean isFollowerQuery, Long userId, Long cursorId) { + BooleanBuilder builder = getBooleanBuilder(isFollowerQuery, userId, cursorId); + + return from(follow) + .select(Projections.constructor(UserThumbnailResponseDto.class, + getNumberPath(!isFollowerQuery), + user.nickname, + user.nickname)) + .where(builder) + .distinct() + .fetch(); + } + + //내 팔로워 아이디 슬라이스 + public Slice getFollowerSliceByUserId(Long userId, Long cursorId, Pageable pageable) { + boolean isFollowerQuery = true; + List content = getContent(isFollowerQuery, userId, cursorId); + return PageableUtil.checkAndCreateSlice(content, pageable); + } + + // 내가 팔로잉하는 아이디 슬라이스 + public Slice getFollowingSliceByUserId(Long userId, Long cursorId, Pageable pageable) { + boolean isFollowerQuery = false; + List content = getContent(isFollowerQuery, userId, cursorId); + return PageableUtil.checkAndCreateSlice(content, pageable); + } +} diff --git a/src/main/java/com/example/log4u/domain/reports/controller/ReportsController.java b/src/main/java/com/example/log4u/domain/reports/controller/ReportsController.java index f23911f5..8bb630e7 100644 --- a/src/main/java/com/example/log4u/domain/reports/controller/ReportsController.java +++ b/src/main/java/com/example/log4u/domain/reports/controller/ReportsController.java @@ -2,12 +2,14 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.example.log4u.common.oauth2.dto.CustomOAuth2User; import com.example.log4u.domain.reports.dto.ReportCreateRequestDto; import com.example.log4u.domain.reports.service.ReportService; @@ -21,18 +23,22 @@ public class ReportsController { @PostMapping("/diaries/{diaryId}") public ResponseEntity createReportForDiary( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody ReportCreateRequestDto reportCreateRequestDto, - @PathVariable Long diaryId) { - long reporterId = 1L; // SecurityContextHolder 에서 온다고 가정 + @PathVariable Long diaryId + ) { + long reporterId = customOAuth2User.getUserId(); reportService.createDiaryReport(reporterId, reportCreateRequestDto, diaryId); return new ResponseEntity<>(HttpStatus.CREATED); } @PostMapping("/comments/{commentId}") public ResponseEntity createReport( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody ReportCreateRequestDto reportCreateRequestDto, - @PathVariable Long commentId) { - long reporterId = 1L; + @PathVariable Long commentId + ) { + long reporterId = customOAuth2User.getUserId(); reportService.createCommentReport(reporterId, reportCreateRequestDto, commentId); return new ResponseEntity<>(HttpStatus.CREATED); } diff --git a/src/main/java/com/example/log4u/domain/subscription/PaymentProvider.java b/src/main/java/com/example/log4u/domain/subscription/PaymentProvider.java new file mode 100644 index 00000000..e9218c18 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/subscription/PaymentProvider.java @@ -0,0 +1,7 @@ +package com.example.log4u.domain.subscription; + +public enum PaymentProvider { + TOSS, + KAKAO, + NAVER +} diff --git a/src/main/java/com/example/log4u/domain/subscription/PaymentStatus.java b/src/main/java/com/example/log4u/domain/subscription/PaymentStatus.java new file mode 100644 index 00000000..1307e7dc --- /dev/null +++ b/src/main/java/com/example/log4u/domain/subscription/PaymentStatus.java @@ -0,0 +1,7 @@ +package com.example.log4u.domain.subscription; + +public enum PaymentStatus { + SUCCESS, // 결제 완료 + FAILED, // 결제 실패 + REFUNDED // 환불 완료 +} diff --git a/src/main/java/com/example/log4u/domain/subscription/dto/SubscriptionResponseDto.java b/src/main/java/com/example/log4u/domain/subscription/dto/SubscriptionResponseDto.java new file mode 100644 index 00000000..da861973 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/subscription/dto/SubscriptionResponseDto.java @@ -0,0 +1,20 @@ +package com.example.log4u.domain.subscription.dto; + +import java.time.LocalDateTime; + +import com.example.log4u.domain.subscription.PaymentProvider; + +import jakarta.annotation.Nullable; +import lombok.Builder; + +@Builder +public record SubscriptionResponseDto( + boolean isSubscriptionActive, + + @Nullable + PaymentProvider paymentProvider, + + @Nullable + LocalDateTime startDate +) { +} diff --git a/src/main/java/com/example/log4u/domain/subscription/entity/Subscription.java b/src/main/java/com/example/log4u/domain/subscription/entity/Subscription.java new file mode 100644 index 00000000..22d99607 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/subscription/entity/Subscription.java @@ -0,0 +1,47 @@ +package com.example.log4u.domain.subscription.entity; + +import com.example.log4u.common.entity.BaseEntity; +import com.example.log4u.domain.subscription.PaymentProvider; +import com.example.log4u.domain.subscription.PaymentStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PACKAGE) + +@Entity +public class Subscription extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long userId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PaymentProvider paymentProvider; + + @Column(nullable = false) + private Long amount; + + @Column(nullable = false, unique = true) + private String paymentKey; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PaymentStatus paymentStatus; +} diff --git a/src/main/java/com/example/log4u/domain/subscription/repository/SubscriptionRepository.java b/src/main/java/com/example/log4u/domain/subscription/repository/SubscriptionRepository.java new file mode 100644 index 00000000..1fe12bd7 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/subscription/repository/SubscriptionRepository.java @@ -0,0 +1,16 @@ +package com.example.log4u.domain.subscription.repository; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.example.log4u.domain.subscription.PaymentStatus; +import com.example.log4u.domain.subscription.entity.Subscription; + +public interface SubscriptionRepository extends JpaRepository { + Optional findByUserIdAndCreatedAtBeforeAndPaymentStatusOrderByCreatedAtDesc(Long userId, + LocalDateTime now, + PaymentStatus paymentStatus); +} + diff --git a/src/main/java/com/example/log4u/domain/supports/controller/SupportController.java b/src/main/java/com/example/log4u/domain/supports/controller/SupportController.java index 60077d51..00a9c437 100644 --- a/src/main/java/com/example/log4u/domain/supports/controller/SupportController.java +++ b/src/main/java/com/example/log4u/domain/supports/controller/SupportController.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.example.log4u.common.oauth2.dto.CustomOAuth2User; import com.example.log4u.domain.supports.dto.SupportCreateRequestDto; import com.example.log4u.domain.supports.dto.SupportGetResponseDto; import com.example.log4u.domain.supports.dto.SupportOverviewGetResponseDto; @@ -28,19 +30,21 @@ public class SupportController { @PostMapping public ResponseEntity createSupport( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody @Valid SupportCreateRequestDto supportCreateRequestDto ) { - long requesterId = 1L; + long requesterId = customOAuth2User.getUserId(); supportService.createSupport(requesterId, supportCreateRequestDto); return new ResponseEntity<>(HttpStatus.CREATED); } @GetMapping public ResponseEntity> getSupportOverviewPage( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestParam(defaultValue = "1") int page, @RequestParam(required = false) SupportType supportType ) { - long requesterId = 1L; + long requesterId = customOAuth2User.getUserId(); Page supportOverviewPage = supportService.getSupportPage(requesterId, page, supportType); return ResponseEntity.ok().body(supportOverviewPage); @@ -48,8 +52,9 @@ public ResponseEntity> getSupportOverviewPag @GetMapping("/{supportId}") public ResponseEntity getSupportBySupportId( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @PathVariable Long supportId) { - long requesterId = 1L; + long requesterId = customOAuth2User.getUserId(); SupportGetResponseDto supportGetResponseDto = supportService.getSupportById(requesterId, supportId); return ResponseEntity.ok().body(supportGetResponseDto); } diff --git a/src/main/java/com/example/log4u/domain/user/dto/UserThumbnailResponseDto.java b/src/main/java/com/example/log4u/domain/user/dto/UserThumbnailResponseDto.java new file mode 100644 index 00000000..4453e2d3 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/user/dto/UserThumbnailResponseDto.java @@ -0,0 +1,11 @@ +package com.example.log4u.domain.user.dto; + +import lombok.Builder; + +@Builder +public record UserThumbnailResponseDto( + Long userId, + String nickname, + String thumbnailUrl +) { +} diff --git a/src/main/java/com/example/log4u/domain/user/mypage/controller/MyPageController.java b/src/main/java/com/example/log4u/domain/user/mypage/controller/MyPageController.java new file mode 100644 index 00000000..ec1f9e41 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/user/mypage/controller/MyPageController.java @@ -0,0 +1,68 @@ +package com.example.log4u.domain.user.mypage.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.log4u.common.dto.PageResponse; +import com.example.log4u.common.oauth2.dto.CustomOAuth2User; +import com.example.log4u.domain.diary.VisibilityType; +import com.example.log4u.domain.diary.dto.DiaryResponseDto; +import com.example.log4u.domain.subscription.dto.SubscriptionResponseDto; +import com.example.log4u.domain.user.dto.UserThumbnailResponseDto; +import com.example.log4u.domain.user.mypage.service.MyPageService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class MyPageController { + private final MyPageService myPageService; + + @GetMapping("/users/me/diaries") + public ResponseEntity> getMyDiaryPage( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestParam(required = false, name = "visibilityType") VisibilityType visibilityType, + @RequestParam(required = false) Long cursorId + ) { + long userId = customOAuth2User.getUserId(); + return ResponseEntity.ok(myPageService.getMyDiariesByCursor(userId, visibilityType, cursorId)); + } + + @GetMapping("/users/me/likes") + public ResponseEntity> getMyLikesPage( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestParam(required = false) Long cursorId + ) { + long userId = customOAuth2User.getUserId(); + return ResponseEntity.ok(myPageService.getLikeDiariesByCursor(userId, cursorId)); + } + + @GetMapping("/users/me/followings") + public ResponseEntity> getMyFollowingPage( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestParam(required = false) Long cursorId + ) { + long userId = customOAuth2User.getUserId(); + return ResponseEntity.ok(myPageService.getMyFollowings(userId, cursorId)); + } + + @GetMapping("/users/me/followers") + public ResponseEntity> getMyFollowerPage( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestParam(required = false) Long cursorId + ) { + long userId = customOAuth2User.getUserId(); + return ResponseEntity.ok(myPageService.getMyFollowers(userId, cursorId)); + } + + @GetMapping("/users/me/subscriptions") + public ResponseEntity getMySubscriptions( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User + ) { + long userId = customOAuth2User.getUserId(); + return ResponseEntity.ok(myPageService.getMySubscription(userId)); + } +} diff --git a/src/main/java/com/example/log4u/domain/user/mypage/service/MyPageService.java b/src/main/java/com/example/log4u/domain/user/mypage/service/MyPageService.java new file mode 100644 index 00000000..457181b9 --- /dev/null +++ b/src/main/java/com/example/log4u/domain/user/mypage/service/MyPageService.java @@ -0,0 +1,89 @@ +package com.example.log4u.domain.user.mypage.service; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.log4u.common.dto.PageResponse; +import com.example.log4u.domain.diary.VisibilityType; +import com.example.log4u.domain.diary.dto.DiaryResponseDto; +import com.example.log4u.domain.diary.service.DiaryService; +import com.example.log4u.domain.follow.repository.FollowQuerydsl; +import com.example.log4u.domain.subscription.PaymentStatus; +import com.example.log4u.domain.subscription.dto.SubscriptionResponseDto; +import com.example.log4u.domain.subscription.entity.Subscription; +import com.example.log4u.domain.subscription.repository.SubscriptionRepository; +import com.example.log4u.domain.user.dto.UserThumbnailResponseDto; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional +@RequiredArgsConstructor +public class MyPageService { + private final int defaultPageSize = 6; + private final DiaryService diaryService; + private final FollowQuerydsl followQuerydsl; + private final SubscriptionRepository subscriptionRepository; + + @Transactional(readOnly = true) + public PageResponse getMyDiariesByCursor(Long userId, VisibilityType visibilityType, + Long cursorId) { + return diaryService.getMyDiariesByCursor(userId, visibilityType, cursorId, + defaultPageSize); // 일단 로직 자체가 구현 그대로 돼있길래 그대로 갖다 썼는데 이래도 구조가 괜찮을 지 모르겠습니다. + } + + @Transactional(readOnly = true) + public PageResponse getLikeDiariesByCursor(Long userId, Long cursorId) { + return diaryService.getLikeDiariesByCursor(userId, userId, + cursorId, defaultPageSize); + } + + @Transactional(readOnly = true) + public PageResponse getMyFollowers(Long userId, Long cursorId) { + Slice slice = followQuerydsl.getFollowerSliceByUserId( + userId, + cursorId, + PageRequest.of(0, defaultPageSize)); + + Long nextCursor = !slice.isEmpty() ? slice.getContent().getLast().userId() : null; + + return PageResponse.of(slice, nextCursor); + } + + @Transactional(readOnly = true) + public PageResponse getMyFollowings(Long userId, Long cursorId) { + Slice slice = followQuerydsl.getFollowingSliceByUserId( + userId, + cursorId, + PageRequest.of(0, defaultPageSize)); + + Long nextCursor = !slice.isEmpty() ? slice.getContent().getLast().userId() : null; + + return PageResponse.of(slice, nextCursor); + } + + @Transactional(readOnly = true) + public SubscriptionResponseDto getMySubscription(Long userId) { + Optional optionalSubscription = subscriptionRepository + .findByUserIdAndCreatedAtBeforeAndPaymentStatusOrderByCreatedAtDesc( + userId, + LocalDateTime.now(), PaymentStatus.SUCCESS); + if (optionalSubscription.isEmpty()) { + return SubscriptionResponseDto.builder() + .isSubscriptionActive(false) + .build(); + } else { + Subscription subscription = optionalSubscription.get(); + return SubscriptionResponseDto.builder() + .isSubscriptionActive(true) + .paymentProvider(subscription.getPaymentProvider()) + .startDate(subscription.getCreatedAt()) + .build(); + } + } +} diff --git a/src/test/java/com/example/log4u/domain/user/mypage/service/MyPageServiceTest.java b/src/test/java/com/example/log4u/domain/user/mypage/service/MyPageServiceTest.java new file mode 100644 index 00000000..2c0253c7 --- /dev/null +++ b/src/test/java/com/example/log4u/domain/user/mypage/service/MyPageServiceTest.java @@ -0,0 +1,152 @@ +package com.example.log4u.domain.user.mypage.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.SliceImpl; + +import com.example.log4u.common.dto.PageResponse; +import com.example.log4u.domain.diary.VisibilityType; +import com.example.log4u.domain.diary.dto.DiaryResponseDto; +import com.example.log4u.domain.diary.service.DiaryService; +import com.example.log4u.domain.follow.repository.FollowQuerydsl; +import com.example.log4u.domain.subscription.PaymentProvider; +import com.example.log4u.domain.subscription.PaymentStatus; +import com.example.log4u.domain.subscription.dto.SubscriptionResponseDto; +import com.example.log4u.domain.subscription.entity.Subscription; +import com.example.log4u.domain.subscription.repository.SubscriptionRepository; +import com.example.log4u.domain.user.dto.UserThumbnailResponseDto; +import com.example.log4u.fixture.DiaryFixture; + +@ExtendWith(MockitoExtension.class) +public class MyPageServiceTest { + @InjectMocks + private MyPageService myPageService; + + @Mock + private DiaryService diaryService; + + @Mock + private FollowQuerydsl followQuerydsl; + + @Mock + private SubscriptionRepository subscriptionRepository; + + private final Long userId = 1L; + private final Long cursorId = 10L; + + private List diaries; + + @BeforeEach + public void setUp() { + diaries = DiaryFixture.createDiariesFixture() + .stream() + .map(diary -> DiaryResponseDto.of(diary, new ArrayList<>())) + .toList(); + } + + @DisplayName("성공 테스트 : 내 다이어리 조회") + @Test + void getMyDiariesByCursor_returnsCorrectData() { + PageResponse mockResponse = PageResponse.of( + new SliceImpl<>(diaries), null + ); + + when(diaryService.getMyDiariesByCursor(userId, VisibilityType.PUBLIC, cursorId, 6)).thenReturn(mockResponse); + + PageResponse result = myPageService.getMyDiariesByCursor(userId, VisibilityType.PUBLIC, + cursorId); + + assertThat(result).isNotNull(); + verify(diaryService).getMyDiariesByCursor(userId, VisibilityType.PUBLIC, cursorId, 6); + } + + @DisplayName("성공 테스트 : 좋아요한 다이어리 조회") + @Test + void getLikeDiariesByCursor_returnsCorrectData() { + PageResponse mockResponse = PageResponse.of( + new SliceImpl<>(diaries), null + ); + + when(diaryService.getLikeDiariesByCursor(userId, userId, cursorId, 6)).thenReturn(mockResponse); + + PageResponse result = myPageService.getLikeDiariesByCursor(userId, cursorId); + + assertThat(result).isNotNull(); + verify(diaryService).getLikeDiariesByCursor(userId, userId, cursorId, 6); + } + + @DisplayName("성공 테스트 : 내 팔로워 조회") + @Test + void getMyFollowers_returnsCorrectData() { + var slice = new SliceImpl<>(List.of(new UserThumbnailResponseDto(userId, "nick", "image"))); + + when(followQuerydsl.getFollowerSliceByUserId(eq(userId), eq(cursorId), any(PageRequest.class))) + .thenReturn(slice); + + PageResponse result = myPageService.getMyFollowers(userId, cursorId); + + assertThat(result).isNotNull(); + } + + @DisplayName("성공 테스트 : 내 팔로잉 조회") + @Test + void getMyFollowings_returnsCorrectData() { + var slice = new SliceImpl<>(List.of(new UserThumbnailResponseDto(userId, "nick", "image"))); + + when(followQuerydsl.getFollowingSliceByUserId(eq(userId), eq(cursorId), any(PageRequest.class))) + .thenReturn(slice); + + PageResponse result = myPageService.getMyFollowings(userId, cursorId); + + assertThat(result).isNotNull(); + } + + @DisplayName("구독 정보 조회 - 구독이 있을 때") + @Test + void getMySubscription_whenExists_returnsActiveSubscription() { + LocalDateTime createdAt = LocalDateTime.now().minusDays(1); + Subscription subscription = Subscription.builder() + .id(1L) + .userId(userId) + .createdAt(createdAt) + .paymentProvider(PaymentProvider.KAKAO) + .paymentStatus(PaymentStatus.SUCCESS) + .build(); + + when(subscriptionRepository.findByUserIdAndCreatedAtBeforeAndPaymentStatusOrderByCreatedAtDesc( + eq(userId), any(LocalDateTime.class), eq(PaymentStatus.SUCCESS))) + .thenReturn(Optional.of(subscription)); + + SubscriptionResponseDto result = myPageService.getMySubscription(userId); + + assertThat(result.isSubscriptionActive()).isTrue(); + assertThat(result.startDate()).isEqualTo(createdAt); + assertThat(result.paymentProvider()).isEqualTo(PaymentProvider.KAKAO); + } + + @DisplayName("구독 정보 조회 - 구독이 없을 때") + @Test + void getMySubscription_whenNotExists_returnsInactive() { + when(subscriptionRepository.findByUserIdAndCreatedAtBeforeAndPaymentStatusOrderByCreatedAtDesc( + anyLong(), any(), eq(PaymentStatus.SUCCESS))) + .thenReturn(Optional.empty()); + + SubscriptionResponseDto result = myPageService.getMySubscription(userId); + + assertThat(result.isSubscriptionActive()).isFalse(); + } +}