Skip to content

Commit 32b7d2f

Browse files
authored
Merge pull request #125 from InningLog/feat/#124/journal-mypage
feat : 직관일지 마이페이지 기능 추가 #124
2 parents afe4dd4 + 746e62d commit 32b7d2f

11 files changed

Lines changed: 378 additions & 0 deletions

File tree

src/main/java/com/inninglog/inninglog/domain/comment/service/CommentGetService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,10 @@ protected List<CommentResDto> buildCommentTree(List<Comment> comments, Member me
7979
public Slice<Long> getCommentedPostIds(Member member, Pageable pageable) {
8080
return commentRepository.findDistinctTargetIdsByMemberAndContentType(member, ContentType.POST, pageable);
8181
}
82+
83+
//마이페이지: 내가 댓글 단 콘텐츠 ID 조회 (범용)
84+
@Transactional(readOnly = true)
85+
public Slice<Long> getCommentedContentIds(Member member, ContentType contentType, Pageable pageable) {
86+
return commentRepository.findDistinctTargetIdsByMemberAndContentType(member, contentType, pageable);
87+
}
8288
}

src/main/java/com/inninglog/inninglog/domain/journal/controller/JournalController.java

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,5 +757,161 @@ public ResponseEntity<SuccessResponse<SliceResponse<JournalSumListResDto>>> getM
757757

