diff --git a/src/main/java/com/c4cometrue/mystorage/controller/FileController.java b/src/main/java/com/c4cometrue/mystorage/controller/FileController.java index 36ca717..30ebf5b 100644 --- a/src/main/java/com/c4cometrue/mystorage/controller/FileController.java +++ b/src/main/java/com/c4cometrue/mystorage/controller/FileController.java @@ -8,15 +8,18 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import com.c4cometrue.mystorage.dto.request.FileReq; -import com.c4cometrue.mystorage.dto.request.UploadFileReq; -import com.c4cometrue.mystorage.dto.response.FileDownloadRes; -import com.c4cometrue.mystorage.dto.response.FileMetaDataRes; +import com.c4cometrue.mystorage.dto.request.file.FileReq; +import com.c4cometrue.mystorage.dto.request.file.MoveFileReq; +import com.c4cometrue.mystorage.dto.request.file.UploadFileReq; +import com.c4cometrue.mystorage.dto.response.file.FileDownloadRes; +import com.c4cometrue.mystorage.dto.response.file.FileMetaDataRes; import com.c4cometrue.mystorage.service.FileService; import jakarta.validation.Valid; @@ -34,7 +37,7 @@ public class FileController { /** * 파일 업로드 요청 * @param req (파일, 사용자 이름, 폴더 기본키) - * @return {@link com.c4cometrue.mystorage.dto.response.FileMetaDataRes} + * @return {@link FileMetaDataRes} */ @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -49,8 +52,8 @@ public FileMetaDataRes uploadFile(@Valid UploadFileReq req */ @DeleteMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteFile(@Valid FileReq req) { - fileService.deleteFile(req.fileStorageName(), req.userName(), req.folderId()); + public void deleteFile(@RequestBody @Valid FileReq req) { + fileService.deleteFile(req.fileId(), req.userName(), req.folderId()); } /** @@ -60,10 +63,20 @@ public void deleteFile(@Valid FileReq req) { */ @GetMapping public ResponseEntity downloadFile(@Valid FileReq req) { - FileDownloadRes file = fileService.downloadFile(req.fileStorageName(), req.userName(), req.folderId()); + FileDownloadRes file = fileService.downloadFile(req.fileId(), req.userName(), req.folderId()); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(file.mime())) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.fileName() + "\"") .body(file.resource()); } + + /** + * 파일 이동 요청 + * @param req (파일 기본키, 이동할 폴더 기본키) + */ + @PatchMapping + @ResponseStatus(HttpStatus.MOVED_PERMANENTLY) + public void moveFile(@RequestBody @Valid MoveFileReq req) { + fileService.moveFile(req.fileId(), req.folderId(), req.userName()); + } } diff --git a/src/main/java/com/c4cometrue/mystorage/controller/FolderController.java b/src/main/java/com/c4cometrue/mystorage/controller/FolderController.java index c3f9821..5cc17a4 100644 --- a/src/main/java/com/c4cometrue/mystorage/controller/FolderController.java +++ b/src/main/java/com/c4cometrue/mystorage/controller/FolderController.java @@ -1,7 +1,10 @@ package com.c4cometrue.mystorage.controller; +import java.util.List; + import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -10,11 +13,16 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import com.c4cometrue.mystorage.dto.request.CreateFolderReq; -import com.c4cometrue.mystorage.dto.request.GetFolderReq; -import com.c4cometrue.mystorage.dto.request.UpdateFolderNameReq; -import com.c4cometrue.mystorage.dto.response.CreateFolderRes; -import com.c4cometrue.mystorage.dto.response.FolderOverviewRes; +import com.c4cometrue.mystorage.dto.request.folder.CreateFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.DeleteFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.GetFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.GetSubInfoReq; +import com.c4cometrue.mystorage.dto.request.folder.MoveFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.UpdateFolderNameReq; +import com.c4cometrue.mystorage.dto.response.file.FileMetaDataRes; +import com.c4cometrue.mystorage.dto.response.folder.CreateFolderRes; +import com.c4cometrue.mystorage.dto.response.folder.FolderMetaDataRes; +import com.c4cometrue.mystorage.dto.response.folder.FolderOverviewRes; import com.c4cometrue.mystorage.service.FolderService; import jakarta.validation.Valid; @@ -31,18 +39,40 @@ public class FolderController { /** * 폴더의 개략적인 정보 요청 * @param req (폴더 기본키, 폴더 이름, 사용자 이름, 부모 폴더 기본키) - * @return {@link com.c4cometrue.mystorage.dto.response.FolderOverviewRes} + * @return {@link FolderOverviewRes} */ @GetMapping @ResponseStatus(HttpStatus.OK) public FolderOverviewRes getFolderData(@Valid GetFolderReq req) { - return folderService.getFolderData(req.folderId(), req.userName()); + return folderService.getFolderTotalInfo(req.folderId(), req.userName()); + } + + /** + * 폴더의 하위 폴더들을 페이징으로 조회 + * @param req (폴더 기본키, 페이지 번호) + * @return page 번호에 맞는 하위 폴더 목록 + */ + @GetMapping("/subFolder") + @ResponseStatus(HttpStatus.OK) + public List getSubFolders(@Valid GetSubInfoReq req) { + return folderService.getFolders(req.folderId(), req.page()); + } + + /** + * 폴더의 하위 파일들을 페이징으로 조회 + * @param req (폴더 기본키, 페이지 번호) + * @return page 번호에 맞는 하위 파일 목록 + */ + @GetMapping("/subFile") + @ResponseStatus(HttpStatus.OK) + public List getSubFiles(@Valid GetSubInfoReq req) { + return folderService.getFiles(req.folderId(), req.page()); } /** * 폴더 생성 요청이 성공하면 해당 폴더 pk를 포함한 정보 반환 - * @param req (폴더 이름, 사용자 이름, 부모 폴더 기본키) - * @return {@link com.c4cometrue.mystorage.dto.response.CreateFolderRes} + * @param req (폴더 기본키, 사용자 이름, 부모 폴더 기본키) + * @return {@link CreateFolderRes} */ @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -51,13 +81,33 @@ public CreateFolderRes createFolder(@RequestBody @Valid CreateFolderReq req) { } /** - * 폴더 이름 수정 요청 - * @param updateFolderNameReq (이전 폴더 이름, 사용자 이름, 새로운 폴더 이름, 부모 폴더 기본키) + * 폴더 이름을 수정하는 요청 + * @param req (폴더 기본키, 부모 폴더 기본키, 사용자 이름, 새로운 폴더 이름) */ @PatchMapping("/name") @ResponseStatus(HttpStatus.OK) - public void updateFolderName(@RequestBody @Valid UpdateFolderNameReq updateFolderNameReq) { - folderService.updateFolderName(updateFolderNameReq.folderId(), updateFolderNameReq.parentFolderId(), - updateFolderNameReq.userName(), updateFolderNameReq.newFolderName()); + public void updateFolderName(@RequestBody @Valid UpdateFolderNameReq req) { + folderService.updateFolderName(req.folderId(), req.parentFolderId(), + req.userName(), req.newFolderName()); + } + + /** + * 폴더를 삭제하는 요청 + * @param req (폴더 기본키) + */ + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteFolder(@RequestBody @Valid DeleteFolderReq req) { + folderService.deleteFolder(req.folderId(), req.userName()); + } + + /** + * 폴더를 특정 폴더 위치로 이동하는 요청 + * @param req (폴더 기본키, 이동할 폴더 기본키, 사용자 이름) + */ + @PatchMapping + @ResponseStatus(HttpStatus.OK) + public void moveFolder(@RequestBody @Valid MoveFolderReq req) { + folderService.moveFolder(req.folderId(), req.targetFolderId(), req.userName()); } } diff --git a/src/main/java/com/c4cometrue/mystorage/controller/UserController.java b/src/main/java/com/c4cometrue/mystorage/controller/UserController.java index ebc6e82..de65f8d 100644 --- a/src/main/java/com/c4cometrue/mystorage/controller/UserController.java +++ b/src/main/java/com/c4cometrue/mystorage/controller/UserController.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import com.c4cometrue.mystorage.dto.request.SignUpReq; +import com.c4cometrue.mystorage.dto.request.file.SignUpReq; import com.c4cometrue.mystorage.dto.response.SignUpRes; import com.c4cometrue.mystorage.service.UserService; diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/FileReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/FileReq.java deleted file mode 100644 index f55abd0..0000000 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/FileReq.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.c4cometrue.mystorage.dto.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -/** - * @see com.c4cometrue.mystorage.entity.FileMetaData - */ -public record FileReq( - @NotBlank(message = "file storage name is blank") String fileStorageName, - @NotBlank(message = "user name is blank") String userName, - @NotNull(message = "folder id is blank") long folderId -) { -} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/file/FileReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/file/FileReq.java new file mode 100644 index 0000000..ce7a5dd --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/file/FileReq.java @@ -0,0 +1,14 @@ +package com.c4cometrue.mystorage.dto.request.file; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +/** + * @see com.c4cometrue.mystorage.entity.FileMetaData + */ +public record FileReq( + @Positive(message = "file id should be positive") long fileId, + @NotBlank(message = "user name is blank") String userName, + @Positive(message = "folder id should be positive") long folderId +) { +} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/file/MoveFileReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/file/MoveFileReq.java new file mode 100644 index 0000000..73c8be8 --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/file/MoveFileReq.java @@ -0,0 +1,14 @@ +package com.c4cometrue.mystorage.dto.request.file; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +/** + * DTO for {@link com.c4cometrue.mystorage.entity.FileMetaData} + */ +public record MoveFileReq( + @Positive(message = "file id should be positive") long fileId, + @Positive(message = "folder id should be positive") long folderId, + @NotBlank(message = "user name is blank") String userName +) { +} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/SignUpReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/file/SignUpReq.java similarity index 79% rename from src/main/java/com/c4cometrue/mystorage/dto/request/SignUpReq.java rename to src/main/java/com/c4cometrue/mystorage/dto/request/file/SignUpReq.java index 0fd68c2..c17e68b 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/SignUpReq.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/file/SignUpReq.java @@ -1,4 +1,4 @@ -package com.c4cometrue.mystorage.dto.request; +package com.c4cometrue.mystorage.dto.request.file; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/UploadFileReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/file/UploadFileReq.java similarity index 65% rename from src/main/java/com/c4cometrue/mystorage/dto/request/UploadFileReq.java rename to src/main/java/com/c4cometrue/mystorage/dto/request/file/UploadFileReq.java index dbeb15d..7372865 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/UploadFileReq.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/file/UploadFileReq.java @@ -1,13 +1,14 @@ -package com.c4cometrue.mystorage.dto.request; +package com.c4cometrue.mystorage.dto.request.file; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; public record UploadFileReq( @NotNull(message = "file doesn't exist") MultipartFile file, @NotBlank(message = "user name is blank") String userName, - @NotNull(message = "folder id is null") long folderId + @Positive(message = "folder id should be positive") long folderId ) { } diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/CreateFolderReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/CreateFolderReq.java similarity index 60% rename from src/main/java/com/c4cometrue/mystorage/dto/request/CreateFolderReq.java rename to src/main/java/com/c4cometrue/mystorage/dto/request/folder/CreateFolderReq.java index 1b4f88d..6a714da 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/CreateFolderReq.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/CreateFolderReq.java @@ -1,13 +1,13 @@ -package com.c4cometrue.mystorage.dto.request; +package com.c4cometrue.mystorage.dto.request.folder; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; /** * @see com.c4cometrue.mystorage.entity.FolderMetaData */ public record CreateFolderReq( - @NotNull(message = "parent folder can't be null") long parentFolderId, + @Positive(message = "parent folder should be positive") long parentFolderId, @NotBlank(message = "user name is blank") String userName, @NotBlank(message = "folder name is blank") String folderName ) { diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/folder/DeleteFolderReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/DeleteFolderReq.java new file mode 100644 index 0000000..a8f023b --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/DeleteFolderReq.java @@ -0,0 +1,12 @@ +package com.c4cometrue.mystorage.dto.request.folder; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +/** + * @see com.c4cometrue.mystorage.entity.FolderMetaData + */ +public record DeleteFolderReq( + @Positive(message = "folder id should be positive") long folderId, + @NotBlank(message = "user name is blank") String userName) { +} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/GetFolderReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetFolderReq.java similarity index 54% rename from src/main/java/com/c4cometrue/mystorage/dto/request/GetFolderReq.java rename to src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetFolderReq.java index d8c9664..415a3d0 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/GetFolderReq.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetFolderReq.java @@ -1,13 +1,13 @@ -package com.c4cometrue.mystorage.dto.request; +package com.c4cometrue.mystorage.dto.request.folder; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; /** * @see com.c4cometrue.mystorage.entity.FolderMetaData */ public record GetFolderReq( - @NotNull long folderId, + @Positive(message = "folder id should be positive") long folderId, @NotBlank(message = "user name is blank") String userName ) { } diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetSubInfoReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetSubInfoReq.java new file mode 100644 index 0000000..eec6194 --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/GetSubInfoReq.java @@ -0,0 +1,10 @@ +package com.c4cometrue.mystorage.dto.request.folder; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; + +public record GetSubInfoReq( + @Positive(message = "folder id should be positive") long folderId, + @Min(value = 0, message = "page number should be 0 or positive") int page +) { +} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/folder/MoveFolderReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/MoveFolderReq.java new file mode 100644 index 0000000..7698b27 --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/MoveFolderReq.java @@ -0,0 +1,14 @@ +package com.c4cometrue.mystorage.dto.request.folder; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +/** + * @see com.c4cometrue.mystorage.entity.FolderMetaData + */ +public record MoveFolderReq( + @Positive(message = "folder id should be positive") long folderId, + @Positive(message = "target folder should be positive") long targetFolderId, + @NotBlank(message = "user name is blank") String userName +) { +} diff --git a/src/main/java/com/c4cometrue/mystorage/dto/request/UpdateFolderNameReq.java b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/UpdateFolderNameReq.java similarity index 53% rename from src/main/java/com/c4cometrue/mystorage/dto/request/UpdateFolderNameReq.java rename to src/main/java/com/c4cometrue/mystorage/dto/request/folder/UpdateFolderNameReq.java index 3a6296f..0cbb63e 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/request/UpdateFolderNameReq.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/request/folder/UpdateFolderNameReq.java @@ -1,14 +1,14 @@ -package com.c4cometrue.mystorage.dto.request; +package com.c4cometrue.mystorage.dto.request.folder; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; /** * @see com.c4cometrue.mystorage.entity.FolderMetaData */ public record UpdateFolderNameReq( - @NotNull long folderId, - @NotNull long parentFolderId, + @Positive(message = "folder id should be positive") long folderId, + @Positive(message = "parent folder id should be positive") long parentFolderId, @NotBlank(message = "user name is blank") String userName, @NotBlank(message = "new folder name is blank") String newFolderName ) { diff --git a/src/main/java/com/c4cometrue/mystorage/dto/response/FileDownloadRes.java b/src/main/java/com/c4cometrue/mystorage/dto/response/file/FileDownloadRes.java similarity index 87% rename from src/main/java/com/c4cometrue/mystorage/dto/response/FileDownloadRes.java rename to src/main/java/com/c4cometrue/mystorage/dto/response/file/FileDownloadRes.java index 4c46f77..8d442e2 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/response/FileDownloadRes.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/response/file/FileDownloadRes.java @@ -1,4 +1,4 @@ -package com.c4cometrue.mystorage.dto.response; +package com.c4cometrue.mystorage.dto.response.file; import org.springframework.core.io.Resource; diff --git a/src/main/java/com/c4cometrue/mystorage/dto/response/FileMetaDataRes.java b/src/main/java/com/c4cometrue/mystorage/dto/response/file/FileMetaDataRes.java similarity index 93% rename from src/main/java/com/c4cometrue/mystorage/dto/response/FileMetaDataRes.java rename to src/main/java/com/c4cometrue/mystorage/dto/response/file/FileMetaDataRes.java index fbefdea..fa9570f 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/response/FileMetaDataRes.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/response/file/FileMetaDataRes.java @@ -1,4 +1,4 @@ -package com.c4cometrue.mystorage.dto.response; +package com.c4cometrue.mystorage.dto.response.file; import com.c4cometrue.mystorage.entity.FileMetaData; diff --git a/src/main/java/com/c4cometrue/mystorage/dto/response/CreateFolderRes.java b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/CreateFolderRes.java similarity index 60% rename from src/main/java/com/c4cometrue/mystorage/dto/response/CreateFolderRes.java rename to src/main/java/com/c4cometrue/mystorage/dto/response/folder/CreateFolderRes.java index c54819c..b129efb 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/response/CreateFolderRes.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/CreateFolderRes.java @@ -1,4 +1,4 @@ -package com.c4cometrue.mystorage.dto.response; +package com.c4cometrue.mystorage.dto.response.folder; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -8,7 +8,6 @@ */ public record CreateFolderRes( @NotNull(message = "folder Number can't be null") long folderId, - @NotBlank(message = "folder name is blank") String folderName, - @NotBlank(message = "user name is blank") String userName + @NotBlank(message = "folder name is blank") String folderName ) { } diff --git a/src/main/java/com/c4cometrue/mystorage/dto/response/FolderMetaDataRes.java b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderMetaDataRes.java similarity index 87% rename from src/main/java/com/c4cometrue/mystorage/dto/response/FolderMetaDataRes.java rename to src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderMetaDataRes.java index 5edc077..4c7647f 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/response/FolderMetaDataRes.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderMetaDataRes.java @@ -1,4 +1,4 @@ -package com.c4cometrue.mystorage.dto.response; +package com.c4cometrue.mystorage.dto.response.folder; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/c4cometrue/mystorage/dto/response/FolderOverviewRes.java b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderOverviewRes.java similarity index 85% rename from src/main/java/com/c4cometrue/mystorage/dto/response/FolderOverviewRes.java rename to src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderOverviewRes.java index 9930d47..0ed773f 100644 --- a/src/main/java/com/c4cometrue/mystorage/dto/response/FolderOverviewRes.java +++ b/src/main/java/com/c4cometrue/mystorage/dto/response/folder/FolderOverviewRes.java @@ -1,7 +1,9 @@ -package com.c4cometrue.mystorage.dto.response; +package com.c4cometrue.mystorage.dto.response.folder; import java.util.List; +import com.c4cometrue.mystorage.dto.response.file.FileMetaDataRes; + import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/c4cometrue/mystorage/entity/DeleteLog.java b/src/main/java/com/c4cometrue/mystorage/entity/DeleteLog.java new file mode 100644 index 0000000..29079fe --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/entity/DeleteLog.java @@ -0,0 +1,31 @@ +package com.c4cometrue.mystorage.entity; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; + +@Entity +@RequiredArgsConstructor +public class DeleteLog { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long logId; + + @NotBlank(message = "file storage name is blank") + private String fileStorageName; + + @NotNull + private ZonedDateTime deleteTime; + + public DeleteLog(String fileStorageName) { + this.fileStorageName = fileStorageName; + this.deleteTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")); + } +} diff --git a/src/main/java/com/c4cometrue/mystorage/entity/FileMetaData.java b/src/main/java/com/c4cometrue/mystorage/entity/FileMetaData.java index a70eee0..632dc90 100644 --- a/src/main/java/com/c4cometrue/mystorage/entity/FileMetaData.java +++ b/src/main/java/com/c4cometrue/mystorage/entity/FileMetaData.java @@ -53,4 +53,7 @@ public FileMetaData(String fileName, String fileStorageName, long size, String m this.folderId = folderId; } + public void setFolderId(Long folderId) { + this.folderId = folderId; + } } diff --git a/src/main/java/com/c4cometrue/mystorage/entity/FolderMetaData.java b/src/main/java/com/c4cometrue/mystorage/entity/FolderMetaData.java index 5cfda6a..7d8db59 100644 --- a/src/main/java/com/c4cometrue/mystorage/entity/FolderMetaData.java +++ b/src/main/java/com/c4cometrue/mystorage/entity/FolderMetaData.java @@ -42,4 +42,8 @@ public FolderMetaData(String folderName, String userName, long parentFolderId) { public void setFolderName(String folderName) { this.folderName = folderName; } + + public void setParentFolderId(Long parentFolderId) { + this.parentFolderId = parentFolderId; + } } diff --git a/src/main/java/com/c4cometrue/mystorage/entity/UserData.java b/src/main/java/com/c4cometrue/mystorage/entity/UserData.java index e26e939..cf38311 100644 --- a/src/main/java/com/c4cometrue/mystorage/entity/UserData.java +++ b/src/main/java/com/c4cometrue/mystorage/entity/UserData.java @@ -13,7 +13,8 @@ @RequiredArgsConstructor @Getter public class UserData { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; @NotBlank(message = "user name is blank") diff --git a/src/main/java/com/c4cometrue/mystorage/exception/ErrorCd.java b/src/main/java/com/c4cometrue/mystorage/exception/ErrorCd.java index 431cdf9..20abb2c 100644 --- a/src/main/java/com/c4cometrue/mystorage/exception/ErrorCd.java +++ b/src/main/java/com/c4cometrue/mystorage/exception/ErrorCd.java @@ -17,7 +17,7 @@ public enum ErrorCd { // Folder 관련 에러 FOLDER_NOT_EXIST(HttpStatus.NOT_FOUND, "Folder doesn't exist"), // 폴더가 존재하지 않는 경우 DUPLICATE_FOLDER(HttpStatus.BAD_REQUEST, "Duplicate Folder"), // 이미 존재하는 폴더명 - + FOLDER_CANT_BE_MOVED(HttpStatus.BAD_REQUEST, "folder route might have cycle"), // 하위 폴더로 이동하려는 경우 // User 관련 에러 DUPLICATE_USER(HttpStatus.BAD_REQUEST, "User name Duplicate"), // 이미 존재하는 유저 이름 diff --git a/src/main/java/com/c4cometrue/mystorage/exception/ExceptionHandlingController.java b/src/main/java/com/c4cometrue/mystorage/exception/ExceptionHandlingController.java index 1e7eb89..0380520 100644 --- a/src/main/java/com/c4cometrue/mystorage/exception/ExceptionHandlingController.java +++ b/src/main/java/com/c4cometrue/mystorage/exception/ExceptionHandlingController.java @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -16,7 +17,7 @@ public class ExceptionHandlingController { private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass().getSimpleName()); final ZoneId timeZone = ZoneId.of("Asia/Seoul"); - // value에 포함된 예외들 처리함 + @ExceptionHandler(ServiceException.class) public ResponseEntity handleServiceException(ServiceException exception) { if (exception.getDebugMessage() != null) { @@ -32,6 +33,28 @@ public ResponseEntity handleServiceException(ServiceException e return new ResponseEntity<>(apiExceptionRes, httpStatus); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidException(MethodArgumentNotValidException exception) { + StringBuilder errorMessageBuilder = new StringBuilder(); + exception.getBindingResult().getFieldErrors().forEach(error -> { + String fieldName = error.getField(); + String errorMessage = error.getDefaultMessage(); + errorMessageBuilder.append(fieldName).append(" : ").append(errorMessage).append("\n"); + }); + + if (!errorMessageBuilder.isEmpty()) { + String errorMessages = errorMessageBuilder.toString(); + logger.error(errorMessages); + } + + var apiExceptionRes = new ApiExceptionRes( + "Request is not Valid. Please Check Again", + ZonedDateTime.now(timeZone) + ); + + return new ResponseEntity<>(apiExceptionRes, HttpStatus.BAD_REQUEST); + } + @ExceptionHandler() public ResponseEntity handleException(Exception exception) { logger.error(exception.getMessage()); diff --git a/src/main/java/com/c4cometrue/mystorage/repository/DeleteLogRepository.java b/src/main/java/com/c4cometrue/mystorage/repository/DeleteLogRepository.java new file mode 100644 index 0000000..17ed40f --- /dev/null +++ b/src/main/java/com/c4cometrue/mystorage/repository/DeleteLogRepository.java @@ -0,0 +1,8 @@ +package com.c4cometrue.mystorage.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.c4cometrue.mystorage.entity.DeleteLog; + +public interface DeleteLogRepository extends JpaRepository { +} diff --git a/src/main/java/com/c4cometrue/mystorage/repository/FileRepository.java b/src/main/java/com/c4cometrue/mystorage/repository/FileRepository.java index a9cd8cd..3736e1b 100644 --- a/src/main/java/com/c4cometrue/mystorage/repository/FileRepository.java +++ b/src/main/java/com/c4cometrue/mystorage/repository/FileRepository.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.c4cometrue.mystorage.entity.FileMetaData; @@ -10,7 +12,7 @@ public interface FileRepository extends JpaRepository { Optional findByFolderIdAndUserNameAndFileName(long folderId, String username, String filename); - Optional findByFileStorageName(String fileStorageName); - Optional> findAllByFolderId(long folderId); + + Page findAllByFolderId(long folderId, Pageable pageable); } diff --git a/src/main/java/com/c4cometrue/mystorage/repository/FolderRepository.java b/src/main/java/com/c4cometrue/mystorage/repository/FolderRepository.java index 49a5397..9fdb23c 100644 --- a/src/main/java/com/c4cometrue/mystorage/repository/FolderRepository.java +++ b/src/main/java/com/c4cometrue/mystorage/repository/FolderRepository.java @@ -3,15 +3,23 @@ import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import com.c4cometrue.mystorage.entity.FolderMetaData; public interface FolderRepository extends JpaRepository { Optional findByFolderId(long folderId); + @Query("SELECT f.parentFolderId FROM FolderMetaData f where f.folderId=:folderId") + Long findParentFolderIdByFolderId(long folderId); + Optional findByFolderNameAndParentFolderIdAndUserName(String folderName, long parentFolderId, String userName); Optional> findAllByParentFolderId(long parentFolderId); + + Page findAllByParentFolderId(long parentFolderId, Pageable pageable); } diff --git a/src/main/java/com/c4cometrue/mystorage/service/FileService.java b/src/main/java/com/c4cometrue/mystorage/service/FileService.java index b20da4a..c408fcc 100644 --- a/src/main/java/com/c4cometrue/mystorage/service/FileService.java +++ b/src/main/java/com/c4cometrue/mystorage/service/FileService.java @@ -8,10 +8,13 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import com.c4cometrue.mystorage.dto.response.FileDownloadRes; -import com.c4cometrue.mystorage.dto.response.FileMetaDataRes; +import com.c4cometrue.mystorage.dto.response.file.FileDownloadRes; +import com.c4cometrue.mystorage.dto.response.file.FileMetaDataRes; +import com.c4cometrue.mystorage.entity.DeleteLog; import com.c4cometrue.mystorage.entity.FileMetaData; +import com.c4cometrue.mystorage.entity.FolderMetaData; import com.c4cometrue.mystorage.exception.ErrorCd; +import com.c4cometrue.mystorage.repository.DeleteLogRepository; import com.c4cometrue.mystorage.repository.FileRepository; import com.c4cometrue.mystorage.repository.FolderRepository; import com.c4cometrue.mystorage.util.FileUtil; @@ -22,10 +25,11 @@ @Service @RequiredArgsConstructor public class FileService { - private final FileRepository fileRepository; private final ResourceLoader resourceLoader; - private final FolderRepository folderRepository; private final StoragePathService storagePathService; + private final FolderRepository folderRepository; + private final FileRepository fileRepository; + private final DeleteLogRepository deleteLogRepository; /** * 파일 업로드 @@ -36,8 +40,8 @@ public class FileService { */ @Transactional public FileMetaDataRes uploadFile(MultipartFile file, String userName, long folderId) { - // 폴더 확인 - checkFolder(folderId); + // 폴더 유무 및 권한 확인 + checkFolder(folderId, userName); // 특정 사용자의 동일한 파일명 중복 처리 checkDuplicate(folderId, userName, file.getOriginalFilename()); // 파일 저장소 이름 @@ -62,48 +66,32 @@ public FileMetaDataRes uploadFile(MultipartFile file, String userName, long fold } /** - * 파일 삭제 - * @param fileStorageName 파일 저장소 이름 + * 폴더가 DB에 존재하는지 확인하고, 소유자를 확인한다. + * @param folderId 폴더 기본키 * @param userName 사용자 이름 - * @param folderId 파일이 존재하는 폴더 기본키 */ - @Transactional - public void deleteFile(String fileStorageName, String userName, long folderId) { - // 폴더 존재 확인 - checkFolder(folderId); - // 파일 데이터 조회 - FileMetaData fileMetaData = getFileMetaData(fileStorageName, userName); - // 파일 DB 정보 삭제 - fileRepository.delete(fileMetaData); - // 파일 물리적 삭제 - Path filePath = getFilePath(userName, fileStorageName); - FileUtil.deleteFile(filePath); + private void checkFolder(long folderId, String userName) { + FolderMetaData folderMetaData = getFolderInfo(folderId); + isOwner(folderMetaData.getUserName(), userName); } /** - * 파일 다운로드 - * @param fileStorageName 파일 저장소 이름 - * @param userName 사용자 이름 - * @param folderId 삭제할 파일이 존재하는 폴더 기본키 - * @return 파일 Resource 데이터 및 메타 데이터 + * 폴더 기본키로 특정 폴더가 존재하는지 확인한다. + * @param folderId 폴더 기본키 + * @return 해당 폴더 메타 데이터 */ - public FileDownloadRes downloadFile(String fileStorageName, String userName, long folderId) { - // 폴더 존재 확인 - checkFolder(folderId); - // 파일 메타 데이터 조회 - FileMetaData fileMetaData = getFileMetaData(fileStorageName, userName); - // 파일 물리적 경로 - Path filePath = getFilePath(userName, fileStorageName); - return new FileDownloadRes(getFileResource(filePath), fileMetaData.getFileName(), fileMetaData.getMime()); + private FolderMetaData getFolderInfo(long folderId) { + return folderRepository.findByFolderId(folderId).orElseThrow(ErrorCd.FOLDER_NOT_EXIST::serviceException); } /** - * 폴더가 DB에 존재하는지 확인한다. - * @param folderId 폴더 기본키 + * 폴더의 주인이 일치하는지 확인한다. + * @param ownerName 실제 소유자 이름 + * @param userName 요청한 사용자 이름 */ - private void checkFolder(long folderId) { - if (folderRepository.findByFolderId(folderId).isEmpty()) { - throw ErrorCd.FOLDER_NOT_EXIST.serviceException(); + private void isOwner(String ownerName, String userName) { + if (!ownerName.equals(userName)) { + throw ErrorCd.NO_PERMISSION.serviceException(); } } @@ -120,6 +108,56 @@ private void checkDuplicate(long folderId, String userName, String fileName) { } } + /** + * 파일 삭제 + * @param fileId 파일 기본키 + * @param userName 사용자 이름 + * @param folderId 파일이 존재하는 폴더 기본키 + */ + @Transactional + public void deleteFile(long fileId, String userName, long folderId) { + // 폴더 유무 및 권한 확인 + checkFolder(folderId, userName); + // 파일 메타 데이터 조회 및 권한 확인 + FileMetaData fileMetaData = getFileMetaData(fileId, userName); + // 파일 DB 정보 삭제 + fileRepository.delete(fileMetaData); + // 파일 물리적 삭제 예약 + deleteLogRepository.save(new DeleteLog(fileMetaData.getFileStorageName())); + } + + /** + * 파일이 DB에 존재하는지 확인하고 파일의 주인이 요청한 사용자명과 일치하는지 확인한다. + * @param fileId 파일 기본 키 + * @param userName 사용자 이름 + * @return 파일 메타 데이터 + */ + private FileMetaData getFileMetaData(long fileId, String userName) { + FileMetaData fileMetaData = fileRepository.findById(fileId) + .orElseThrow(() -> ErrorCd.FILE_NOT_EXIST + .serviceException("[getFileMetaData] file not exist - fileId: {}", fileId)); + isOwner(fileMetaData.getUserName(), userName); + return fileMetaData; + } + + + /** + * 파일 다운로드 + * @param fileId 파일 기본키 + * @param userName 사용자 이름 + * @param folderId 삭제할 파일이 존재하는 폴더 기본키 + * @return 파일 Resource 데이터 및 메타 데이터 + */ + public FileDownloadRes downloadFile(long fileId, String userName, long folderId) { + // 폴더 유무 및 권한 확인 + checkFolder(folderId, userName); + // 파일 메타 데이터 조회 및 권한 확인 + FileMetaData fileMetaData = getFileMetaData(fileId, userName); + // 파일 물리적 경로 + Path filePath = getFilePath(userName, fileMetaData.getFileStorageName()); + return new FileDownloadRes(getFileResource(filePath), fileMetaData.getFileName(), fileMetaData.getMime()); + } + /** * 파일이 서버에 저장될 이름을 반환한다. * @param fileOriginalName 파일의 원래 이름 @@ -139,25 +177,6 @@ private Path getFilePath(String userName, String fileStorageName) { return storagePathService.createPathByUser(userName).resolve(fileStorageName); } - /** - * 파일이 DB에 존재하는지 확인 - * @param fileStorageName 파일 저장소 이름 - * @param username 사용자 이름 - * @return 파일 메타 데이터 - */ - FileMetaData getFileMetaData(String fileStorageName, String username) { - FileMetaData fileMetaData = fileRepository.findByFileStorageName(fileStorageName) - .orElseThrow(() -> ErrorCd.FILE_NOT_EXIST - .serviceException("[getFileMetaData] file not exist - fileStorageName: {}", fileStorageName)); - - if (!fileMetaData.getUserName().equals(username)) { - throw ErrorCd.NO_PERMISSION - .serviceException("[getFileMetaData] no permission - userName: {}", username); - } - - return fileMetaData; - } - /** * 파일 자체를 반환한다. * @param filePath 파일이 존재하는 경로 @@ -172,4 +191,20 @@ private Resource getFileResource(Path filePath) { return file; } + /** + * 해당 파일의 folderId를 변경한다. 물리적으로 파일을 옮기지 않는다. + * @param fileId 해당 파일의 기본키 + * @param targetFolderId 이동할 폴더의 기본키 + */ + @Transactional + public void moveFile(long fileId, long targetFolderId, String userName) { + // 폴더 유무 및 권한 확인 + checkFolder(targetFolderId, userName); + + // 파일 메타 데이터 조회 및 권한 확인 + FileMetaData fileMetaData = getFileMetaData(fileId, userName); + + // 파일의 폴더 값 업데이트 + fileMetaData.setFolderId(targetFolderId); + } } diff --git a/src/main/java/com/c4cometrue/mystorage/service/FolderService.java b/src/main/java/com/c4cometrue/mystorage/service/FolderService.java index a4812b6..c22daa8 100644 --- a/src/main/java/com/c4cometrue/mystorage/service/FolderService.java +++ b/src/main/java/com/c4cometrue/mystorage/service/FolderService.java @@ -1,18 +1,23 @@ package com.c4cometrue.mystorage.service; +import java.util.ArrayDeque; import java.util.LinkedList; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import com.c4cometrue.mystorage.dto.response.CreateFolderRes; -import com.c4cometrue.mystorage.dto.response.FileMetaDataRes; -import com.c4cometrue.mystorage.dto.response.FolderMetaDataRes; -import com.c4cometrue.mystorage.dto.response.FolderOverviewRes; +import com.c4cometrue.mystorage.dto.response.file.FileMetaDataRes; +import com.c4cometrue.mystorage.dto.response.folder.CreateFolderRes; +import com.c4cometrue.mystorage.dto.response.folder.FolderMetaDataRes; +import com.c4cometrue.mystorage.dto.response.folder.FolderOverviewRes; +import com.c4cometrue.mystorage.entity.DeleteLog; import com.c4cometrue.mystorage.entity.FileMetaData; import com.c4cometrue.mystorage.entity.FolderMetaData; import com.c4cometrue.mystorage.exception.ErrorCd; +import com.c4cometrue.mystorage.repository.DeleteLogRepository; import com.c4cometrue.mystorage.repository.FileRepository; import com.c4cometrue.mystorage.repository.FolderRepository; @@ -24,21 +29,23 @@ public class FolderService { private final FolderRepository folderRepository; private final FileRepository fileRepository; + private final DeleteLogRepository deleteLogRepository; + private static final int PAGE_SIZE = 50; /** * 특정 폴더의 대략적인 정보 반환 * @param folderId 해당 폴더 기본키 * @param userName 사용자 이름 - * @return {@link com.c4cometrue.mystorage.dto.response.FolderOverviewRes} + * @return {@link FolderOverviewRes} */ - public FolderOverviewRes getFolderData(long folderId, String userName) { + public FolderOverviewRes getFolderTotalInfo(long folderId, String userName) { // 폴더 정보 조회 FolderMetaData folder = getFolderInfo(folderId); // 권한 확인 - checkOwner(folder.getUserName(), userName); + isOwner(folder.getUserName(), userName); return new FolderOverviewRes(folderId, folder.getFolderName(), userName, - getFiles(folderId), getFolders(folderId)); + getFiles(folderId, 0), getFolders(folderId, 0)); } /** @@ -55,7 +62,7 @@ private FolderMetaData getFolderInfo(long folderId) { * @param ownerName 실제 소유자 이름 * @param userName 요청한 사용자 이름 */ - private void checkOwner(String ownerName, String userName) { + private void isOwner(String ownerName, String userName) { if (!ownerName.equals(userName)) { throw ErrorCd.NO_PERMISSION.serviceException(); } @@ -66,7 +73,7 @@ private void checkOwner(String ownerName, String userName) { * @param parentFolderId 부모 폴더 기본키 * @param userName 사용자 이름 * @param folderName 생성할 폴더 이름 - * @return {@link com.c4cometrue.mystorage.dto.response.CreateFolderRes} + * @return {@link CreateFolderRes} */ public CreateFolderRes createFolder(long parentFolderId, String userName, String folderName) { // 중복 폴더 존재 확인 @@ -78,10 +85,9 @@ public CreateFolderRes createFolder(long parentFolderId, String userName, String .userName(userName) .parentFolderId(parentFolderId) .build(); - folderMetaData = folderRepository.save(folderMetaData); - return new CreateFolderRes(folderMetaData.getFolderId(), folderName, userName); + return new CreateFolderRes(folderMetaData.getFolderId(), folderName); } /** @@ -109,47 +115,136 @@ public void updateFolderName(long folderId, long parentFolderId, String userName // 폴더 존재 여부 확인 FolderMetaData folder = getFolderInfo(folderId); - // 바꿀 권한이 있는가? - checkOwner(folder.getUserName(), userName); + // 권한 확인 + isOwner(folder.getUserName(), userName); - // 바꿀 폴더 이름이 이미 같은 위치에 존재하는가 + // 바꿀 폴더 이름이 이미 같은 위치에 존재하면 예외 처리 checkDuplicateFolder(newFolderName, parentFolderId, userName); // 폴더명 변경 folder.setFolderName(newFolderName); - folderRepository.save(folder); } /** - * 특정 폴더의 자식 폴더들의 목록을 반환한다. + * 폴더에 포함된 파일들을 pageSize만큼 반환한다. * @param folderId 특정 폴더 기본키 - * @return 자식 폴더 목록 + * @param page 페이지 번호 + * @return 파일 목록 */ - private List getFiles(long folderId) { - Optional> fileList = fileRepository.findAllByFolderId(folderId); + public List getFiles(long folderId, int page) { + Page filePage = fileRepository.findAllByFolderId(folderId, PageRequest.of(page, PAGE_SIZE)); + LinkedList result = new LinkedList<>(); - return fileList.map(files -> - files.stream().map(fileMetaData -> new FileMetaDataRes( - fileMetaData.getFileStorageName(), - fileMetaData.getSize(), - fileMetaData.getMime(), - fileMetaData.getUserName())).toList()) - .orElseGet(LinkedList::new); + for (FileMetaData fileMetaData : filePage) { + result.add(new FileMetaDataRes(fileMetaData.getFileStorageName(), fileMetaData.getSize(), + fileMetaData.getMime(), fileMetaData.getUserName())); + } + return result; } /** - * 특정 폴더의 자식 폴더들의 목록을 반환한다. + * 폴더의 하위 폴더들을 pageSize만큼 반환한다. * @param folderId 특정 폴더 기본키 - * @return 자식 폴더 목록 + * @param page 페이지 번호 + * @return 하위 폴더 목록 */ - private List getFolders(long folderId) { - Optional> folderList = folderRepository.findAllByParentFolderId(folderId); - - return folderList.map(folders -> - folders.stream().map(folderMetaData -> new FolderMetaDataRes( - folderMetaData.getFolderId(), - folderMetaData.getFolderName(), - folderMetaData.getUserName())).toList()) - .orElseGet(LinkedList::new); + public List getFolders(long folderId, int page) { + Page folderList = folderRepository.findAllByParentFolderId(folderId, + PageRequest.of(page, PAGE_SIZE)); + LinkedList result = new LinkedList<>(); + + for (FolderMetaData folderMetaData : folderList) { + result.add(new FolderMetaDataRes(folderMetaData.getFolderId(), folderMetaData.getFolderName(), + folderMetaData.getUserName())); + } + return result; + } + + /** + * 폴더를 삭제하고, 해당 폴더의 모든 하위 폴더와 파일들을 삭제한다. + * @param folderId 폴더 기본키 + */ + @Transactional + public void deleteFolder(long folderId, String userName) { + // folderId 권한 확인 후 삭제 + FolderMetaData folder = getFolderInfo(folderId); + isOwner(folder.getUserName(), userName); + folderRepository.delete(folder); + + // 하위 폴더 삭제 BFS + ArrayDeque folderQueue = new ArrayDeque<>(); + folderQueue.add(folderId); + + while (!folderQueue.isEmpty()) { + long parentFolderId = folderQueue.removeFirst(); + // 해당 폴더의 모든 파일 메타 데이터 삭제 + deleteAllFile(parentFolderId); + // 해당 폴더의 자식 폴더들 조회 + Optional> children = folderRepository.findAllByParentFolderId(parentFolderId); + if (children.isPresent()) { + folderRepository.deleteAllInBatch(children.get()); + for (FolderMetaData folderMetaData : children.get()) { + folderQueue.add(folderMetaData.getFolderId()); + } + } + } + } + + /** + * 특정 폴더에 속한 모든 파일 메타 데이터를 삭제한다. + * @param folderId 폴더 기본키 + */ + @Transactional + public void deleteAllFile(long folderId) { + Optional> fileList = fileRepository.findAllByFolderId(folderId); + + if (fileList.isPresent()) { + List deleteLogs = new LinkedList<>(); + for (FileMetaData fileMetaData : fileList.get()) { + deleteLogs.add(new DeleteLog(fileMetaData.getFileStorageName())); + } + deleteLogRepository.saveAll(deleteLogs); + fileRepository.deleteAllInBatch(fileList.get()); + } + } + + /** + * 폴더를 이동한다. + * 논리적으로는 하위의 모든 폴더와 파일이 이동해야 하지만 일일이 수정 할 필요가 없다. + * 하위 폴더나 파일이나, 다 이동할 'folderId'를 보고 있기 때문에, 'folderId'를 조회할 때 어차피 따라온다. + * 'folderId'에게 화살표를 가리키고 있기 때문에, 'folderId'가 어디로 이동하던 상관 없는 것이다. + * 그러므로 'folderId'의 부모 폴더 pk만 바꾼다. + * @param folderId 이동하는 폴더 + * @param targetFolderId 이동할 위치의 폴더 기본키 + */ + @Transactional + public void moveFolder(long folderId, long targetFolderId, String userName) { + // 이동할 폴더가 존재하는가 + FolderMetaData folder = getFolderInfo(folderId); + // 이동시킬 권한을 가졌는가 + isOwner(folder.getUserName(), userName); + // 이동할 수 있는가 (targetFolder의 부모에 folderId가 없는가) + checkFolderRelationship(folderId, targetFolderId); + // 이동 + folder.setParentFolderId(targetFolderId); + } + + /** + * targetFolderId가 folderId의 하위 폴더인지 확인한다. + * folderId가 targetFolderId의 하위 폴더라면 폴더 이동이 가능하므로 문제가 없다. + * @param folderId 움직일 폴더의 아이디 + * @param targetFolderId 도착 지점의 폴더 아이디 + */ + private void checkFolderRelationship(long folderId, long targetFolderId) { + long nowFolderId = targetFolderId; + + // 0은 root 값이며, 이에 도달하면 종료한다. + while (nowFolderId != 0) { + nowFolderId = folderRepository.findParentFolderIdByFolderId(nowFolderId); + if (nowFolderId == folderId) { + // 만약 부모 - 자식 관계에 있다면 경로에 사이클이 생긴다. + throw ErrorCd.FOLDER_CANT_BE_MOVED.serviceException(); + } + } } } diff --git a/src/main/java/com/c4cometrue/mystorage/util/FileUtil.java b/src/main/java/com/c4cometrue/mystorage/util/FileUtil.java index 33fc7ce..f43f302 100644 --- a/src/main/java/com/c4cometrue/mystorage/util/FileUtil.java +++ b/src/main/java/com/c4cometrue/mystorage/util/FileUtil.java @@ -58,7 +58,7 @@ public static void createFolder(Path folderPath) { public static void renameFolder(Path oldPath, Path newPath) { if (!Files.exists(oldPath)) { - throw ErrorCd.FOLDER_NOT_EXIST.serviceException("[Rename Folder] folder name is duplicate"); + throw ErrorCd.FOLDER_NOT_EXIST.serviceException("[Rename Folder] folder name is duplicated"); } try { diff --git a/src/test/java/com/c4cometrue/mystorage/controller/FileControllerTest.java b/src/test/java/com/c4cometrue/mystorage/controller/FileControllerTest.java index 8ab105b..1f361ae 100644 --- a/src/test/java/com/c4cometrue/mystorage/controller/FileControllerTest.java +++ b/src/test/java/com/c4cometrue/mystorage/controller/FileControllerTest.java @@ -11,9 +11,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.Resource; -import com.c4cometrue.mystorage.dto.request.FileReq; -import com.c4cometrue.mystorage.dto.request.UploadFileReq; -import com.c4cometrue.mystorage.dto.response.FileDownloadRes; +import com.c4cometrue.mystorage.dto.request.file.FileReq; +import com.c4cometrue.mystorage.dto.request.file.MoveFileReq; +import com.c4cometrue.mystorage.dto.request.file.UploadFileReq; +import com.c4cometrue.mystorage.dto.response.file.FileDownloadRes; import com.c4cometrue.mystorage.service.FileService; @ExtendWith(MockitoExtension.class) @@ -39,27 +40,40 @@ void uploadFile() { @DisplayName("파일 삭제") void deleteFile() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); + var req = new FileReq(1L, MOCK_USER_NAME, 1L); // when fileController.deleteFile(req); // then - verify(fileService, times(1)).deleteFile(req.fileStorageName(), req.userName(), req.folderId()); + verify(fileService, times(1)).deleteFile(req.fileId(), req.userName(), req.folderId()); } @Test @DisplayName("파일 다운로드") void downloadFile() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); - given(fileService.downloadFile(req.fileStorageName(), req.userName(), req.folderId())).willReturn( + var req = new FileReq(1L, MOCK_USER_NAME, 1L); + given(fileService.downloadFile(req.fileId(), req.userName(), req.folderId())).willReturn( new FileDownloadRes(mock(Resource.class), MOCK_FILE_NAME, MOCK_CONTENT_TYPE)); // when fileController.downloadFile(req); // then - verify(fileService, times(1)).downloadFile(req.fileStorageName(), req.userName(), req.folderId()); + verify(fileService, times(1)).downloadFile(req.fileId(), req.userName(), req.folderId()); + } + + @Test + @DisplayName("파일 이동") + void moveFile() { + // given + var req = new MoveFileReq(1L, 1L, MOCK_USER_NAME); + + // when + fileController.moveFile(req); + + // then + verify(fileService, times(1)).moveFile(req.fileId(), req.folderId(), req.userName()); } } diff --git a/src/test/java/com/c4cometrue/mystorage/controller/FolderControllerTest.java b/src/test/java/com/c4cometrue/mystorage/controller/FolderControllerTest.java index a8c8e0c..02203a6 100644 --- a/src/test/java/com/c4cometrue/mystorage/controller/FolderControllerTest.java +++ b/src/test/java/com/c4cometrue/mystorage/controller/FolderControllerTest.java @@ -10,9 +10,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.c4cometrue.mystorage.dto.request.CreateFolderReq; -import com.c4cometrue.mystorage.dto.request.GetFolderReq; -import com.c4cometrue.mystorage.dto.request.UpdateFolderNameReq; +import com.c4cometrue.mystorage.dto.request.folder.CreateFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.DeleteFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.GetFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.GetSubInfoReq; +import com.c4cometrue.mystorage.dto.request.folder.MoveFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.UpdateFolderNameReq; import com.c4cometrue.mystorage.service.FolderService; @ExtendWith(MockitoExtension.class) @@ -32,7 +35,39 @@ void getFolderData() { folderController.getFolderData(req); // then - verify(folderService, times(1)).getFolderData(req.folderId(), req.userName()); + verify(folderService, times(1)).getFolderTotalInfo(req.folderId(), req.userName()); + } + + @Test + @DisplayName("폴더의 하위 폴더 n 페이지 조회") + void getFolderSubFolders() { + // given + var folderId = 1L; + var pageNumber = 1; + var req = new GetSubInfoReq(folderId, pageNumber); + + // when + folderController.getSubFolders(req); + + // then + verify(folderService, times(1)).getFolders(folderId, pageNumber); + + } + + + @Test + @DisplayName("폴더의 하위 파일 n 페이지 조회") + void getFolderSubFiles() { + // given + var folderId = 1L; + var pageNumber = 1; + var req = new GetSubInfoReq(folderId, pageNumber); + + // when + folderController.getSubFiles(req); + + // then + verify(folderService, times(1)).getFiles(folderId, pageNumber); } @@ -40,7 +75,7 @@ void getFolderData() { @DisplayName("폴더 생성") void createFolder() { // given - var req = new CreateFolderReq(0L, MOCK_USER_NAME, "my_folder"); + var req = new CreateFolderReq(1L, MOCK_USER_NAME, "my_folder"); // when folderController.createFolder(req); @@ -53,7 +88,7 @@ void createFolder() { @DisplayName("폴더 이름 수정") void updateFolderName() { // given - var req = new UpdateFolderNameReq(1L, 0L, MOCK_USER_NAME, "new_folder"); + var req = new UpdateFolderNameReq(2L, 1L, MOCK_USER_NAME, "new_folder"); // when folderController.updateFolderName(req); @@ -63,4 +98,30 @@ void updateFolderName() { req.newFolderName()); } + @Test + @DisplayName("폴더 이동") + void moveFolder() { + // given + var req = new MoveFolderReq(1L, 99L, MOCK_USER_NAME); + + // when + folderController.moveFolder(req); + + // then + verify(folderService, times(1)).moveFolder(req.folderId(), req.targetFolderId(), req.userName()); + } + + @Test + @DisplayName("폴더 삭제") + void deleteFolder() { + // given + var req = new DeleteFolderReq(1L, MOCK_USER_NAME); + + // when + folderController.deleteFolder(req); + + // then + verify(folderService, times(1)).deleteFolder(req.folderId(), req.userName()); + } + } diff --git a/src/test/java/com/c4cometrue/mystorage/controller/UserControllerTest.java b/src/test/java/com/c4cometrue/mystorage/controller/UserControllerTest.java index b61bead..bb0df56 100644 --- a/src/test/java/com/c4cometrue/mystorage/controller/UserControllerTest.java +++ b/src/test/java/com/c4cometrue/mystorage/controller/UserControllerTest.java @@ -10,7 +10,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.c4cometrue.mystorage.dto.request.SignUpReq; +import com.c4cometrue.mystorage.dto.request.file.SignUpReq; import com.c4cometrue.mystorage.service.UserService; @ExtendWith(MockitoExtension.class) diff --git a/src/test/java/com/c4cometrue/mystorage/service/FileServiceTest.java b/src/test/java/com/c4cometrue/mystorage/service/FileServiceTest.java index f6ac76e..61065c1 100644 --- a/src/test/java/com/c4cometrue/mystorage/service/FileServiceTest.java +++ b/src/test/java/com/c4cometrue/mystorage/service/FileServiceTest.java @@ -21,12 +21,12 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import com.c4cometrue.mystorage.dto.request.FileReq; -import com.c4cometrue.mystorage.dto.request.UploadFileReq; +import com.c4cometrue.mystorage.dto.request.file.UploadFileReq; import com.c4cometrue.mystorage.entity.FileMetaData; import com.c4cometrue.mystorage.entity.FolderMetaData; import com.c4cometrue.mystorage.exception.ErrorCd; import com.c4cometrue.mystorage.exception.ServiceException; +import com.c4cometrue.mystorage.repository.DeleteLogRepository; import com.c4cometrue.mystorage.repository.FileRepository; import com.c4cometrue.mystorage.repository.FolderRepository; import com.c4cometrue.mystorage.util.FileUtil; @@ -43,6 +43,8 @@ public class FileServiceTest { ResourceLoader mockResourceLoader; @Mock StoragePathService storagePathService; + @Mock + DeleteLogRepository deleteLogRepository; private static MockedStatic fileUtilMockedStatic; @@ -60,13 +62,14 @@ public static void tearDown() { @DisplayName("파일 업로드 성공") void uploadFile() { // given + var folderId = 1L; var mockFolderMetaData = FolderMetaData.builder() .folderName("folderName") .userName(MOCK_USER_NAME) .parentFolderId(1L) .build(); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.of(mockFolderMetaData)); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); given(storagePathService.createPathByUser(MOCK_USER_NAME)).willReturn( Paths.get(MOCK_ROOT_PATH, MOCK_USER_NAME)); @@ -74,7 +77,7 @@ void uploadFile() { given(MOCK_MULTIPART_FILE.getSize()).willReturn(MOCK_SIZE); given(MOCK_MULTIPART_FILE.getContentType()).willReturn(MOCK_CONTENT_TYPE); - var req = new UploadFileReq(MOCK_MULTIPART_FILE, MOCK_USER_NAME, 1L); + var req = new UploadFileReq(MOCK_MULTIPART_FILE, MOCK_USER_NAME, folderId); // when var createFileRes = fileService.uploadFile(req.file(), req.userName(), req.folderId()); @@ -92,126 +95,198 @@ void uploadFile() { @DisplayName("파일 업로드 실패 - 중복 파일명") void uploadFileFailDuplicateName() { // given + var folderId = 1L; var mockFolderMetaData = FolderMetaData.builder() .folderName("folderName") .userName(MOCK_USER_NAME) - .parentFolderId(1L) + .parentFolderId(folderId) .build(); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.of(mockFolderMetaData)); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); given(MOCK_MULTIPART_FILE.getOriginalFilename()).willReturn(MOCK_FILE_NAME); - given(fileRepository.findByFolderIdAndUserNameAndFileName(1L, MOCK_USER_NAME, MOCK_FILE_NAME)) + given(fileRepository.findByFolderIdAndUserNameAndFileName(folderId, MOCK_USER_NAME, MOCK_FILE_NAME)) .willReturn(Optional.of(new FileMetaData())); - var req = new UploadFileReq(MOCK_MULTIPART_FILE, MOCK_USER_NAME, 1L); // when var exception = assertThrows(ServiceException.class, - () -> fileService.uploadFile(MOCK_MULTIPART_FILE, MOCK_USER_NAME, 1L)); + () -> fileService.uploadFile(MOCK_MULTIPART_FILE, MOCK_USER_NAME, folderId)); // then assertEquals(ErrorCd.DUPLICATE_FILE.name(), exception.getErrCode()); } @Test - @DisplayName("파일 데이터 DB 확인") - void getFileMetaData() { + @DisplayName("파일 삭제") + void deleteFile() { // given + var fileId = 1L; + var folderId = 1L; var mockFileMetaData = FileMetaData.builder() .fileName(MOCK_FILE_NAME) .fileStorageName(MOCK_FILE_STORAGE_NAME) .userName(MOCK_USER_NAME) .size(MOCK_SIZE) .mime(MOCK_CONTENT_TYPE) - .folderId(1L) + .folderId(folderId) + .build(); + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) .build(); - given(fileRepository.findByFileStorageName(MOCK_FILE_STORAGE_NAME)).willReturn(Optional.of(mockFileMetaData)); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.of(mockFileMetaData)); // when - var fileMetadata = fileService.getFileMetaData(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME); + fileService.deleteFile(fileId, MOCK_USER_NAME, folderId); // then - assertThat(fileMetadata) - .matches(metadata -> StringUtils.equals(metadata.getFileName(), MOCK_FILE_NAME)) - .matches(metadata -> metadata.getSize() == MOCK_SIZE) - .matches(metadata -> StringUtils.equals(metadata.getMime(), MOCK_CONTENT_TYPE)) - .matches(metadata -> StringUtils.equals(metadata.getUserName(), MOCK_USER_NAME)); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(fileRepository, times(1)).findById(fileId); + verify(fileRepository, times(1)).delete(mockFileMetaData); + verify(deleteLogRepository, times(1)).save(any()); } @Test - @DisplayName("파일 데이터 DB 확인 실패 - 파일 없음") - void getFileMetaDataFailWrongFileStorageName() { + @DisplayName("파일 삭제 실패 - 파일 데이터 조회 실패") + void deleteFileFailByFileMetaDataNotFound() { // given - var wrongFileStorageName = "wrong_file_path.txt"; - given(fileRepository.findByFileStorageName(wrongFileStorageName)).willReturn(Optional.empty()); + var fileId = 1L; + var folderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.empty()); // when var exception = assertThrows(ServiceException.class, - () -> fileService.getFileMetaData(wrongFileStorageName, MOCK_USER_NAME)); + () -> fileService.deleteFile(fileId, MOCK_USER_NAME, folderId)); // then assertEquals(ErrorCd.FILE_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(fileRepository, times(1)).findById(fileId); } @Test - @DisplayName("파일 데이터 DB 확인 실패 - 요청자가 주인이 아님") - void getFileMetaDataFailNotOwner() { + @DisplayName("파일 삭제 실패 - 권한이 없음") + void deleteFileFailByNoAuthority() { // given - var mockFileMetaData = FileMetaData.builder() - .fileName(MOCK_FILE_NAME) - .fileStorageName(MOCK_FILE_STORAGE_NAME) + var fileId = 1L; + var folderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") .userName(MOCK_USER_NAME) - .size(MOCK_SIZE) - .mime(MOCK_CONTENT_TYPE) - .folderId(1L) + .parentFolderId(0L) .build(); - given(fileRepository.findByFileStorageName(MOCK_FILE_STORAGE_NAME)).willReturn(Optional.of(mockFileMetaData)); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); // when var exception = assertThrows(ServiceException.class, - () -> fileService.getFileMetaData(MOCK_FILE_STORAGE_NAME, "anonymous")); + () -> fileService.deleteFile(fileId, "anonymous", folderId)); // then assertEquals(ErrorCd.NO_PERMISSION.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); } @Test - @DisplayName("파일 삭제") - void deleteFile() { + @DisplayName("파일 다운로드") + void downloadFile() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); + var fileId = 1L; + var folderId = 1L; + var mockResource = mock(Resource.class); var mockFileMetaData = FileMetaData.builder() .fileName(MOCK_FILE_NAME) .fileStorageName(MOCK_FILE_STORAGE_NAME) .userName(MOCK_USER_NAME) .size(MOCK_SIZE) .mime(MOCK_CONTENT_TYPE) - .folderId(1L) + .folderId(folderId) .build(); var mockFolderMetaData = FolderMetaData.builder() .folderName("folderName") .userName(MOCK_USER_NAME) - .parentFolderId(1L) + .parentFolderId(folderId) .build(); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.of(mockFileMetaData)); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + given(mockResourceLoader.getResource(any())).willReturn(mockResource); given(storagePathService.createPathByUser(MOCK_USER_NAME)).willReturn( Paths.get(MOCK_ROOT_PATH, MOCK_USER_NAME)); - given(fileRepository.findByFileStorageName(MOCK_FILE_STORAGE_NAME)).willReturn(Optional.of(mockFileMetaData)); + + given(mockResource.exists()).willReturn(true); // when - fileService.deleteFile(req.fileStorageName(), req.userName(), req.folderId()); + fileService.downloadFile(fileId, MOCK_USER_NAME, folderId); // then - verify(folderRepository, times(1)).findByFolderId(1L); - verify(fileRepository, times(1)).findByFileStorageName(any()); - verify(fileRepository, times(1)).delete(any()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(fileRepository, times(1)).findById(fileId); } @Test - @DisplayName("파일 다운로드") - void downloadFile() { + @DisplayName("파일 다운로드 실패 - 파일 데이터 조회 실패") + void downloadFileFailByFileMetaDataNotFound() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); + var fileId = 1L; + var folderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.empty()); + + // when + var exception = assertThrows(ServiceException.class, + () -> fileService.downloadFile(fileId, MOCK_USER_NAME, folderId)); + + // then + assertEquals(ErrorCd.FILE_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(fileRepository, times(1)).findById(fileId); + } + + @Test + @DisplayName("파일 다운로드 실패 - 권한이 없음") + void downloadFileFailByNoAuthority() { + // given + var fileId = 1L; + var folderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + + // when + var exception = assertThrows(ServiceException.class, + () -> fileService.downloadFile(fileId, "anonymous", folderId)); + + // then + assertEquals(ErrorCd.NO_PERMISSION.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + } + + + @Test + @DisplayName("파일 다운로드 실패") + void downloadFileFailByFileNotExist() { + // given + var fileId = 1L; + var folderId = 1L; var mockResource = mock(Resource.class); var mockFileMetaData = FileMetaData.builder() .fileName(MOCK_FILE_NAME) @@ -219,36 +294,56 @@ void downloadFile() { .userName(MOCK_USER_NAME) .size(MOCK_SIZE) .mime(MOCK_CONTENT_TYPE) - .folderId(1L) + .folderId(folderId) .build(); + var mockFolderMetaData = FolderMetaData.builder() .folderName("folderName") .userName(MOCK_USER_NAME) - .parentFolderId(1L) + .parentFolderId(0L) .build(); - given(fileRepository.findByFileStorageName(MOCK_FILE_STORAGE_NAME)).willReturn(Optional.of(mockFileMetaData)); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.of(mockFileMetaData)); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); given(mockResourceLoader.getResource(any())).willReturn(mockResource); given(storagePathService.createPathByUser(MOCK_USER_NAME)).willReturn( Paths.get(MOCK_ROOT_PATH, MOCK_USER_NAME)); + // 물리적 파일을 찾지 못함 + given(mockResource.exists()).willReturn(false); - given(mockResource.exists()).willReturn(true); + // when + var exception = assertThrows(ServiceException.class, + () -> fileService.downloadFile(fileId, MOCK_USER_NAME, folderId)); + + // then + assertEquals(ErrorCd.FILE_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(fileRepository, times(1)).findById(fileId); + } + + @Test + @DisplayName("파일이 있어야할 폴더가 없다") + void folderNotExist() { + // given + var fileId = 1L; + var folderId = 1L; + given(folderRepository.findByFolderId(fileId)).willReturn(Optional.empty()); // when - fileService.downloadFile(req.fileStorageName(), req.userName(), req.folderId()); + var exception = assertThrows(ServiceException.class, + () -> fileService.downloadFile(fileId, MOCK_USER_NAME, folderId)); // then - verify(folderRepository, times(1)).findByFolderId(1L); - verify(fileRepository, times(1)).findByFileStorageName(any()); + assertEquals(ErrorCd.FOLDER_NOT_EXIST.name(), exception.getErrCode()); } @Test - @DisplayName("파일 다운로드 실패") - void downloadFileFail() { + @DisplayName("파일 이동 성공") + void moveFile() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); - var mockResource = mock(Resource.class); + var fileId = 1L; + var targetFolderId = 1234L; + var mockFileMetaData = FileMetaData.builder() .fileName(MOCK_FILE_NAME) .fileStorageName(MOCK_FILE_STORAGE_NAME) @@ -257,44 +352,112 @@ void downloadFileFail() { .mime(MOCK_CONTENT_TYPE) .folderId(1L) .build(); + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(fileRepository.findById(fileId)).willReturn(Optional.of(mockFileMetaData)); + given(folderRepository.findByFolderId(targetFolderId)).willReturn(Optional.of(mockFolderMetaData)); + + // when + fileService.moveFile(fileId, targetFolderId, MOCK_USER_NAME); + // then + assertEquals(mockFileMetaData.getFolderId(), targetFolderId); + verify(fileRepository, times(1)).findById(fileId); + verify(folderRepository, times(1)).findByFolderId(targetFolderId); + } + + @Test + @DisplayName("파일 이동 실패 - 파일 데이터 조회 실패") + void moveFileFailByFileMetaDataNotFound() { + // given + var fileId = 1L; + var targetFolderId = 1L; var mockFolderMetaData = FolderMetaData.builder() .folderName("folderName") .userName(MOCK_USER_NAME) .parentFolderId(0L) .build(); - given(fileRepository.findByFileStorageName(MOCK_FILE_STORAGE_NAME)).willReturn(Optional.of(mockFileMetaData)); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.of(mockFolderMetaData)); - given(mockResourceLoader.getResource(any())).willReturn(mockResource); - given(storagePathService.createPathByUser(MOCK_USER_NAME)).willReturn( - Paths.get(MOCK_ROOT_PATH, MOCK_USER_NAME)); - // 물리적 파일을 찾지 못함 - given(mockResource.exists()).willReturn(false); + given(folderRepository.findByFolderId(targetFolderId)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.empty()); // when var exception = assertThrows(ServiceException.class, - () -> fileService.downloadFile(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L)); + () -> fileService.moveFile(fileId, targetFolderId, MOCK_USER_NAME)); // then - verify(folderRepository, times(1)).findByFolderId(1L); - verify(fileRepository, times(1)).findByFileStorageName(any()); assertEquals(ErrorCd.FILE_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(targetFolderId); + verify(fileRepository, times(1)).findById(fileId); } @Test - @DisplayName("파일이 있어야할 폴더가 없다") - void folderNotExist() { + @DisplayName("파일 이동 실패 - 권한이 없음") + void moveFileFailByNoAuthority() { + // given + var fileId = 1L; + var targetFolderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(targetFolderId)).willReturn(Optional.of(mockFolderMetaData)); + + // when + var exception = assertThrows(ServiceException.class, + () -> fileService.moveFile(fileId, targetFolderId, "anonymous")); + + // then + assertEquals(ErrorCd.NO_PERMISSION.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(targetFolderId); + } + + @Test + @DisplayName("파일 이동 실패 - 파일 정보 없음") + void moveFileFailByFileNotFound() { + // given + var fileId = 1L; + var targetFolderId = 1234L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("folderName") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(targetFolderId)).willReturn(Optional.of(mockFolderMetaData)); + given(fileRepository.findById(fileId)).willReturn(Optional.empty()); + + // when + var exception = assertThrows(ServiceException.class, + () -> fileService.moveFile(fileId, targetFolderId, MOCK_USER_NAME)); + + // then + assertEquals(ErrorCd.FILE_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(targetFolderId); + verify(fileRepository, times(1)).findById(fileId); + } + + @Test + @DisplayName("파일 이동 실패 - 이동할 폴더가 없음") + void moveFileFailByFolderNotFound() { // given - var req = new FileReq(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L); - given(folderRepository.findByFolderId(1L)).willReturn(Optional.empty()); + var fileId = 1L; + var targetFolderId = 1234L; + given(folderRepository.findByFolderId(targetFolderId)).willReturn(Optional.empty()); // when var exception = assertThrows(ServiceException.class, - () -> fileService.downloadFile(MOCK_FILE_STORAGE_NAME, MOCK_USER_NAME, 1L)); + () -> fileService.moveFile(fileId, targetFolderId, MOCK_USER_NAME)); // then assertEquals(ErrorCd.FOLDER_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(targetFolderId); } } diff --git a/src/test/java/com/c4cometrue/mystorage/service/FolderServiceTest.java b/src/test/java/com/c4cometrue/mystorage/service/FolderServiceTest.java index 38c6f2f..5e33228 100644 --- a/src/test/java/com/c4cometrue/mystorage/service/FolderServiceTest.java +++ b/src/test/java/com/c4cometrue/mystorage/service/FolderServiceTest.java @@ -15,15 +15,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.test.util.ReflectionTestUtils; -import com.c4cometrue.mystorage.dto.request.CreateFolderReq; -import com.c4cometrue.mystorage.dto.request.GetFolderReq; -import com.c4cometrue.mystorage.dto.request.UpdateFolderNameReq; +import com.c4cometrue.mystorage.dto.request.folder.CreateFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.GetFolderReq; +import com.c4cometrue.mystorage.dto.request.folder.UpdateFolderNameReq; import com.c4cometrue.mystorage.entity.FileMetaData; import com.c4cometrue.mystorage.entity.FolderMetaData; import com.c4cometrue.mystorage.exception.ErrorCd; import com.c4cometrue.mystorage.exception.ServiceException; +import com.c4cometrue.mystorage.repository.DeleteLogRepository; import com.c4cometrue.mystorage.repository.FileRepository; import com.c4cometrue.mystorage.repository.FolderRepository; @@ -31,11 +34,12 @@ class FolderServiceTest { @InjectMocks FolderService folderService; - @Mock FolderRepository folderRepository; @Mock FileRepository fileRepository; + @Mock + DeleteLogRepository deleteLogRepository; @Test @DisplayName("폴더 정보 조회 성공") @@ -74,11 +78,12 @@ void getFolderData() { ReflectionTestUtils.setField(subFolder, "folderId", 3L); folderList.add(subFolder); - given(fileRepository.findAllByFolderId(2L)).willReturn(Optional.of(fileList)); - given(folderRepository.findAllByParentFolderId(2L)).willReturn(Optional.of(folderList)); + var pageRequest = PageRequest.of(0, 50); + given(fileRepository.findAllByFolderId(2L, pageRequest)).willReturn(new PageImpl<>(fileList)); + given(folderRepository.findAllByParentFolderId(2L, pageRequest)).willReturn(new PageImpl<>(folderList)); // when - var folderOverviewRes = folderService.getFolderData(req.folderId(), req.userName()); + var folderOverviewRes = folderService.getFolderTotalInfo(req.folderId(), req.userName()); // then assertThat(folderOverviewRes) @@ -96,7 +101,7 @@ void getFolderDataFailNoFolder() { // when var exception = assertThrows(ServiceException.class, - () -> folderService.getFolderData(1L, MOCK_USER_NAME)); + () -> folderService.getFolderTotalInfo(1L, MOCK_USER_NAME)); // then assertEquals(exception.getErrCode(), ErrorCd.FOLDER_NOT_EXIST.name()); @@ -118,7 +123,7 @@ void getFolderDataFailNotOwner() { // when var exception = assertThrows(ServiceException.class, - () -> folderService.getFolderData(1L, "Anonymous")); + () -> folderService.getFolderTotalInfo(1L, "Anonymous")); // then assertEquals(exception.getErrCode(), ErrorCd.NO_PERMISSION.name()); @@ -151,8 +156,7 @@ void createFolder() { // then assertThat(createFolderRes) - .matches(res -> StringUtils.equals(res.folderName(), folderName)) - .matches(res -> StringUtils.equals(res.userName(), MOCK_USER_NAME)); + .matches(res -> StringUtils.equals(res.folderName(), folderName)); } @Test @@ -236,4 +240,129 @@ void updateFolderNameFail() { verify(folderRepository, times(1)).findByFolderNameAndParentFolderIdAndUserName(mockNewFolderName, 1L, MOCK_USER_NAME); } + + @Test + @DisplayName("폴더 이동") + void moveFolder() { + // given + var folderId = 1L; + var targetFolderId = 99L; + + var mockFolderMetaData = FolderMetaData.builder() + .folderName("my_folder") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + given(folderRepository.findParentFolderIdByFolderId(targetFolderId)).willReturn(0L); + + // when + folderService.moveFolder(folderId, targetFolderId, MOCK_USER_NAME); + + // then + assertEquals(targetFolderId, mockFolderMetaData.getParentFolderId()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(folderRepository, times(1)).findParentFolderIdByFolderId(targetFolderId); + } + + @Test + @DisplayName("폴더 이동 실패 - 폴더가 존재하지 않음") + void moveFolderFailByFolderNotFound() { + // given + var folderId = 1L; + var targetFolderId = 99L; + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.empty()); + + // when + var exception = assertThrows(ServiceException.class, + () -> folderService.moveFolder(folderId, targetFolderId, MOCK_USER_NAME)); + + // then + assertEquals(ErrorCd.FOLDER_NOT_EXIST.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(folderRepository, times(0)).findParentFolderIdByFolderId(targetFolderId); + } + + @Test + @DisplayName("폴더 이동 실패 - 폴더 경로에 사이클이 생김") + void moveFolderFailByLoopRelation() { + // given + var folderId = 1L; + var targetFolderId = 99L; + + var mockFolderMetaData = FolderMetaData.builder() + .folderName("my_folder") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + // targetFolderId의 부모 폴더가 folderId라면, 자식 폴더에 이동하는 셈이므로 막아야한다. + given(folderRepository.findParentFolderIdByFolderId(targetFolderId)).willReturn(folderId); + + // when + var exception = assertThrows(ServiceException.class, + () -> folderService.moveFolder(folderId, targetFolderId, MOCK_USER_NAME)); + + // then + assertEquals(ErrorCd.FOLDER_CANT_BE_MOVED.name(), exception.getErrCode()); + verify(folderRepository, times(1)).findByFolderId(folderId); + verify(folderRepository, times(1)).findParentFolderIdByFolderId(targetFolderId); + } + + @Test + @DisplayName("폴더 삭제") + void deleteFolder() { + // given + // 1. 삭제할 폴더 + var folderId = 1L; + var mockFolderMetaData = FolderMetaData.builder() + .folderName("my_folder") + .userName(MOCK_USER_NAME) + .parentFolderId(0L) + .build(); + given(folderRepository.findByFolderId(folderId)).willReturn(Optional.of(mockFolderMetaData)); + + // 2. 삭제할 폴더의 하위 파일 목록 + var fileList = new ArrayList(); + fileList.add(FileMetaData.builder() + .fileName(MOCK_FILE_NAME) + .fileStorageName(MOCK_FILE_STORAGE_NAME) + .userName(MOCK_USER_NAME) + .size(MOCK_SIZE) + .mime(MOCK_CONTENT_TYPE) + .folderId(2L) + .build()); + given(fileRepository.findAllByFolderId(folderId)).willReturn(Optional.of(fileList)); + + // 3. 삭제할 폴더의 하위 폴더 목록 + var folderList = new ArrayList(); + var subFolderId = 2L; + var subFolder = FolderMetaData.builder() + .folderName("childFolder") + .userName(MOCK_USER_NAME) + .parentFolderId(folderId) + .build(); + ReflectionTestUtils.setField(subFolder, "folderId", subFolderId); + folderList.add(subFolder); + given(folderRepository.findAllByParentFolderId(folderId)).willReturn(Optional.of(folderList)); + given(fileRepository.findAllByFolderId(subFolderId)).willReturn(Optional.empty()); + given(folderRepository.findAllByParentFolderId(subFolderId)).willReturn(Optional.empty()); + + // when + folderService.deleteFolder(folderId, MOCK_USER_NAME); + + // then + verify(folderRepository, times(1)).delete(mockFolderMetaData); + + verify(fileRepository, times(1)).findAllByFolderId(folderId); + verify(deleteLogRepository, times(1)).saveAll(any()); + verify(fileRepository, times(1)).deleteAllInBatch(any()); + + verify(folderRepository, times(1)).findAllByParentFolderId(subFolderId); + verify(fileRepository, times(1)).findAllByFolderId(subFolderId); + } + } diff --git a/src/test/java/com/c4cometrue/mystorage/service/UserServiceTest.java b/src/test/java/com/c4cometrue/mystorage/service/UserServiceTest.java index 29e1984..0fb8bcf 100644 --- a/src/test/java/com/c4cometrue/mystorage/service/UserServiceTest.java +++ b/src/test/java/com/c4cometrue/mystorage/service/UserServiceTest.java @@ -19,7 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import com.c4cometrue.mystorage.dto.request.SignUpReq; +import com.c4cometrue.mystorage.dto.request.file.SignUpReq; import com.c4cometrue.mystorage.entity.FolderMetaData; import com.c4cometrue.mystorage.entity.UserData; import com.c4cometrue.mystorage.exception.ErrorCd;