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 @@ -37,6 +37,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -203,7 +204,7 @@ public FolderTrashResponse moveFolderToTrash(
.orElseThrow(() -> resolveTrashLookupException(userId, folderId));

folder.delete();
materialRepository.trashMaterialsByFolder(folderId, userId);
materialRepository.trashMaterialsByFolder(folderId, userId, folder.getDeletedAt());

return FolderTrashResponse.from(folder);
}
Expand All @@ -230,14 +231,20 @@ public FolderRestoreResponse restoreFolder(
throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME);
}

long materialRestoreCount = materialRepository.countDeletedByFolderIdAndUserId(folderId, userId);
// 폴더 자신의 deleted_at을 복구(NULL로 되돌리기) 전에 미리 스냅샷으로 떼어둔다 —
// restoreFolderOrThrowDuplicateName 이후에는 이 값을 더 이상 조회할 수 없다.
LocalDateTime folderDeletedAt = folderRepository.findTrashedByIdAndUserId(folderId, userId)
.orElseThrow(() -> new FolderException(FolderErrorCode.FOLDER_NOT_FOUND))
.getDeletedAt();

long materialRestoreCount = materialRepository.countDeletedByFolderIdAndUserId(folderId, userId, folderDeletedAt);
folderMaterialCapacityValidator.validateCanAdd(userId, folderId, materialRestoreCount);

int restoredCount = restoreFolderOrThrowDuplicateName(folderId, userId);
if (restoredCount == 0) {
throw new FolderException(FolderErrorCode.FOLDER_NOT_FOUND);
}
materialRepository.restoreTrashedMaterialsByFolder(folderId, userId);
materialRepository.restoreTrashedMaterialsByFolder(folderId, userId, folderDeletedAt);