758758
return ResponseEntity.ok(SuccessResponse.success(SuccessCode.OK, result));
759759
}
760+
761+
@Operation(
762+
summary = "내가 댓글 단 직관 일지 목록 조회",
763+
description = """
764+
내가 댓글을 작성한 직관 일지 목록을 조회합니다.
765+
766+
✔ 최근 댓글 단 순으로 정렬되어 반환됩니다.
767+
✔ page는 0부터 시작합니다. (0=첫 페이지)
768+
✔ size는 한 페이지에서 가져올 일지 수를 의미합니다.
769+
✔ hasNext가 true이면 다음 페이지 요청이 가능합니다.
770+
""",
771+
tags = {"마이페이지"}
772+
)
773+
@ApiResponses({
774+
@ApiResponse(
775+
responseCode = "200",
776+
description = "내가 댓글 단 직관 일지 목록 조회 성공",
777+
content = @Content(
778+
mediaType = "application/json",
779+
schema = @Schema(implementation = SliceResponse.class)
780+
)
781+
)
782+
})
783+
@GetMapping("/my/commented")
784+
public ResponseEntity<SuccessResponse<SliceResponse<JournalFeedResDto>>> getMyCommentedJournals(
785+
@Parameter(hidden = true)
786+
@AuthenticationPrincipal CustomUserDetails user,
787+
788+
@Parameter(description = "조회할 페이지 번호 (0부터 시작)", example = "0")
789+
@RequestParam(defaultValue = "0") int page,
790+
791+
@Parameter(description = "한 페이지당 일지 개수", example = "10")
792+
@RequestParam(defaultValue = "10") int size
793+
) {
794+
Pageable pageable = PageRequest.of(page, size);
795+
SliceResponse<JournalFeedResDto> result = journalUsecase.getMyCommentedJournals(user.getMemberId(), pageable);
796+
797+
return ResponseEntity.ok(SuccessResponse.success(SuccessCode.OK, result));
798+
}
799+
800+
@Operation(
801+
summary = "내가 스크랩한 직관 일지 목록 조회",
802+
description = """
803+
내가 스크랩한 직관 일지 목록을 조회합니다.
804+
805+
✔ 최근 스크랩한 순으로 정렬되어 반환됩니다.
806+
✔ page는 0부터 시작합니다. (0=첫 페이지)
807+
✔ size는 한 페이지에서 가져올 일지 수를 의미합니다.
808+
✔ hasNext가 true이면 다음 페이지 요청이 가능합니다.
809+
""",
810+
tags = {"마이페이지"}
811+
)
812+
@ApiResponses({
813+
@ApiResponse(
814+
responseCode = "200",
815+
description = "내가 스크랩한 직관 일지 목록 조회 성공",
816+
content = @Content(
817+
mediaType = "application/json",
818+
schema = @Schema(implementation = SliceResponse.class)
819+
)
820+
)
821+
})
822+
@GetMapping("/my/scrapped")
823+
public ResponseEntity<SuccessResponse<SliceResponse<JournalFeedResDto>>> getMyScrappedJournals(
824+
@Parameter(hidden = true)
825+
@AuthenticationPrincipal CustomUserDetails user,
826+
827+
@Parameter(description = "조회할 페이지 번호 (0부터 시작)", example = "0")
828+
@RequestParam(defaultValue = "0") int page,
829+
830+
@Parameter(description = "한 페이지당 일지 개수", example = "10")
831+
@RequestParam(defaultValue = "10") int size
832+
) {
833+
Pageable pageable = PageRequest.of(page, size);
834+
SliceResponse<JournalFeedResDto> result = journalUsecase.getMyScrappedJournals(user.getMemberId(), pageable);
835+
836+
return ResponseEntity.ok(SuccessResponse.success(SuccessCode.OK, result));
837+
}
838+
839+
@Operation(
840+
summary = "내가 좋아요 누른 직관 일지 목록 조회",
841+
description = """
842+
내가 좋아요를 누른 직관 일지 목록을 조회합니다.
843+
844+
✔ 최근 좋아요 누른 순으로 정렬되어 반환됩니다.
845+
✔ page는 0부터 시작합니다. (0=첫 페이지)
846+
✔ size는 한 페이지에서 가져올 일지 수를 의미합니다.
847+
✔ hasNext가 true이면 다음 페이지 요청이 가능합니다.
848+
""",
849+
tags = {"마이페이지"}
850+
)
851+
@ApiResponses({
852+
@ApiResponse(
853+
responseCode = "200",
854+
description = "내가 좋아요 누른 직관 일지 목록 조회 성공",
855+
content = @Content(
856+
mediaType = "application/json",
857+
schema = @Schema(implementation = SliceResponse.class)
858+
)
859+
)
860+
})
861+
@GetMapping("/my/liked")
862+
public ResponseEntity<SuccessResponse<SliceResponse<JournalFeedResDto>>> getMyLikedJournals(
863+
@Parameter(hidden = true)
864+
@AuthenticationPrincipal CustomUserDetails user,
865+
866+
@Parameter(description = "조회할 페이지 번호 (0부터 시작)", example = "0")
867+
@RequestParam(defaultValue = "0") int page,
868+
869+
@Parameter(description = "한 페이지당 일지 개수", example = "10")
870+
@RequestParam(defaultValue = "10") int size
871+
) {
872+
Pageable pageable = PageRequest.of(page, size);
873+
SliceResponse<JournalFeedResDto> result = journalUsecase.getMyLikedJournals(user.getMemberId(), pageable);
874+
875+
return ResponseEntity.ok(SuccessResponse.success(SuccessCode.OK, result));
876+
}
877+
878+
@Operation(
879+
summary = "인기 직관 일지 목록 조회",
880+
description = """
881+
좋아요 10개 이상인 공개 직관 일지 목록을 조회합니다.
882+
883+
✔ 최신순(createdAt DESC)으로 정렬되어 반환됩니다.
884+
✔ page는 0부터 시작합니다. (0=첫 페이지)
885+
✔ size는 한 페이지에서 가져올 일지 수를 의미합니다.
886+
✔ hasNext가 true이면 다음 페이지 요청이 가능합니다.
887+
""",
888+
tags = {"마이페이지"}
889+
)
890+
@ApiResponses({
891+
@ApiResponse(
892+
responseCode = "200",
893+
description = "인기 직관 일지 목록 조회 성공",
894+
content = @Content(
895+
mediaType = "application/json",
896+
schema = @Schema(implementation = SliceResponse.class)
897+
)
898+
)
899+
})
900+
@GetMapping("/popular")
901+
public ResponseEntity<SuccessResponse<SliceResponse<JournalFeedResDto>>> getPopularJournals(
902+
@Parameter(hidden = true)
903+
@AuthenticationPrincipal CustomUserDetails user,
904+
905+
@Parameter(description = "조회할 페이지 번호 (0부터 시작)", example = "0")
906+
@RequestParam(defaultValue = "0") int page,
907+
908+
@Parameter(description = "한 페이지당 일지 개수", example = "10")
909+
@RequestParam(defaultValue = "10") int size
910+
) {
911+
Pageable pageable = PageRequest.of(page, size);
912+
SliceResponse<JournalFeedResDto> result = journalUsecase.getPopularJournals(user.getMemberId(), pageable);
913+
914+
return ResponseEntity.ok(SuccessResponse.success(SuccessCode.OK, result));
915+
}
760916
}
761917

