Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package UMC.news.newsIntelligent.domain.member.controller;

import UMC.news.newsIntelligent.domain.member.dto.MemberTopicResponseDTO;
import UMC.news.newsIntelligent.domain.member.service.MemberTopicQueryService;
import UMC.news.newsIntelligent.global.apiPayload.CustomResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/api/mypage")
@RequiredArgsConstructor
@Tag(name = "마이페이지 내에서의 토픽 목록 조회 컨트롤러", description = "마이페이지 내에서의 조회와 관련된 API들을 관리하는 컨트롤러")
public class MemberTopicController {

private final MemberTopicQueryService memberTopicQueryService;

@Operation(summary = "읽은 토픽 목록 내 조회 API by 서동혁", description = "읽은 토픽 목록 내 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토픽 조회 성공")
})
@GetMapping("/read-topics")
public CustomResponse<MemberTopicResponseDTO.MemberTopicPreviewListResDTO> searchReadTopics(
@RequestParam String keyword,
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") @Max(10) int size
// @AuthenticationPrincipal PrincipalDetails principalDetails
) {

// 로그인 기능 구현 이후 memberId(1) 대신 매개변수로 principalDetails
MemberTopicResponseDTO.MemberTopicPreviewListResDTO topicResDTO =
memberTopicQueryService.searchReadTopics(keyword, cursor, size, 1L);

return CustomResponse.onSuccess(topicResDTO);
}

@Operation(summary = "읽은 토픽 목록 리스트 조회 API by 서동혁", description = "읽은 토픽 목록 리스트 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토픽 리스트 조회 성공")
})
@GetMapping("/read-topics")
public CustomResponse<MemberTopicResponseDTO.MemberTopicPreviewListResDTO> getReadTopics(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") @Max(10) int size
// @AuthenticationPrincipal PrincipalDetails principalDetails
) {

// 로그인 기능 구현 이후 memberId(1) 대신 매개변수로 principalDetails
MemberTopicResponseDTO.MemberTopicPreviewListResDTO topicResDTO =
memberTopicQueryService.getReadTopics(cursor, size, 1L);

return CustomResponse.onSuccess(topicResDTO);
}

