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
Expand Up @@ -39,53 +39,72 @@ List<Object[]> findDailyActivityCountsByUserAndPeriod(
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to);

// DP-293: 2단계 페이징 - 1단계: ID만 조회 (actionType 필터 있을 때)
// DP-293: 2단계 페이징 - 1단계: ID만 조회 (actionType 필터 있을 때, 날짜 필터 없음)
@Query(value = "SELECT h.id FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType IN :actionTypes " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate) " +
"ORDER BY h.createdAt DESC",
countQuery = "SELECT COUNT(h) FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType IN :actionTypes")
Page<UUID> findHistoryIdsByActionTypes(
@Param("userId") UUID userId,
@Param("actionTypes") List<String> actionTypes,
Pageable pageable);

// DP-293: 2단계 페이징 - 1단계: ID만 조회 (actionType 필터 있을 때, 날짜 필터 있음)
@Query(value = "SELECT h.id FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType IN :actionTypes " +
"AND h.createdAt >= :startDate " +
"AND h.createdAt <= :endDate " +
"ORDER BY h.createdAt DESC",
countQuery = "SELECT COUNT(h) FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType IN :actionTypes " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate)")
"AND h.createdAt >= :startDate " +
"AND h.createdAt <= :endDate")
Page<UUID> findHistoryIdsByActionTypesAndDateRange(
@Param("userId") UUID userId,
@Param("actionTypes") List<String> actionTypes,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);

// DP-293: 2단계 페이징 - 1단계: ID만 조회 (actionType 필터 없을 때)
// DP-293: 2단계 페이징 - 1단계: ID만 조회 (전체, 날짜 필터 없음)
@Query(value = "SELECT h.id FROM History h " +
"WHERE h.user.id = :userId " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate) " +
"ORDER BY h.createdAt DESC",
countQuery = "SELECT COUNT(h) FROM History h " +
"WHERE h.user.id = :userId")
Page<UUID> findAllHistoryIds(
@Param("userId") UUID userId,
Pageable pageable);

/** 학습 히스토리: content_liked 제외, 날짜 필터 없음 */
@Query(value = "SELECT h.id FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType <> 'content_liked' " +
"ORDER BY h.createdAt DESC",
countQuery = "SELECT COUNT(h) FROM History h " +
"WHERE h.user.id = :userId " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate)")
Page<UUID> findHistoryIdsByDateRange(
"AND h.actionType <> 'content_liked'")
Page<UUID> findHistoryIdsExcludingContentLiked(
@Param("userId") UUID userId,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);

