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 @@ -75,11 +75,13 @@ public ResponseEntity<ApiResponse<ShareResponse>> create(
@ApiDocs.ListQuery
@Operation(summary = "내 공유 목록 조회",
description = "내 공유를 최신순으로 페이지 조회합니다. 상태 필터는 생략할 수 있고, "
+ "결과가 없으면 200과 빈 items를 반환합니다. share_url 은 저장된 것이 없어 "
+ "결과가 없으면 200과 빈 items를 반환합니다. ACTIVE 필터는 만료 시각이 "
+ "지나지 않은 것만 돌려줍니다. share_url 은 저장된 것이 없어 "
+ "생성 응답으로만 내려간다.")
public ApiResponse<PageResponse<ShareSummaryResponse>> list(
@AuthenticationPrincipal UUID userId,
@Parameter(description = "상태 필터(ACTIVE/REVOKED). 생략 시 전체")
@Parameter(description = "상태 필터(ACTIVE/REVOKED). 생략 시 전체. "
+ "ACTIVE 는 만료되지 않은 것만 포함한다")
@RequestParam(required = false) ShareStatus status,
@Parameter(description = "0부터 시작하는 페이지 번호. 음수면 400 INVALID_REQUEST")
@RequestParam(defaultValue = "0") int page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ interface ShareJpaRepository extends JpaRepository<ShareEntity, UUID> {

Page<ShareEntity> findByUserIdAndStatus(UUID userId, String status, Pageable pageable);

/** ACTIVE 필터 전용. 상태가 ACTIVE 라도 만료 시각이 지났으면 더 이상 열리지 않는다. */
Page<ShareEntity> findByUserIdAndStatusAndExpiresAtAfter(
UUID userId, String status, Instant now, Pageable pageable);

void deleteAllByUserId(UUID userId);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,9 @@ public Optional<Share> findByTokenHash(String tokenHash) {
}

@Override
public SharePage findPage(UUID userId, ShareStatus status, int page, int size) {
Page<ShareEntity> result = status == null
? shareRepository.findByUserId(userId, pageRequest(page, size))
: shareRepository.findByUserIdAndStatus(userId, status.name(),
pageRequest(page, size));
public SharePage findPage(
UUID userId, ShareStatus status, int page, int size, Instant now) {
Page<ShareEntity> result = findEntityPage(userId, status, pageRequest(page, size), now);
return new SharePage(
result.getContent().stream().map(ShareEntity::toDomain).toList(),
result.getTotalElements());
Expand All @@ -86,6 +84,22 @@ public Set<UUID> findSharedRecordIds(UUID ownerId, Collection<UUID> recordIds, I
ownerId, recordIds, ShareStatus.ACTIVE.name(), now);
}

/**
* ACTIVE 만 만료 조건이 붙는다. REVOKED 는 철회 시각이 이미 상태로 굳었고, 전체 조회는
* 만료된 링크까지 보여 주는 것이 목록의 목적이라 그대로 둔다.
*/
private Page<ShareEntity> findEntityPage(
UUID userId, ShareStatus status, PageRequest pageRequest, Instant now) {
if (status == null) {
return shareRepository.findByUserId(userId, pageRequest);
}
if (status == ShareStatus.ACTIVE) {
return shareRepository.findByUserIdAndStatusAndExpiresAtAfter(
userId, status.name(), now, pageRequest);
}
return shareRepository.findByUserIdAndStatus(userId, status.name(), pageRequest);
}

private PageRequest pageRequest(int page, int size) {
int boundedSize = Math.min(Math.max(size, 1), MAX_PAGE_SIZE);
return PageRequest.of(Math.max(page, 0), boundedSize, Sort.by(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public ListSharesUseCase.Result list(ListSharesUseCase.Query query) {
throw new ApplicationException(ErrorCode.INVALID_REQUEST);
}
SharePage page = shareRepositoryPort.findPage(
query.requesterId(), query.status(), query.page(), query.size());
query.requesterId(), query.status(), query.page(), query.size(), Instant.now());
return new ListSharesUseCase.Result(
page.items(), query.page(), query.size(), page.totalElements());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.heddy.domain.sharing.model.SharePage;
import com.heddy.domain.sharing.model.ShareStatus;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -30,8 +31,14 @@ public interface ShareRepositoryPort {
*/
void detachSavedStyle(UUID savedStyleId);

/** 소유자의 공유를 최신순으로 페이지 조회한다. 상태 필터는 생략 가능하다. */
SharePage findPage(UUID userId, ShareStatus status, int page, int size);
/**
* 소유자의 공유를 최신순으로 페이지 조회한다. 상태 필터는 생략 가능하다.
*
* <p>ACTIVE 필터는 {@code expiresAt > now} 까지 함께 본다. 만료는 상태가 아니라 시각
* 비교로 판정하는 것이 이 도메인의 설계라(V19 주석) 상태 컬럼만 보면 이미 죽은 링크가
* 활성으로 잡힌다.
*/
SharePage findPage(UUID userId, ShareStatus status, int page, int size, Instant now);

/** 회원 탈퇴 시 공개 링크와 연결 행을 함께 제거한다. */
void deleteAllByUserId(UUID userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.heddy.domain.sharing.model.Share;
import com.heddy.domain.sharing.model.ShareFieldType;
import com.heddy.domain.sharing.model.SharePage;
import com.heddy.domain.sharing.model.ShareStatus;
import com.heddy.support.PostgresIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -108,6 +109,49 @@ void answersWithoutQueryingWhenThereAreNoRecords() {
assertThat(adapter.findSharedRecordIds(OWNER_ID, List.of(), NOW)).isEmpty();
}

// ------------------------------------------------------------- 목록 상태 필터

/**
* ACTIVE 필터는 상태만 보면 안 된다. 만료는 상태가 아니라 시각 비교로 판정하는 설계라
* 이미 열리지 않는 링크가 상태 컬럼에는 그대로 ACTIVE 로 남아 있다.
*/
@Test
void excludesExpiredSharesFromTheActiveFilter() {
UUID recordId = insertRecord(OWNER_ID);
insertShare(OWNER_ID, recordId, ShareStatus.ACTIVE, NOW.minusSeconds(1));
insertShare(OWNER_ID, recordId, ShareStatus.ACTIVE, NOW.plusSeconds(60));

SharePage page = adapter.findPage(OWNER_ID, ShareStatus.ACTIVE, 0, 20, NOW);

assertThat(page.items()).hasSize(1);
assertThat(page.totalElements()).isEqualTo(1);
assertThat(page.items().getFirst().expiresAt()).isEqualTo(NOW.plusSeconds(60));
}

/** 만료 시각이 현재와 같으면 이미 지난 것으로 본다. 공개 조회 판정과 같은 기준이다. */
@Test
void treatsAnExpiryEqualToNowAsExpiredInTheActiveFilter() {
insertShare(OWNER_ID, insertRecord(OWNER_ID), ShareStatus.ACTIVE, NOW);

assertThat(adapter.findPage(OWNER_ID, ShareStatus.ACTIVE, 0, 20, NOW).items()).isEmpty();
}

/** 전체 조회는 만료 여부로 거르지 않는다. 만료된 링크도 내 공유 이력이다. */
@Test
void keepsExpiredSharesWhenNoStatusFilterIsGiven() {
insertShare(OWNER_ID, insertRecord(OWNER_ID), ShareStatus.ACTIVE, NOW.minusSeconds(1));

assertThat(adapter.findPage(OWNER_ID, null, 0, 20, NOW).items()).hasSize(1);
}

/** REVOKED 필터에는 만료 조건이 붙지 않는다. 철회 시각이 이미 상태로 굳어 있다. */
@Test
void keepsExpiredSharesInTheRevokedFilter() {
insertShare(OWNER_ID, insertRecord(OWNER_ID), ShareStatus.REVOKED, NOW.minusSeconds(1));

assertThat(adapter.findPage(OWNER_ID, ShareStatus.REVOKED, 0, 20, NOW).items()).hasSize(1);
}

// ------------------------------------------------------------------ 헬퍼

private void insertUser(UUID userId, String email) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.within;
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.BDDMockito.then;
import static org.mockito.Mockito.never;
Expand Down Expand Up @@ -171,7 +173,8 @@ void rejectsSelectionWithoutFieldsBeforeOwnershipQueries() {
@Test
void listsOwnSharesWithStatusFilterAndPagination() {
List<Share> items = List.of(share(ShareStatus.REVOKED), share(ShareStatus.ACTIVE));
given(shareRepositoryPort.findPage(USER_ID, ShareStatus.ACTIVE, 0, 20))
given(shareRepositoryPort.findPage(eq(USER_ID), eq(ShareStatus.ACTIVE), eq(0), eq(20),
any(Instant.class)))
.willReturn(new SharePage(items, 2));

ListSharesUseCase.Result result = service.list(
Expand All @@ -185,7 +188,8 @@ void listsOwnSharesWithStatusFilterAndPagination() {

@Test
void answersAnEmptyPageInsteadOfAnError() {
given(shareRepositoryPort.findPage(USER_ID, null, 0, 20))
given(shareRepositoryPort.findPage(eq(USER_ID), isNull(), eq(0), eq(20),
any(Instant.class)))
.willReturn(new SharePage(List.of(), 0));

ListSharesUseCase.Result result = service.list(
Expand Down
Loading