src/main/java/com/inninglog/inninglog/domain/journal/repository/JournalRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public interface JournalRepository extends JpaRepository<Journal, Long> {
1919

2020
List<Journal> findAllByMemberAndResultScore(Member member, ResultScore resultScore);
2121

22+
long countByMember(Member member);
23+
24+
long countByMemberAndResultScore(Member member, ResultScore resultScore);
25+
2226
Page<Journal> findAllByMember(Member member, Pageable pageable);
2327

2428
Page<Journal> findAllByMemberAndResultScore(Member member, ResultScore resultScore, Pageable pageable);
@@ -42,4 +46,12 @@ public interface JournalRepository extends JpaRepository<Journal, Long> {
4246
@Query("SELECT j FROM Journal j JOIN FETCH j.member m JOIN FETCH m.team t " +
4347
"WHERE j.isPublic = true AND t.shortCode = :teamShortCode AND j.review_text LIKE %:keyword% ORDER BY j.createdAt DESC")
4448
Slice<Journal> searchPublicJournalsByTeam(@Param("keyword") String keyword, @Param("teamShortCode") String teamShortCode, Pageable pageable);
49+
50+
// 마이페이지: ID 목록으로 일지 조회 (N+1 최적화)
51+
@Query("SELECT j FROM Journal j JOIN FETCH j.member WHERE j.id IN :ids")
52+
List<Journal> findAllByIdInWithMember(@Param("ids") List<Long> ids);
53+
54+
// 인기 직관일지 조회: 좋아요 수 기준, 공개 일지만 (N+1 최적화)
55+
@Query("SELECT j FROM Journal j JOIN FETCH j.member WHERE j.likeCount >= :minLikeCount AND j.isPublic = true ORDER BY j.createdAt DESC")
56+
Slice<Journal> findPopularJournalsWithMember(@Param("minLikeCount") long minLikeCount, Pageable pageable);
4557
}

src/main/java/com/inninglog/inninglog/domain/journal/service/JournalGetService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import com.inninglog.inninglog.domain.member.domain.Member;
66
import com.inninglog.inninglog.global.exception.CustomException;
77
import com.inninglog.inninglog.global.exception.ErrorCode;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.function.Function;
11+
import java.util.stream.Collectors;
812
import lombok.RequiredArgsConstructor;
913
import lombok.extern.slf4j.Slf4j;
1014
import org.springframework.data.domain.Pageable;
@@ -56,4 +60,16 @@ public Slice<Journal> searchPublicJournals(String keyword, Pageable pageable) {
5660
public Slice<Journal> searchPublicJournalsByTeam(String keyword, String teamShortCode, Pageable pageable) {
5761
return journalRepository.searchPublicJournalsByTeam(keyword, teamShortCode, pageable);
5862
}
63+
64+
// 마이페이지: ID 목록으로 일지 조회 (순서 보존)
65+
@Transactional(readOnly = true)
66+
public List<Journal> findAllByIds(List<Long> ids) {
67+
return journalRepository.findAllByIdInWithMember(ids);
68+
}
69+
70+
// 인기 직관일지 조회 (좋아요 수 기준)
71+
@Transactional(readOnly = true)
72+
public Slice<Journal> getPopularJournals(long minLikeCount, Pageable pageable) {
73+
return journalRepository.findPopularJournalsWithMember(minLikeCount, pageable);
74+
}
5975
}

src/main/java/com/inninglog/inninglog/domain/journal/usecase/JournalUsecase.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.inninglog.inninglog.domain.journal.usecase;
22

3+
import com.inninglog.inninglog.domain.comment.service.CommentGetService;
34
import com.inninglog.inninglog.domain.contentType.ContentType;
45
import com.inninglog.inninglog.domain.journal.domain.Journal;
56
import com.inninglog.inninglog.domain.journal.domain.ResultScore;
@@ -25,14 +26,17 @@
2526
import lombok.RequiredArgsConstructor;
2627
import org.springframework.data.domain.Page;
2728
import org.springframework.data.domain.Slice;
29+
import org.springframework.data.domain.SliceImpl;
2830
import org.springframework.stereotype.Service;
2931
import org.springframework.transaction.annotation.Transactional;
3032

3133
import org.springframework.data.domain.Pageable;
3234

3335
import java.time.LocalDate;
3436
import java.util.List;
37+
import java.util.Map;
3538
import java.util.Set;
39+
import java.util.function.Function;
3640
import java.util.stream.Collectors;
3741

3842
@Service
@@ -49,6 +53,7 @@ public class JournalUsecase {
4953
private final S3Uploader s3Uploader;
5054
private final LikeValidateService likeValidateService;
5155
private final ScrapValidateService scrapValidateService;
56+
private final CommentGetService commentGetService;
5257

5358

5459
//직관 일지 생성
@@ -248,4 +253,94 @@ public SliceResponse<JournalSumListResDto> getMyJournals(Member member, Pageable
248253

249254
return SliceResponse.of(dtoSlice);
250255
}
256+
257+
// 마이페이지: 내가 댓글 단 직관일지
258+
@Transactional(readOnly = true)
259+
public SliceResponse<JournalFeedResDto> getMyCommentedJournals(Long memberId, Pageable pageable) {
260+
Member member = memberValidateService.findById(memberId);
261+
Slice<Long> journalIdSlice = commentGetService.getCommentedContentIds(member, ContentType.JOURNAL, pageable);
262+
return toFeedPage(journalIdSlice, member, pageable);
263+
}
264+
265+
// 마이페이지: 내가 스크랩한 직관일지
266+
@Transactional(readOnly = true)
267+
public SliceResponse<JournalFeedResDto> getMyScrappedJournals(Long memberId, Pageable pageable) {
268+
Member member = memberValidateService.findById(memberId);
269+
Slice<Long> journalIdSlice = scrapValidateService.getScrappedContentIds(member, ContentType.JOURNAL, pageable);
270+
return toFeedPage(journalIdSlice, member, pageable);
271+
}
272+
273+
// 마이페이지: 내가 좋아요 누른 직관일지
274+
@Transactional(readOnly = true)
275+
public SliceResponse<JournalFeedResDto> getMyLikedJournals(Long memberId, Pageable pageable) {
276+
Member member = memberValidateService.findById(memberId);
277+
Slice<Long> journalIdSlice = likeValidateService.getLikedContentIds(member, ContentType.JOURNAL, pageable);
278+
return toFeedPage(journalIdSlice, member, pageable);
279+
}
280+
281+
// 인기 직관일지 조회 (좋아요 10개 이상, 공개)
282+
@Transactional(readOnly = true)
283+
public SliceResponse<JournalFeedResDto> getPopularJournals(Long memberId, Pageable pageable) {
284+
Member member = memberValidateService.findById(memberId);
285+
Slice<Journal> journals = journalGetService.getPopularJournals(10L, pageable);
286+
287+
List<Long> journalIds = journals.getContent().stream()
288+
.map(Journal::getId)
289+
.toList();
290+
291+
Set<Long> likedIds = likeValidateService.findLikedTargetIds(ContentType.JOURNAL, journalIds, member);
292+
Set<Long> scrapedIds = scrapValidateService.findScrapedTargetIds(ContentType.JOURNAL, journalIds, member);
293+
294+
Slice<JournalFeedResDto> dtoSlice = journals.map(journal -> {
295+
boolean writedByMe = journal.getMember().getId().equals(memberId);
296+
boolean likedByMe = likedIds.contains(journal.getId());
297+
boolean scrapedByMe = scrapedIds.contains(journal.getId());
298+
299+
return JournalFeedResDto.from(
300+
journal,
301+
s3Uploader.getDirectUrl(journal.getMedia_url()),
302+
writedByMe,
303+
likedByMe,
304+
scrapedByMe
305+
);
306+
});
307+
308+
return SliceResponse.of(dtoSlice);
309+
}
310+
311+
// ID Slice → JournalFeedResDto Slice 변환 헬퍼 (N+1 최적화)
312+
private SliceResponse<JournalFeedResDto> toFeedPage(Slice<Long> journalIdSlice, Member member, Pageable pageable) {
313+
List<Long> journalIds = journalIdSlice.getContent();
314+
315+
if (journalIds.isEmpty()) {
316+
return SliceResponse.empty(pageable);
317+
}
318+
319+
List<Journal> journals = journalGetService.findAllByIds(journalIds);
320+
Map<Long, Journal> journalMap = journals.stream()
321+
.collect(Collectors.toMap(Journal::getId, Function.identity()));
322+
323+
Set<Long> likedIds = likeValidateService.findLikedTargetIds(ContentType.JOURNAL, journalIds, member);
324+
Set<Long> scrapedIds = scrapValidateService.findScrapedTargetIds(ContentType.JOURNAL, journalIds, member);
325+
326+
List<JournalFeedResDto> dtos = journalIds.stream()
327+
.map(journalMap::get)
328+
.filter(journal -> journal != null)
329+
.map(journal -> {
330+
boolean writedByMe = journal.getMember().getId().equals(member.getId());
331+
boolean likedByMe = likedIds.contains(journal.getId());
332+
boolean scrapedByMe = scrapedIds.contains(journal.getId());
333+
334+
return JournalFeedResDto.from(
335+
journal,
336+
s3Uploader.getDirectUrl(journal.getMedia_url()),
337+
writedByMe,
338+
likedByMe,
339+
scrapedByMe
340+
);
341+
})
342+
.toList();
343+
344+
return SliceResponse.of(dtos, journalIdSlice.hasNext(), pageable);
345+
}
251346
}

src/main/java/com/inninglog/inninglog/domain/like/repository/LikeRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.List;
88
import java.util.Optional;
99
import java.util.Set;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.data.domain.Slice;
1012
import org.springframework.data.jpa.repository.JpaRepository;
1113
import org.springframework.data.jpa.repository.Modifying;
1214
import org.springframework.data.jpa.repository.Query;
@@ -26,4 +28,8 @@ public interface LikeRepository extends JpaRepository<Like, Long> {
2628
@Modifying
2729
@Query("DELETE FROM Like l WHERE l.contentType = :contentType AND l.targetId = :targetId")
2830
void deleteAllByContent(ContentType contentType, Long targetId);
31+
32+
// 마이페이지: 내가 좋아요 누른 콘텐츠 ID 조회 (최신순)
33+
@Query("SELECT l.targetId FROM Like l WHERE l.member = :member AND l.contentType = :contentType ORDER BY l.createdAt DESC")
34+
Slice<Long> findTargetIdsByMemberAndContentType(Member member, ContentType contentType, Pageable pageable);
2935
}

src/main/java/com/inninglog/inninglog/domain/like/service/LikeValidateService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import java.util.List;
1212
import java.util.Set;
1313
import lombok.RequiredArgsConstructor;
14+
import org.springframework.data.domain.Pageable;
15+
import org.springframework.data.domain.Slice;
1416
import org.springframework.stereotype.Service;
1517
import org.springframework.transaction.annotation.Transactional;
1618

@@ -39,6 +41,12 @@ public Set<Long> findLikedTargetIds(ContentType contentType, List<Long> targetId
3941
return likeRepository.findLikedTargetIds(contentType, targetIds, member);
4042
}
4143

44+
//마이페이지: 내가 좋아요 누른 콘텐츠 ID 조회
45+
@Transactional(readOnly = true)
46+
public Slice<Long> getLikedContentIds(Member member, ContentType contentType, Pageable pageable) {
47+
return likeRepository.findTargetIdsByMemberAndContentType(member, contentType, pageable);
48+
}
49+
4250
//이미 좋아요 누른건지 확인
4351
@Transactional
4452
public Like getLike(ContentType contentType, Long targetId, Member member){

0 commit comments

Comments
 (0)