/** 학습 히스토리: content_liked 제외 (GET /history 기본) */
/** 학습 히스토리: content_liked 제외, 날짜 필터 있음 */
@Query(value = "SELECT h.id FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType <> 'content_liked' " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate) " +
"AND h.createdAt >= :startDate " +
"AND h.createdAt <= :endDate " +
"ORDER BY h.createdAt DESC",
countQuery = "SELECT COUNT(h) FROM History h " +
"WHERE h.user.id = :userId " +
"AND h.actionType <> 'content_liked' " +
"AND (:startDate IS NULL OR h.createdAt >= :startDate) " +
"AND (:endDate IS NULL OR h.createdAt <= :endDate)")
"AND h.createdAt >= :startDate " +
"AND h.createdAt <= :endDate")
Page<UUID> findHistoryIdsByDateRangeExcludingContentLiked(
@Param("userId") UUID userId,
@Param("startDate") LocalDateTime startDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,20 @@ public HistoryPageResponse getHistory(UUID userId, List<String> actionTypes,
? endDate.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime() : null;

// 1단계: SQL LIMIT/OFFSET이 적용된 ID 페이징 조회
// DP-310: PostgreSQL이 null 파라미터의 타입을 추론 못하는 문제 방지 — null 여부에 따라 메서드 분기
Page<UUID> idPage;
if (actionTypes != null && !actionTypes.isEmpty()) {
idPage = historyRepository.findHistoryIdsByActionTypesAndDateRange(userId, actionTypes, start, end, pageable);
if (start == null && end == null) {
idPage = historyRepository.findHistoryIdsByActionTypes(userId, actionTypes, pageable);
} else {
idPage = historyRepository.findHistoryIdsByActionTypesAndDateRange(userId, actionTypes, start, end, pageable);
}
} else {
idPage = historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(userId, start, end, pageable);
if (start == null && end == null) {
idPage = historyRepository.findHistoryIdsExcludingContentLiked(userId, pageable);
} else {
idPage = historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(userId, start, end, pageable);
}
}

if (idPage.isEmpty()) {
Expand Down Expand Up @@ -74,7 +83,7 @@ public ActivityPageResponse getActivityHistory(UUID userId, Pageable pageable) {
userRepository.findByIdAndIsActiveTrue(userId)
.orElseThrow(() -> new DevpickException(ErrorCode.USER_NOT_FOUND));

Page<UUID> idPage = historyRepository.findHistoryIdsByDateRange(userId, null, null, pageable);
Page<UUID> idPage = historyRepository.findAllHistoryIds(userId, pageable);

if (idPage.isEmpty()) {
return new ActivityPageResponse(List.of(), idPage.getNumber(), idPage.getSize(), 0, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.devpick.domain.community.entity.Post;
import com.devpick.domain.content.entity.Content;
import com.devpick.domain.report.dto.ActivityPageResponse;
import com.devpick.domain.report.dto.HistoryPageResponse;
import com.devpick.domain.report.entity.History;
import com.devpick.domain.report.repository.HistoryRepository;
Expand All @@ -26,6 +27,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.test.util.ReflectionTestUtils;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -35,7 +37,6 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
Expand Down Expand Up @@ -85,7 +86,7 @@ void getLearningHistory_success_withContent() {

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(eq(userId), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsExcludingContentLiked(eq(userId), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));
Expand Down Expand Up @@ -116,7 +117,7 @@ void getLearningHistory_historyWithPost_postInfoPopulated() {

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(eq(userId), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsExcludingContentLiked(eq(userId), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));
Expand All @@ -134,7 +135,7 @@ void getLearningHistory_historyWithPost_postInfoPopulated() {
void getLearningHistory_emptyHistory_returnsEmptyItems() {
Page<UUID> emptyIdPage = new PageImpl<>(List.of(), pageable, 0);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(eq(userId), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsExcludingContentLiked(eq(userId), any(Pageable.class)))
.willReturn(emptyIdPage);

HistoryPageResponse response = historyService.getHistory(userId, null, null, null, pageable);
Expand Down Expand Up @@ -165,7 +166,7 @@ void getLearningHistory_historyWithNoContentAndPost_bothNull() {

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(eq(userId), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsExcludingContentLiked(eq(userId), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));
Expand All @@ -188,7 +189,7 @@ void historyItemResponse_of_pointsMappedCorrectly(String actionType, Integer exp

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(eq(userId), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsExcludingContentLiked(eq(userId), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));
Expand Down Expand Up @@ -225,17 +226,93 @@ void getLearningHistory_withActionTypes_usesActionTypeQuery() {

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByActionTypesAndDateRange(
eq(userId), eq(actionTypes), isNull(), isNull(), any(Pageable.class)))
given(historyRepository.findHistoryIdsByActionTypes(
eq(userId), eq(actionTypes), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));

HistoryPageResponse response = historyService.getHistory(userId, actionTypes, null, null, pageable);

assertThat(response.items()).hasSize(1);
verify(historyRepository).findHistoryIdsByActionTypes(
eq(userId), eq(actionTypes), any(Pageable.class));
verify(historyRepository, never()).findHistoryIdsExcludingContentLiked(any(), any());
}

@Test
@DisplayName("날짜 필터 있을 때 (actionTypes 없음) - 날짜 범위 쿼리가 호출된다")
void getLearningHistory_withDateRange_usesDateRangeQuery() {
OffsetDateTime start = OffsetDateTime.parse("2026-04-01T00:00:00Z");
OffsetDateTime end = OffsetDateTime.parse("2026-04-07T23:59:59Z");

History history = History.builder()
.user(user).actionType("content_opened").build();
UUID historyId = UUID.randomUUID();
ReflectionTestUtils.setField(history, "id", historyId);

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByDateRangeExcludingContentLiked(
eq(userId), any(), any(), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));

HistoryPageResponse response = historyService.getHistory(userId, null, start, end, pageable);

assertThat(response.items()).hasSize(1);
verify(historyRepository).findHistoryIdsByDateRangeExcludingContentLiked(
eq(userId), any(), any(), any(Pageable.class));
verify(historyRepository, never()).findHistoryIdsExcludingContentLiked(any(), any());
}

@Test
@DisplayName("날짜 필터 + actionTypes 모두 있을 때 - actionTypes+날짜 범위 쿼리가 호출된다")
void getLearningHistory_withActionTypesAndDateRange_usesActionTypesDateRangeQuery() {
List<String> actionTypes = List.of("content_opened");
OffsetDateTime start = OffsetDateTime.parse("2026-04-01T00:00:00Z");
OffsetDateTime end = OffsetDateTime.parse("2026-04-07T23:59:59Z");

History history = History.builder()
.user(user).actionType("content_opened").build();
UUID historyId = UUID.randomUUID();
ReflectionTestUtils.setField(history, "id", historyId);

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findHistoryIdsByActionTypesAndDateRange(
eq(userId), eq(actionTypes), any(), any(), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));

HistoryPageResponse response = historyService.getHistory(userId, actionTypes, start, end, pageable);

assertThat(response.items()).hasSize(1);
verify(historyRepository).findHistoryIdsByActionTypesAndDateRange(
eq(userId), eq(actionTypes), isNull(), isNull(), any(Pageable.class));
verify(historyRepository, never()).findHistoryIdsByDateRangeExcludingContentLiked(any(), any(), any(), any());
eq(userId), eq(actionTypes), any(), any(), any(Pageable.class));
verify(historyRepository, never()).findHistoryIdsByActionTypes(any(), any(), any());
}

@Test
@DisplayName("활동 히스토리 조회 - 정상 반환")
void getActivityHistory_success() {
History history = History.builder()
.user(user).actionType("content_liked").build();
UUID historyId = UUID.randomUUID();
ReflectionTestUtils.setField(history, "id", historyId);

Page<UUID> idPage = new PageImpl<>(List.of(historyId), pageable, 1);
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
given(historyRepository.findAllHistoryIds(eq(userId), any(Pageable.class)))
.willReturn(idPage);
given(historyRepository.findHistoriesWithAssociationsByIds(List.of(historyId)))
.willReturn(List.of(history));

ActivityPageResponse response = historyService.getActivityHistory(userId, pageable);

assertThat(response.items()).hasSize(1);
assertThat(response.totalElements()).isEqualTo(1L);
}
}
Loading