return FolderRestoreResponse.of(folderId, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

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

Expand Down Expand Up @@ -77,23 +78,28 @@ int restoreDeletedMaterials(
@Param("userId") Long userId
);

/** 자료 자신이 휴지통 상태이면서, 상위 폴더는 휴지통이 아닌(또는 폴더가 없는) 것만 노출한다.
* 폴더 삭제로 함께 휴지통 처리된 자료는 폴더 휴지통 목록에만 나타나야 하므로 여기서 제외한다. */
/**
* 자료 자신이 휴지통 상태이면서, "폴더와 함께" 휴지통에 들어간 게 아닌 것만 노출한다
* (그런 자료는 폴더 휴지통 상세 목록에만 나타나야 하므로 여기서 제외한다).
* 폴더가 없거나, 폴더가 활성 상태이거나, 폴더는 나중에 삭제됐지만 이 자료는 그 전에
* 이미 개별적으로 삭제됐던 경우(m.deleted_at < f.deleted_at)는 폴더 삭제와 무관한
* 독립적인 휴지통 항목이므로 그대로 노출한다.
*/
@Query(
value = """
SELECT m.* FROM materials m
LEFT JOIN folders f ON f.id = m.folder_id
WHERE m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND (m.folder_id IS NULL OR f.deleted_at IS NULL)
AND (m.folder_id IS NULL OR f.deleted_at IS NULL OR m.deleted_at < f.deleted_at)
ORDER BY m.deleted_at DESC, m.id DESC
""",
countQuery = """
SELECT COUNT(*) FROM materials m
LEFT JOIN folders f ON f.id = m.folder_id
WHERE m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND (m.folder_id IS NULL OR f.deleted_at IS NULL)
AND (m.folder_id IS NULL OR f.deleted_at IS NULL OR m.deleted_at < f.deleted_at)
""",
nativeQuery = true
)
Expand All @@ -105,15 +111,15 @@ SELECT COUNT(*) FROM materials m
LEFT JOIN folders f ON f.id = m.folder_id
WHERE m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND (m.folder_id IS NULL OR f.deleted_at IS NULL)
AND (m.folder_id IS NULL OR f.deleted_at IS NULL OR m.deleted_at < f.deleted_at)
ORDER BY m.deleted_at ASC, m.id ASC
""",
countQuery = """
SELECT COUNT(*) FROM materials m
LEFT JOIN folders f ON f.id = m.folder_id
WHERE m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND (m.folder_id IS NULL OR f.deleted_at IS NULL)
AND (m.folder_id IS NULL OR f.deleted_at IS NULL OR m.deleted_at < f.deleted_at)
""",
nativeQuery = true
)
Expand Down Expand Up @@ -204,13 +210,19 @@ Page<Material> searchFolderMaterials(

List<Material> findAllByFolder_Id(Long folderId);

/** 폴더 삭제 시 그 폴더에 속한(아직 활성 상태인) 자료를 전부 휴지통 상태로 전환한다. */
/**
* 폴더 삭제 시 그 폴더에 속한(아직 활성 상태인) 자료를 전부 휴지통 상태로 전환한다.
* deletedAt은 DB의 CURRENT_TIMESTAMP가 아니라 폴더 자신의 삭제 시각(folder.getDeletedAt())을
* 그대로 넘겨받아 써야 한다 — 그래야 폴더와 "같이" 삭제된 자료만 폴더 복구 시 함께 복구되고,
* 폴더 삭제 이전에 이미 개별적으로 휴지통에 있던 자료는 (deleted_at < 폴더 deletedAt이므로)
* restoreTrashedMaterialsByFolder / countDeletedByFolderIdAndUserId 에서 자동으로 제외된다.
*/
@Modifying
@Query(
value = """
UPDATE materials
SET deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
SET deleted_at = :deletedAt,
updated_at = :deletedAt
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NULL
Expand All @@ -219,10 +231,22 @@ Page<Material> searchFolderMaterials(
)
int trashMaterialsByFolder(
@Param("folderId") Long folderId,
@Param("userId") Long userId
@Param("userId") Long userId,
@Param("deletedAt") LocalDateTime deletedAt
);

/** 폴더 복구 시 그 폴더 안에서 함께 휴지통으로 이동했던 자료를 폴더와 함께 전부 복구한다. */
/**
* 폴더 복구 시 그 폴더 안에서 함께 휴지통으로 이동했던 자료를 폴더와 함께 복구한다.
* folderDeletedAt(폴더가 삭제됐던 시각)보다 먼저 삭제된(=폴더와 무관하게 개별적으로 먼저
* 휴지통에 있던) 자료는 제외한다 — 그 자료들은 폴더 삭제와 상관없이 사용자가 별도로 지운
* 것이라, 폴더를 복구한다고 같이 되살아나면 안 된다.
*
* folders 테이블을 라이브로 조인하지 않고 folderDeletedAt을 호출자가 직접 넘기는 이유:
* 호출하는 쪽(FolderService.restoreFolder, TrashService.restoreFolders)에서 이 메서드보다
* "폴더 자신의 deleted_at을 NULL로 되돌리는 복구"가 먼저 실행되면, 라이브 조인으로는
* f.deleted_at이 이미 NULL이라 어떤 자료도 매칭되지 않는다 — 호출 순서에 안전하도록
* 폴더가 삭제됐던 시각의 스냅샷을 호출 전에 미리 받아서 파라미터로 고정한다.
*/
@Modifying
@Query(
value = """
Expand All @@ -232,27 +256,32 @@ int trashMaterialsByFolder(
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
AND deleted_at >= :folderDeletedAt
""",
nativeQuery = true
)
int restoreTrashedMaterialsByFolder(
@Param("folderId") Long folderId,
@Param("userId") Long userId
@Param("userId") Long userId,
@Param("folderDeletedAt") LocalDateTime folderDeletedAt
);

/** restoreTrashedMaterialsByFolder와 동일한 기준(폴더와 함께 삭제된 자료만)으로 복구 대상 개수를 센다. */
@Query(
value = """
SELECT COUNT(*)
FROM materials
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
AND deleted_at >= :folderDeletedAt
""",
nativeQuery = true
)
long countDeletedByFolderIdAndUserId(
@Param("folderId") Long folderId,
@Param("userId") Long userId
@Param("userId") Long userId,
@Param("folderDeletedAt") LocalDateTime folderDeletedAt
);

@Query(
Expand Down Expand Up @@ -287,15 +316,22 @@ List<FolderMaterialRestoreCountProjection> countDeletedByFolderIdForRestore(
@Param("userId") Long userId
);

/** 휴지통 폴더 목록 조회용: 폴더와 함께 삭제된(휴지통으로 같이 이동한) 자료 개수를 폴더별로 집계한다. */
/**
* 휴지통 폴더 목록 조회용: 폴더와 "함께" 삭제된(휴지통으로 같이 이동한) 자료 개수를 폴더별로 집계한다.
* findTrashedByFolderIdOrderByDeletedAtDesc/Asc, restoreTrashedMaterialsByFolder 와 동일한 기준
* (m.deleted_at >= f.deleted_at)을 써야, 폴더 삭제 이전에 개별적으로 먼저 삭제됐던 자료가
* 이 개수에 섞여 들어가지 않는다.
*/
@Query(
value = """
SELECT folder_id AS folderId, COUNT(*) AS materialCount
FROM materials
WHERE folder_id IN (:folderIds)
AND user_id = :userId
AND deleted_at IS NOT NULL
GROUP BY folder_id
SELECT m.folder_id AS folderId, COUNT(*) AS materialCount
FROM materials m
JOIN folders f ON f.id = m.folder_id
WHERE m.folder_id IN (:folderIds)
AND m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND m.deleted_at >= f.deleted_at
GROUP BY m.folder_id
""",
nativeQuery = true
)
Expand All @@ -304,20 +340,30 @@ List<FolderMaterialRestoreCountProjection> countDeletedByFolderIdsAndUserId(
@Param("userId") Long userId
);

/** 휴지통에 있는 폴더 상세 조회용: 그 폴더 안에 있던(폴더와 함께 휴지통으로 이동한) 자료를 최신순으로 조회한다. */
/**
* 휴지통에 있는 폴더 상세 조회용: 그 폴더 안에 있던(폴더와 "함께" 휴지통으로 이동한) 자료를
* 최신순으로 조회한다. 폴더 삭제 이전에 이미 개별적으로 삭제됐던 자료(m.deleted_at < f.deleted_at)는
* 이 폴더와 무관한 독립적인 휴지통 항목이므로 제외한다 — countDeletedByFolderIdAndUserId /
* restoreTrashedMaterialsByFolder 와 동일한 기준을 사용해야 폴더 카드의 materialCount와
* 상세 목록에 보이는 개수가 일치한다.
*/
@Query(
value = """
SELECT * FROM materials
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
ORDER BY deleted_at DESC, id DESC
SELECT m.* FROM materials m
JOIN folders f ON f.id = m.folder_id
WHERE m.folder_id = :folderId
AND m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND m.deleted_at >= f.deleted_at
ORDER BY m.deleted_at DESC, m.id DESC
""",
countQuery = """
SELECT COUNT(*) FROM materials
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
SELECT COUNT(*) FROM materials m
JOIN folders f ON f.id = m.folder_id
WHERE m.folder_id = :folderId
AND m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND m.deleted_at >= f.deleted_at
""",
nativeQuery = true
)
Expand All @@ -329,17 +375,21 @@ Page<Material> findTrashedByFolderIdOrderByDeletedAtDesc(

@Query(
value = """
SELECT * FROM materials
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
ORDER BY deleted_at ASC, id ASC
SELECT m.* FROM materials m
JOIN folders f ON f.id = m.folder_id
WHERE m.folder_id = :folderId
AND m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND m.deleted_at >= f.deleted_at
ORDER BY m.deleted_at ASC, m.id ASC
""",
countQuery = """
SELECT COUNT(*) FROM materials
WHERE folder_id = :folderId
AND user_id = :userId
AND deleted_at IS NOT NULL
SELECT COUNT(*) FROM materials m
JOIN folders f ON f.id = m.folder_id
WHERE m.folder_id = :folderId
AND m.user_id = :userId
AND m.deleted_at IS NOT NULL
AND m.deleted_at >= f.deleted_at
""",
nativeQuery = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -243,11 +245,20 @@ public FolderTrashRestoreResponse restoreFolders(Long userId, FolderIdsRequest r

List<Long> restoredIds = new ArrayList<>();
for (Long folderId : requestedIds) {
// restoreTrashedFolderIfNameAvailable이 폴더의 deleted_at을 NULL로 되돌리므로,
// "폴더와 함께 삭제된 자료"를 가려낼 기준값(폴더가 삭제됐던 시각)은 그 전에 미리 떼어둔다.
Optional<LocalDateTime> folderDeletedAt = folderRepository.findTrashedByIdAndUserId(folderId, userId)
.map(Folder::getDeletedAt);
if (folderDeletedAt.isEmpty()) {
continue;
}

if (folderRepository.restoreTrashedFolderIfNameAvailable(folderId, userId) > 0) {
long materialRestoreCount = materialRepository.countDeletedByFolderIdAndUserId(folderId, userId);
long materialRestoreCount = materialRepository.countDeletedByFolderIdAndUserId(
folderId, userId, folderDeletedAt.get());
folderMaterialCapacityValidator.validateCanAdd(userId, folderId, materialRestoreCount);
restoredIds.add(folderId);
materialRepository.restoreTrashedMaterialsByFolder(folderId, userId);
materialRepository.restoreTrashedMaterialsByFolder(folderId, userId, folderDeletedAt.get());
}
}

Expand Down
Loading
Loading