@Operation(summary = "구독 토픽 리스트 조회 API by 서동혁", description = "구독 토픽 리스트 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "구독 토픽 리스트 조회 성공")
})
@GetMapping("/subscriptions")
public CustomResponse<MemberTopicResponseDTO.MemberTopicPreviewListResDTO> getSubscriptionTopics(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") @Max(10) int size
// @AuthenticationPrincipal PrincipalDetails principalDetails
) {

// 로그인 기능 구현 이후 memberId(1) 대신 매개변수로 principalDetails
MemberTopicResponseDTO.MemberTopicPreviewListResDTO topicResDTO =
memberTopicQueryService.getSubscriptionTopics(cursor, size, 1L);

return CustomResponse.onSuccess(topicResDTO);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package UMC.news.newsIntelligent.domain.member.converter;

import UMC.news.newsIntelligent.domain.member.dto.MemberTopicResponseDTO;
import UMC.news.newsIntelligent.domain.topic.entity.Topic;

public class MemberTopicConverter {

public static MemberTopicResponseDTO.MemberTopicPreviewResDTO toPreviewResDTO(Topic topic) {

return MemberTopicResponseDTO.MemberTopicPreviewResDTO.builder()
.id(topic.getId())
.topicName(topic.getTopicName())
.aiSummary(topic.getAiSummary())
.imageUrl(topic.getImageUrl())
.summaryTime(topic.getSummaryTime())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package UMC.news.newsIntelligent.domain.member.dto;

import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;

@Getter
public class MemberTopicResponseDTO {

@Builder
public record MemberTopicPreviewResDTO(
Long id,
String topicName,
String aiSummary,
LocalDateTime summaryTime,
String imageUrl
) {}

@Builder
public record MemberTopicPreviewListResDTO(
Long cursor,
Boolean hasNext,
List<MemberTopicResponseDTO.MemberTopicPreviewResDTO> topics
) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package UMC.news.newsIntelligent.domain.member.entity;

import java.time.LocalDateTime;

import java.util.ArrayList;
import java.util.List;

Expand All @@ -14,6 +15,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;

import lombok.*;

@Entity
Expand Down Expand Up @@ -61,7 +63,6 @@ public class Member extends BaseEntity {
orphanRemoval = true)
private List<MemberTopic> memberTopics = new ArrayList<>();


public static Member newMember(String email) {
return Member.builder()
.email(email)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package UMC.news.newsIntelligent.domain.member.entity;

import UMC.news.newsIntelligent.domain.topic.Topic;
import UMC.news.newsIntelligent.domain.topic.entity.Topic;
import UMC.news.newsIntelligent.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -12,8 +12,11 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

import lombok.Getter;

@Entity
@Table(name = "member_topic")
@Getter
public class MemberTopic extends BaseEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package UMC.news.newsIntelligent.domain.member.repository;

import UMC.news.newsIntelligent.domain.member.entity.MemberTopic;
import UMC.news.newsIntelligent.domain.topic.entity.Topic;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface MemberTopicRepository extends JpaRepository<MemberTopic, Long> {

// memberTopic 에서 isRead = true 인 것 중, topicName 을 기준으로 keyword 검색 & 커서 페이지네이션 구현
@Query("""
SELECT mt.topic FROM MemberTopic mt
WHERE mt.member.id = :memberId
AND mt.isRead = true
AND (:keyword IS NULL OR mt.topic.topicName LIKE %:keyword%)
AND mt.topic.id < :cursor
ORDER BY mt.topic.id DESC
""")
Slice<Topic> searchReadTopicsByKeyword(
@Param("memberId") Long memberId,
@Param("keyword") String keyword,
@Param("cursor") Long cursor,
Pageable pageable
);

@Query("""
SELECT mt.topic FROM MemberTopic mt
WHERE mt.member.id = :memberId
AND mt.isRead = true
AND mt.topic.id < :cursor
ORDER BY mt.topic.id DESC
""")
Slice<Topic> getReadTopicsByMemberId(@Param("memberId") Long memberId, @Param("cursor") Long cursor, Pageable pageable);

@Query("""
SELECT mt.topic FROM MemberTopic mt
WHERE mt.member.id = :memberId
AND mt.isSubscribe = true
AND mt.topic.id < :cursor
ORDER BY mt.topic.id DESC
""")
Slice<Topic> getSubscriptionTopicsByMemberId(@Param("memberId") Long memberId, @Param("cursor") Long cursor, Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package UMC.news.newsIntelligent.domain.member.service;

import UMC.news.newsIntelligent.domain.member.dto.MemberTopicResponseDTO;

public interface MemberTopicQueryService {

MemberTopicResponseDTO.MemberTopicPreviewListResDTO searchReadTopics(String keyword, Long cursor, int size, Long memberId);
MemberTopicResponseDTO.MemberTopicPreviewListResDTO getReadTopics(Long cursor, int size, Long memberId);
MemberTopicResponseDTO.MemberTopicPreviewListResDTO getSubscriptionTopics(Long cursor, int size, Long memberId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중복된 플로우 추출해 private로 캡슐화 하고, search / get / subscription만 퍼블릭으로 빼는 것도 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 코드 리펙토링 할 때 참고하겠습니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package UMC.news.newsIntelligent.domain.member.service;

import UMC.news.newsIntelligent.domain.member.converter.MemberTopicConverter;
import UMC.news.newsIntelligent.domain.member.dto.MemberTopicResponseDTO;
import UMC.news.newsIntelligent.domain.member.repository.MemberTopicRepository;
import UMC.news.newsIntelligent.domain.topic.entity.Topic;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberTopicQueryServiceImpl implements MemberTopicQueryService {

private final MemberTopicRepository memberTopicRepository;

@Override
public MemberTopicResponseDTO.MemberTopicPreviewListResDTO searchReadTopics(String keyword, Long cursor, int size, Long memberId) {
cursor = normalizeCursor(cursor);
size = normalizeSize(size);

Pageable pageable = PageRequest.of(0, size);
Slice<Topic> topicSlice = memberTopicRepository.searchReadTopicsByKeyword(memberId, keyword, cursor, pageable);

List<MemberTopicResponseDTO.MemberTopicPreviewResDTO> topicList = topicSlice.stream()
.map(MemberTopicConverter::toPreviewResDTO)
.toList();

Long nextCursor = topicSlice.hasNext() ? topicList.get(topicList.size() - 1).id() : null;

return MemberTopicResponseDTO.MemberTopicPreviewListResDTO.builder()
.cursor(nextCursor)
.hasNext(topicSlice.hasNext())
.topics(topicList)
.build();
}

@Override
public MemberTopicResponseDTO.MemberTopicPreviewListResDTO getReadTopics(Long cursor, int size, Long memberId) {
cursor = normalizeCursor(cursor);
size = normalizeSize(size);

Pageable pageable = PageRequest.of(0, size);
Slice<Topic> topicSlice = memberTopicRepository.getReadTopicsByMemberId(memberId, cursor, pageable);

List<MemberTopicResponseDTO.MemberTopicPreviewResDTO> topicList = topicSlice.stream()
.map(MemberTopicConverter::toPreviewResDTO)
.toList();

Long nextCursor = topicSlice.hasNext() ? topicList.get(topicList.size() - 1).id() : null;

return MemberTopicResponseDTO.MemberTopicPreviewListResDTO.builder()
.cursor(nextCursor)
.hasNext(topicSlice.hasNext())
.topics(topicList)
.build();
}

@Override
public MemberTopicResponseDTO.MemberTopicPreviewListResDTO getSubscriptionTopics(Long cursor, int size, Long memberId) {
cursor = normalizeCursor(cursor);
size = normalizeSize(size);

Pageable pageable = PageRequest.of(0, size);
Slice<Topic> topicSlice = memberTopicRepository.getSubscriptionTopicsByMemberId(memberId, cursor, pageable);

List<MemberTopicResponseDTO.MemberTopicPreviewResDTO> topicList = topicSlice.stream()
.map(MemberTopicConverter::toPreviewResDTO)
.toList();

Long nextCursor = topicSlice.hasNext() ? topicList.get(topicList.size() - 1).id() : null;

return MemberTopicResponseDTO.MemberTopicPreviewListResDTO.builder()
.cursor(nextCursor)
.hasNext(topicSlice.hasNext())
.topics(topicList)
.build();
}

private Long normalizeCursor(Long cursor) {
return (cursor == 0) ? Long.MAX_VALUE : cursor;
}

// 요청 사이즈가 1보다 작으면 기본값 10, 10보다 크면 최대값 10으로 제한
private int normalizeSize(int size) {
return (size < 1 || size > 10) ? 10 : size;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package UMC.news.newsIntelligent.domain.news;

import UMC.news.newsIntelligent.domain.topic.Topic;
import UMC.news.newsIntelligent.domain.topic.entity.Topic;
import UMC.news.newsIntelligent.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package UMC.news.newsIntelligent.domain.topic.controller;

import UMC.news.newsIntelligent.domain.topic.dto.TopicResponseDTO;
import UMC.news.newsIntelligent.domain.topic.service.query.TopicQueryService;
import UMC.news.newsIntelligent.global.apiPayload.CustomResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/topics")
@RequiredArgsConstructor
@Tag(name = "토픽 컨트롤러", description = "토픽과 관련된 API들을 관리하는 컨트롤러")
public class TopicController {

private final TopicQueryService topicQueryService;

@Operation(summary = "토픽 조회 API by 서동혁", description = "토픽 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토픽 조회 성공")
})
@GetMapping("/search")
public CustomResponse<TopicResponseDTO.TopicPreviewListResDTO> searchTopics(
@RequestParam String keyword,
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") @Max(10) int size
) {
TopicResponseDTO.TopicPreviewListResDTO topicResDTO = topicQueryService.searchTopics(keyword, cursor, size);

return CustomResponse.onSuccess(topicResDTO);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package UMC.news.newsIntelligent.domain.topic.converter;

import UMC.news.newsIntelligent.domain.topic.entity.Topic;
import UMC.news.newsIntelligent.domain.topic.dto.TopicResponseDTO;

public class TopicConverter {

public static TopicResponseDTO.TopicPreviewResDTO toPreviewResDTO(Topic topic) {
return TopicResponseDTO.TopicPreviewResDTO.builder()
.id(topic.getId())
.topicName(topic.getTopicName())
.aiSummary(topic.getAiSummary())
.imageUrl(topic.getImageUrl())
.summaryTime(topic.getSummaryTime())
.build();
}
}
Loading