diff --git a/src/main/java/com/tiki/server/common/handler/ErrorHandler.java b/src/main/java/com/tiki/server/common/handler/ErrorHandler.java index 5b7b29f1..d86ae5d7 100644 --- a/src/main/java/com/tiki/server/common/handler/ErrorHandler.java +++ b/src/main/java/com/tiki/server/common/handler/ErrorHandler.java @@ -3,6 +3,7 @@ import com.tiki.server.auth.exception.AuthException; import com.tiki.server.common.dto.ErrorCodeResponse; import com.tiki.server.emailverification.exception.EmailVerificationException; +import com.tiki.server.folder.exception.FolderException; import com.tiki.server.note.exception.NoteException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -79,14 +80,21 @@ public ResponseEntity externalException(ExternalException exceptio } @ExceptionHandler(EmailVerificationException.class) - public ResponseEntity MailException(EmailVerificationException exception) { + public ResponseEntity mailException(EmailVerificationException exception) { + log.error(exception.getMessage()); + val errorCode = exception.getErrorCode(); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.of(errorCode.getMessage())); + } + + @ExceptionHandler(FolderException.class) + public ResponseEntity folderException(FolderException exception) { log.error(exception.getMessage()); val errorCode = exception.getErrorCode(); return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.of(errorCode.getMessage())); } @ExceptionHandler(AuthException.class) - public ResponseEntity AuthException(AuthException exception) { + public ResponseEntity authException(AuthException exception) { log.error(exception.getMessage()); val errorCode = exception.getErrorCode(); return ResponseEntity.status(errorCode.getHttpStatus()).body( @@ -101,7 +109,7 @@ public ResponseEntity httpMessageNotReadableException(HttpMessageN } @ExceptionHandler(Exception.class) - public ResponseEntity Exception(Exception exception) { + public ResponseEntity exception(Exception exception) { log.error(exception.getMessage()); val errorCode = UNCAUGHT_SERVER_EXCEPTION; return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.of(errorCode.getMessage())); diff --git a/src/main/java/com/tiki/server/document/adapter/DeletedDocumentAdapter.java b/src/main/java/com/tiki/server/document/adapter/DeletedDocumentAdapter.java new file mode 100644 index 00000000..2d46afe8 --- /dev/null +++ b/src/main/java/com/tiki/server/document/adapter/DeletedDocumentAdapter.java @@ -0,0 +1,47 @@ +package com.tiki.server.document.adapter; + +import static com.tiki.server.document.message.ErrorCode.INVALID_DOCUMENT; + +import java.util.List; + +import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.document.entity.DeletedDocument; +import com.tiki.server.document.entity.Document; +import com.tiki.server.document.exception.DocumentException; +import com.tiki.server.document.repository.DeletedDocumentRepository; + +import lombok.RequiredArgsConstructor; + +@RepositoryAdapter +@RequiredArgsConstructor +public class DeletedDocumentAdapter { + + private final DeletedDocumentRepository deletedDocumentRepository; + + public List get(final long teamId) { + return deletedDocumentRepository.findAllByTeamId(teamId); + } + + public void save(final List documents) { + documents.forEach(document -> deletedDocumentRepository.save(create(document, document.getTeamId()))); + } + + public List get(final List deletedDocumentIds, final long teamId) { + return deletedDocumentIds.stream() + .map(id -> find(id, teamId)) + .toList(); + } + + public void deleteAll(final List deletedDocuments) { + deletedDocumentRepository.deleteAll(deletedDocuments); + } + + private DeletedDocument create(final Document document, final long teamId) { + return DeletedDocument.of(document.getFileName(), document.getFileUrl(), teamId, document.getCapacity()); + } + + private DeletedDocument find(final long id, final long teamId) { + return deletedDocumentRepository.findByIdAndTeamId(id, teamId) + .orElseThrow(() -> new DocumentException(INVALID_DOCUMENT)); + } +} diff --git a/src/main/java/com/tiki/server/document/adapter/DocumentFinder.java b/src/main/java/com/tiki/server/document/adapter/DocumentFinder.java index 451155dd..fdcc7546 100644 --- a/src/main/java/com/tiki/server/document/adapter/DocumentFinder.java +++ b/src/main/java/com/tiki/server/document/adapter/DocumentFinder.java @@ -1,7 +1,6 @@ package com.tiki.server.document.adapter; import static com.tiki.server.document.message.ErrorCode.INVALID_DOCUMENT; -import static com.tiki.server.folder.constant.Constant.ROOT_PATH; import java.util.List; import java.util.Objects; @@ -21,6 +20,12 @@ public class DocumentFinder { private final DocumentRepository documentRepository; + public List findAllByIdAndTeamId(final List documentIds, final long teamId) { + return documentIds.stream() + .map(id -> findByIdAndTeamId(id, teamId)) + .toList(); + } + public Document findByIdOrElseThrow(final long documentId) { return documentRepository.findById(documentId).orElseThrow(() -> new DocumentException(INVALID_DOCUMENT)); } @@ -52,4 +57,13 @@ public boolean existsById(Long timeBlockId) { public List findByTeamIdAndFolderId(final long teamId, final Long folderId) { return documentRepository.findAllByTeamIdAndFolderIdOrderByCreatedAtDesc(teamId, folderId); } + + public List findAllByFolderId(final long folderId) { + return documentRepository.findAllByFolderId(folderId); + } + + private Document findByIdAndTeamId(long documentId, long teamId) { + return documentRepository.findByIdAndTeamId(documentId, teamId) + .orElseThrow(() -> new DocumentException(INVALID_DOCUMENT)); + } } diff --git a/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java b/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java index 34872245..8648cb85 100644 --- a/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java +++ b/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java @@ -1,6 +1,9 @@ package com.tiki.server.document.adapter; +import java.util.List; + import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.document.entity.DeletedDocument; import com.tiki.server.document.entity.Document; import com.tiki.server.document.repository.DocumentRepository; @@ -12,7 +15,20 @@ public class DocumentSaver { private final DocumentRepository documentRepository; - public Document save(Document document) { + public Document save(final Document document) { return documentRepository.save(document); } + + public void restore(final List deletedDocuments) { + deletedDocuments.forEach(document -> documentRepository.save(create(document))); + } + + private Document create(final DeletedDocument deletedDocument) { + return Document.restore( + deletedDocument.getFileName(), + deletedDocument.getFileUrl(), + deletedDocument.getCapacity(), + deletedDocument.getTeamId() + ); + } } diff --git a/src/main/java/com/tiki/server/document/controller/DocumentController.java b/src/main/java/com/tiki/server/document/controller/DocumentController.java index 47be9c77..21960d56 100644 --- a/src/main/java/com/tiki/server/document/controller/DocumentController.java +++ b/src/main/java/com/tiki/server/document/controller/DocumentController.java @@ -2,8 +2,10 @@ import static com.tiki.server.document.message.SuccessMessage.SUCCESS_CREATE_DOCUMENTS; import static com.tiki.server.document.message.SuccessMessage.SUCCESS_GET_DOCUMENTS; +import static com.tiki.server.document.message.SuccessMessage.SUCCESS_GET_TRASH; import java.security.Principal; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -11,7 +13,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -20,6 +21,7 @@ import com.tiki.server.common.support.UriGenerator; import com.tiki.server.document.controller.docs.DocumentControllerDocs; import com.tiki.server.document.dto.request.DocumentsCreateRequest; +import com.tiki.server.document.dto.response.DeletedDocumentsGetResponse; import com.tiki.server.document.dto.response.DocumentsCreateResponse; import com.tiki.server.document.dto.response.DocumentsGetResponse; import com.tiki.server.document.service.DocumentService; @@ -36,9 +38,9 @@ public class DocumentController implements DocumentControllerDocs { @Override @GetMapping("/documents/team/{teamId}/timeline") public ResponseEntity> getAllDocuments( - Principal principal, - @PathVariable long teamId, - @RequestParam String type + final Principal principal, + @PathVariable final long teamId, + @RequestParam final String type ) { long memberId = Long.parseLong(principal.getName()); DocumentsGetResponse response = documentService.getAllDocuments(memberId, teamId, type); @@ -48,35 +50,79 @@ public ResponseEntity> getAllDocuments( @Override @DeleteMapping("/documents/team/{teamId}/document/{documentId}") public ResponseEntity deleteDocument( - Principal principal, - @PathVariable long teamId, - @PathVariable long documentId + final Principal principal, + @PathVariable final long teamId, + @PathVariable final long documentId ) { long memberId = Long.parseLong(principal.getName()); documentService.deleteDocument(memberId, teamId, documentId); return ResponseEntity.noContent().build(); } - @PostMapping("/documents") - public ResponseEntity> createDocuments( - Principal principal, - @RequestHeader("team-id") long teamId, - @RequestBody DocumentsCreateRequest request + @PostMapping("/teams/{teamId}/documents") + public ResponseEntity> createDocuments( + final Principal principal, + @PathVariable final long teamId, + @RequestParam(required = false) final Long folderId, + @RequestBody final DocumentsCreateRequest request ) { long memberId = Long.parseLong(principal.getName()); - DocumentsCreateResponse response = documentService.createDocuments(memberId, teamId, request); - return ResponseEntity.created(UriGenerator.getUri("api/v1/documents")) - .body(SuccessResponse.success(SUCCESS_CREATE_DOCUMENTS.getMessage(), response)); + documentService.createDocuments(memberId, teamId, folderId, request); + return ResponseEntity.created(UriGenerator.getUri("teams/" + teamId + "/documents")) + .body(SuccessResponse.success(SUCCESS_CREATE_DOCUMENTS.getMessage())); } @GetMapping("/teams/{teamId}/documents") public ResponseEntity> getDocuments( final Principal principal, - @PathVariable long teamId, - @RequestParam(required = false) Long folderId + @PathVariable final long teamId, + @RequestParam(required = false) final Long folderId ) { long memberId = Long.parseLong(principal.getName()); DocumentsGetResponse response = documentService.get(memberId, teamId, folderId); return ResponseEntity.ok(SuccessResponse.success(SUCCESS_GET_DOCUMENTS.getMessage(), response)); } + + @DeleteMapping("/teams/{teamId}/documents") + public ResponseEntity delete( + final Principal principal, + @PathVariable final long teamId, + @RequestParam("documentId") final List documentIds + ) { + long memberId = Long.parseLong(principal.getName()); + documentService.delete(memberId, teamId, documentIds); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/teams/{teamId}/trash") + public ResponseEntity deleteTrash( + final Principal principal, + @PathVariable final long teamId, + @RequestParam("documentId") final List deletedDocumentIds + ) { + long memberId = Long.parseLong(principal.getName()); + documentService.deleteTrash(memberId, teamId, deletedDocumentIds); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/teams/{teamId}/trash") + public ResponseEntity restore( + final Principal principal, + @PathVariable final long teamId, + @RequestParam("documentId") final List deletedDocumentIds + ) { + long memberId = Long.parseLong(principal.getName()); + documentService.restore(memberId, teamId, deletedDocumentIds); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/teams/{teamId}/trash") + public ResponseEntity> getTrash( + final Principal principal, + @PathVariable final long teamId + ) { + long memberId = Long.parseLong(principal.getName()); + DeletedDocumentsGetResponse response = documentService.getTrash(memberId, teamId); + return ResponseEntity.ok(SuccessResponse.success(SUCCESS_GET_TRASH.getMessage(), response)); + } } diff --git a/src/main/java/com/tiki/server/document/dto/request/DocumentsCreateRequest.java b/src/main/java/com/tiki/server/document/dto/request/DocumentsCreateRequest.java index d417496f..91dcc8a8 100644 --- a/src/main/java/com/tiki/server/document/dto/request/DocumentsCreateRequest.java +++ b/src/main/java/com/tiki/server/document/dto/request/DocumentsCreateRequest.java @@ -3,7 +3,6 @@ import java.util.List; public record DocumentsCreateRequest( - List documents, - Long folderId + List documents ) { } diff --git a/src/main/java/com/tiki/server/document/dto/response/DeletedDocumentsGetResponse.java b/src/main/java/com/tiki/server/document/dto/response/DeletedDocumentsGetResponse.java new file mode 100644 index 00000000..cdae1fd8 --- /dev/null +++ b/src/main/java/com/tiki/server/document/dto/response/DeletedDocumentsGetResponse.java @@ -0,0 +1,40 @@ +package com.tiki.server.document.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import java.util.List; + +import com.tiki.server.document.entity.DeletedDocument; + +import lombok.Builder; +import lombok.NonNull; + +@Builder(access = PRIVATE) +public record DeletedDocumentsGetResponse( + List deletedDocuments +) { + + public static DeletedDocumentsGetResponse from(final List deletedDocuments) { + return DeletedDocumentsGetResponse.builder() + .deletedDocuments(deletedDocuments.stream().map(DeletedDocumentGetResponse::from).toList()) + .build(); + } + + @Builder(access = PRIVATE) + private record DeletedDocumentGetResponse( + long documentId, + @NonNull String name, + @NonNull String url, + double capacity + ) { + + private static DeletedDocumentGetResponse from(final DeletedDocument deletedDocument) { + return DeletedDocumentGetResponse.builder() + .documentId(deletedDocument.getId()) + .name(deletedDocument.getFileName()) + .url(deletedDocument.getFileUrl()) + .capacity(deletedDocument.getCapacity()) + .build(); + } + } +} diff --git a/src/main/java/com/tiki/server/document/entity/DeletedDocument.java b/src/main/java/com/tiki/server/document/entity/DeletedDocument.java index dcf7da07..5f1b352f 100644 --- a/src/main/java/com/tiki/server/document/entity/DeletedDocument.java +++ b/src/main/java/com/tiki/server/document/entity/DeletedDocument.java @@ -1,21 +1,26 @@ package com.tiki.server.document.entity; import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PRIVATE; +import static lombok.AccessLevel.PROTECTED; -import java.time.LocalDate; +import com.tiki.server.common.entity.BaseTime; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor -public class DeletedDocument { +@NoArgsConstructor(access = PROTECTED) +@Builder(access = PRIVATE) +@AllArgsConstructor(access = PRIVATE) +public class DeletedDocument extends BaseTime { @Id @GeneratedValue(strategy = IDENTITY) @@ -26,18 +31,17 @@ public class DeletedDocument { private String fileUrl; - @Column(name = "block_id") - private long timeBlockId; + private long teamId; - private LocalDate deletedDate; + private double capacity; - @Builder - public static DeletedDocument of(String fileName, String fileUrl, long timeBlockId, LocalDate deletedDate) { + public static DeletedDocument of(final String fileName, final String fileUrl, final long teamId, + final double capacity) { return DeletedDocument.builder() .fileName(fileName) .fileUrl(fileUrl) - .timeBlockId(timeBlockId) - .deletedDate(deletedDate) + .teamId(teamId) + .capacity(capacity) .build(); } } diff --git a/src/main/java/com/tiki/server/document/entity/Document.java b/src/main/java/com/tiki/server/document/entity/Document.java index 6af381b5..814e8326 100644 --- a/src/main/java/com/tiki/server/document/entity/Document.java +++ b/src/main/java/com/tiki/server/document/entity/Document.java @@ -45,7 +45,7 @@ public class Document extends BaseTime { @JoinColumn(name = "block_id") private TimeBlock timeBlock; - public static Document of(String fileName, String fileUrl, TimeBlock timeBlock) { + public static Document of(final String fileName, final String fileUrl, final TimeBlock timeBlock) { return Document.builder() .fileName(fileName) .fileUrl(fileUrl) @@ -56,7 +56,8 @@ public static Document of(String fileName, String fileUrl, TimeBlock timeBlock) .build(); } - public static Document of(String fileName, String fileUrl, double capacity, long teamId, Long folderId) { + public static Document of(final String fileName, final String fileUrl, + final double capacity, final long teamId, final Long folderId) { return Document.builder() .fileName(fileName) .fileUrl(fileUrl) @@ -66,4 +67,16 @@ public static Document of(String fileName, String fileUrl, double capacity, long .timeBlock(null) // TODO : 타임 블록 생성 api 수정 후 제거 예정 .build(); } + + public static Document restore(final String fileName, final String fileUrl, + final double capacity, final long teamId) { + return Document.builder() + .fileName(fileName) + .fileUrl(fileUrl) + .capacity(capacity) + .teamId(teamId) + .folderId(null) + .timeBlock(null) + .build(); + } } diff --git a/src/main/java/com/tiki/server/document/message/ErrorCode.java b/src/main/java/com/tiki/server/document/message/ErrorCode.java index c78424b2..3a310735 100644 --- a/src/main/java/com/tiki/server/document/message/ErrorCode.java +++ b/src/main/java/com/tiki/server/document/message/ErrorCode.java @@ -1,6 +1,7 @@ package com.tiki.server.document.message; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.NOT_FOUND; @@ -20,7 +21,10 @@ public enum ErrorCode { INVALID_AUTHORIZATION(FORBIDDEN, "문서에 대한 권한이 없습니다."), /* 404 NOT_FOUND : 자원을 찾을 수 없음 */ - INVALID_DOCUMENT(NOT_FOUND, "유효하지 않은 문서입니다."); + INVALID_DOCUMENT(NOT_FOUND, "유효하지 않은 문서입니다."), + + /* 409 CONFLICT : 중복된 자원 */ + DOCUMENT_NAME_DUPLICATE(CONFLICT, "중복된 파일 이름입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/tiki/server/document/message/SuccessMessage.java b/src/main/java/com/tiki/server/document/message/SuccessMessage.java index 78355728..315fceee 100644 --- a/src/main/java/com/tiki/server/document/message/SuccessMessage.java +++ b/src/main/java/com/tiki/server/document/message/SuccessMessage.java @@ -8,7 +8,8 @@ public enum SuccessMessage { SUCCESS_CREATE_DOCUMENTS("파일 생성 성공"), - SUCCESS_GET_DOCUMENTS("전체 문서 조회 성공"); + SUCCESS_GET_DOCUMENTS("전체 문서 조회 성공"), + SUCCESS_GET_TRASH("휴지통 조회 성공"); private final String message; } diff --git a/src/main/java/com/tiki/server/document/repository/DeletedDocumentRepository.java b/src/main/java/com/tiki/server/document/repository/DeletedDocumentRepository.java new file mode 100644 index 00000000..f978ba57 --- /dev/null +++ b/src/main/java/com/tiki/server/document/repository/DeletedDocumentRepository.java @@ -0,0 +1,15 @@ +package com.tiki.server.document.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.tiki.server.document.entity.DeletedDocument; + +public interface DeletedDocumentRepository extends JpaRepository { + + Optional findByIdAndTeamId(long id, long teamId); + + List findAllByTeamId(long teamId); +} diff --git a/src/main/java/com/tiki/server/document/repository/DocumentRepository.java b/src/main/java/com/tiki/server/document/repository/DocumentRepository.java index 9704be04..63c4dfc2 100644 --- a/src/main/java/com/tiki/server/document/repository/DocumentRepository.java +++ b/src/main/java/com/tiki/server/document/repository/DocumentRepository.java @@ -1,6 +1,7 @@ package com.tiki.server.document.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -9,8 +10,11 @@ import com.tiki.server.document.entity.Document; public interface DocumentRepository extends JpaRepository { + List findAllByTimeBlockId(long timeBlockId); + List findAllByFolderId(long folderId); + @Query("select d from Document d join fetch d.timeBlock t " + "where t.team.id = :teamId and t.accessiblePosition = :position order by d.createdAt asc") List findAllByTeamIdAndAccessiblePosition(long teamId, Position position); @@ -24,4 +28,6 @@ public interface DocumentRepository extends JpaRepository { void deleteAllByTimeBlockId(long timeBlockId); List findAllByTeamIdAndFolderIdOrderByCreatedAtDesc(long teamId, Long folderId); + + Optional findByIdAndTeamId(long id, long teamId); } diff --git a/src/main/java/com/tiki/server/document/service/DocumentService.java b/src/main/java/com/tiki/server/document/service/DocumentService.java index 7d10c34c..2acadc30 100644 --- a/src/main/java/com/tiki/server/document/service/DocumentService.java +++ b/src/main/java/com/tiki/server/document/service/DocumentService.java @@ -1,20 +1,26 @@ package com.tiki.server.document.service; +import static com.tiki.server.document.message.ErrorCode.DOCUMENT_NAME_DUPLICATE; + import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.tiki.server.common.entity.Position; +import com.tiki.server.document.adapter.DeletedDocumentAdapter; import com.tiki.server.document.adapter.DocumentDeleter; import com.tiki.server.document.adapter.DocumentFinder; import com.tiki.server.document.adapter.DocumentSaver; import com.tiki.server.document.dto.request.DocumentCreateRequest; import com.tiki.server.document.dto.request.DocumentsCreateRequest; -import com.tiki.server.document.dto.response.DocumentsCreateResponse; +import com.tiki.server.document.dto.response.DeletedDocumentsGetResponse; import com.tiki.server.document.dto.response.DocumentsGetResponse; +import com.tiki.server.document.entity.DeletedDocument; import com.tiki.server.document.entity.Document; +import com.tiki.server.document.exception.DocumentException; import com.tiki.server.folder.adapter.FolderFinder; +import com.tiki.server.folder.entity.Folder; import com.tiki.server.memberteammanager.adapter.MemberTeamManagerFinder; import com.tiki.server.memberteammanager.entity.MemberTeamManager; @@ -30,8 +36,9 @@ public class DocumentService { private final DocumentDeleter documentDeleter; private final FolderFinder folderFinder; private final MemberTeamManagerFinder memberTeamManagerFinder; + private final DeletedDocumentAdapter deletedDocumentAdapter; - public DocumentsGetResponse getAllDocuments(long memberId, long teamId, String type) { + public DocumentsGetResponse getAllDocuments(final long memberId, final long teamId, final String type) { MemberTeamManager memberTeamManager = memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); Position accessiblePosition = Position.getAccessiblePosition(type); memberTeamManager.checkMemberAccessible(accessiblePosition); @@ -39,7 +46,7 @@ public DocumentsGetResponse getAllDocuments(long memberId, long teamId, String t } @Transactional - public void deleteDocument(long memberId, long teamId, long documentId) { + public void deleteDocument(final long memberId, final long teamId, final long documentId) { MemberTeamManager memberTeamManager = memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); Document document = documentFinder.findByIdWithTimeBlock(documentId); memberTeamManager.checkMemberAccessible(document.getTimeBlock().getAccessiblePosition()); @@ -47,36 +54,80 @@ public void deleteDocument(long memberId, long teamId, long documentId) { } @Transactional - public DocumentsCreateResponse createDocuments(long memberId, long teamId, DocumentsCreateRequest request) { + public void createDocuments(final long memberId, final long teamId, + final Long folderId, final DocumentsCreateRequest request) { memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); - checkFolderIsExist(request.folderId()); - List documentIds = request.documents().stream() - .map(document -> saveDocument(teamId, request.folderId(), document).getId()) - .toList(); - return DocumentsCreateResponse.from(documentIds); + validateFolder(folderId, teamId); + validateFileName(folderId, teamId, request); + saveDocuments(teamId, folderId, request); } public DocumentsGetResponse get(final long memberId, final long teamId, final Long folderId) { - memberTeamManagerFinder.findByMemberIdAndTeamId(memberId, teamId); + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); List documents = documentFinder.findByTeamIdAndFolderId(teamId, folderId); return DocumentsGetResponse.from(documents); } - private DocumentsGetResponse getAllDocumentsByType(long teamId, Position accessiblePosition) { + @Transactional + public void delete(final long memberId, final long teamId, final List documentIds) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + List documents = documentFinder.findAllByIdAndTeamId(documentIds, teamId); + deletedDocumentAdapter.save(documents); + documentDeleter.deleteAll(documents); + } + + @Transactional + public void deleteTrash(final long memberId, final long teamId, final List documentIds) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + List deletedDocuments = deletedDocumentAdapter.get(documentIds, teamId); + deletedDocumentAdapter.deleteAll(deletedDocuments); + } + + @Transactional + public void restore(final long memberId, final long teamId, final List documentIds) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + List deletedDocuments = deletedDocumentAdapter.get(documentIds, teamId); + documentSaver.restore(deletedDocuments); + deletedDocumentAdapter.deleteAll(deletedDocuments); + } + + public DeletedDocumentsGetResponse getTrash(final long memberId, final long teamId) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + List deletedDocuments = deletedDocumentAdapter.get(teamId); + return DeletedDocumentsGetResponse.from(deletedDocuments); + } + + private DocumentsGetResponse getAllDocumentsByType(final long teamId, final Position accessiblePosition) { List documents = documentFinder.findAllByTeamIdAndAccessiblePosition(teamId, accessiblePosition); return DocumentsGetResponse.from(documents); } - private void checkFolderIsExist(Long folderId) { + private void validateFolder(final Long folderId, final long teamId) { if (folderId == null) { return; } - folderFinder.findById(folderId); + Folder folder = folderFinder.findById(folderId); + folder.validateTeamId(teamId); + } + + private void validateFileName(final Long folderId, final long teamId, final DocumentsCreateRequest request) { + List documents = documentFinder.findByTeamIdAndFolderId(teamId, folderId); + documents.forEach(document -> checkFileNameIsDuplicated(document.getFileName(), request)); + } + + private void checkFileNameIsDuplicated(final String fileName, final DocumentsCreateRequest request) { + if (request.documents().stream().anyMatch(document -> document.fileName().equals(fileName))) { + throw new DocumentException(DOCUMENT_NAME_DUPLICATE); + } + } + + private void saveDocuments(final long teamId, final Long folderId, final DocumentsCreateRequest request) { + request.documents().forEach(document -> saveDocument(teamId, folderId, document)); } - private Document saveDocument(long teamId, Long folderId, DocumentCreateRequest request) { + private void saveDocument(final long teamId, final Long folderId, final DocumentCreateRequest request) { Document document = Document.of( request.fileName(), request.fileUrl(), request.capacity(), teamId, folderId); - return documentSaver.save(document); + documentSaver.save(document); } } diff --git a/src/main/java/com/tiki/server/folder/adapter/FolderDeleter.java b/src/main/java/com/tiki/server/folder/adapter/FolderDeleter.java new file mode 100644 index 00000000..aecb7c68 --- /dev/null +++ b/src/main/java/com/tiki/server/folder/adapter/FolderDeleter.java @@ -0,0 +1,20 @@ +package com.tiki.server.folder.adapter; + +import java.util.List; + +import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.folder.entity.Folder; +import com.tiki.server.folder.repository.FolderRepository; + +import lombok.RequiredArgsConstructor; + +@RepositoryAdapter +@RequiredArgsConstructor +public class FolderDeleter { + + private final FolderRepository folderRepository; + + public void deleteAll(final List folders) { + folderRepository.deleteAll(folders); + } +} diff --git a/src/main/java/com/tiki/server/folder/adapter/FolderFinder.java b/src/main/java/com/tiki/server/folder/adapter/FolderFinder.java index d74576ae..5322af1c 100644 --- a/src/main/java/com/tiki/server/folder/adapter/FolderFinder.java +++ b/src/main/java/com/tiki/server/folder/adapter/FolderFinder.java @@ -18,7 +18,7 @@ public class FolderFinder { private final FolderRepository folderRepository; - public Folder findById(long id) { + public Folder findById(final long id) { return folderRepository.findById(id) .orElseThrow(() -> new FolderException(INVALID_FOLDER)); } @@ -29,4 +29,19 @@ public List findByTeamIdAndPath(final long teamId, final String path) { } return folderRepository.findAllByPathOrderByCreatedAtDesc(path); } + + public List findAllById(final List folderIds, final long teamId) { + return folderIds.stream() + .map(id -> findByIdAndTeamId(id, teamId)) + .toList(); + } + + public List findAllStartWithPath(final String path) { + return folderRepository.findAllByPathStartsWith(path); + } + + private Folder findByIdAndTeamId(final long id, final long teamId) { + return folderRepository.findByIdAndTeamId(id, teamId) + .orElseThrow(() -> new FolderException(INVALID_FOLDER)); + } } diff --git a/src/main/java/com/tiki/server/folder/adapter/FolderSaver.java b/src/main/java/com/tiki/server/folder/adapter/FolderSaver.java index c00db2b5..f2a6b28b 100644 --- a/src/main/java/com/tiki/server/folder/adapter/FolderSaver.java +++ b/src/main/java/com/tiki/server/folder/adapter/FolderSaver.java @@ -12,7 +12,7 @@ public class FolderSaver { private final FolderRepository folderRepository; - public Folder save(Folder folder) { + public Folder save(final Folder folder) { return folderRepository.save(folder); } } diff --git a/src/main/java/com/tiki/server/folder/constant/Constant.java b/src/main/java/com/tiki/server/folder/constant/Constant.java index c6b2f0a9..e3f536e5 100644 --- a/src/main/java/com/tiki/server/folder/constant/Constant.java +++ b/src/main/java/com/tiki/server/folder/constant/Constant.java @@ -3,4 +3,5 @@ public class Constant { public static final String ROOT_PATH = ""; + public static final String SEPARATOR = "/"; } diff --git a/src/main/java/com/tiki/server/folder/controller/FolderController.java b/src/main/java/com/tiki/server/folder/controller/FolderController.java index 0f70b546..5d5c4c0e 100644 --- a/src/main/java/com/tiki/server/folder/controller/FolderController.java +++ b/src/main/java/com/tiki/server/folder/controller/FolderController.java @@ -1,17 +1,18 @@ package com.tiki.server.folder.controller; import static com.tiki.server.common.dto.SuccessResponse.*; -import static com.tiki.server.folder.constant.Constant.ROOT_PATH; import static com.tiki.server.folder.message.SuccessMessage.SUCCESS_CREATE_FOLDER; import static com.tiki.server.folder.message.SuccessMessage.SUCCESS_GET_FOLDERS; import java.security.Principal; +import java.util.List; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -27,31 +28,44 @@ @RestController @RequiredArgsConstructor -@RequestMapping("api/v1/folders") +@RequestMapping("api/v1") public class FolderController { private final FolderService folderService; - @GetMapping() + @GetMapping("/teams/{teamId}/folders") public ResponseEntity> getFolders( final Principal principal, - @RequestHeader("team-id") long teamId, - @RequestParam(defaultValue = ROOT_PATH) String path + @PathVariable final long teamId, + @RequestParam(required = false) final Long folderId ) { long memberId = Long.parseLong(principal.getName()); - FoldersGetResponse response = folderService.get(memberId, teamId, path); + FoldersGetResponse response = folderService.get(memberId, teamId, folderId); return ResponseEntity.ok(success(SUCCESS_GET_FOLDERS.getMessage(), response)); } - @PostMapping() + @PostMapping("/teams/{teamId}/folders") public ResponseEntity> createFolder( Principal principal, - @RequestHeader("team-id") long teamId, - @RequestBody FolderCreateRequest request + @PathVariable final long teamId, + @RequestParam(required = false) final Long folderId, + @RequestBody final FolderCreateRequest request ) { long memberId = Long.parseLong(principal.getName()); - FolderCreateResponse response = folderService.create(memberId, teamId, request); - return ResponseEntity.created(UriGenerator.getUri("api/v1/folders/" + response.folderId())) - .body(success(SUCCESS_CREATE_FOLDER.getMessage(), response)); + FolderCreateResponse response = folderService.create(memberId, teamId, folderId, request); + return ResponseEntity.created( + UriGenerator.getUri("api/v1/teams/" + teamId + "/folders/" + response.folderId())) + .body(success(SUCCESS_CREATE_FOLDER.getMessage(), response)); + } + + @DeleteMapping("/teams/{teamId}/folders") + public ResponseEntity delete( + final Principal principal, + @PathVariable final long teamId, + @RequestParam("folderId") final List folderIds + ) { + long memberId = Long.parseLong(principal.getName()); + folderService.delete(memberId, teamId, folderIds); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/tiki/server/folder/dto/request/FolderCreateRequest.java b/src/main/java/com/tiki/server/folder/dto/request/FolderCreateRequest.java index af4aa4f8..799af52f 100644 --- a/src/main/java/com/tiki/server/folder/dto/request/FolderCreateRequest.java +++ b/src/main/java/com/tiki/server/folder/dto/request/FolderCreateRequest.java @@ -3,7 +3,6 @@ import lombok.NonNull; public record FolderCreateRequest( - @NonNull String name, - Long parentId + @NonNull String name ) { } diff --git a/src/main/java/com/tiki/server/folder/entity/Folder.java b/src/main/java/com/tiki/server/folder/entity/Folder.java index 21145e84..b7f35a1a 100644 --- a/src/main/java/com/tiki/server/folder/entity/Folder.java +++ b/src/main/java/com/tiki/server/folder/entity/Folder.java @@ -1,8 +1,12 @@ package com.tiki.server.folder.entity; +import static com.tiki.server.document.message.ErrorCode.INVALID_AUTHORIZATION; +import static com.tiki.server.folder.constant.Constant.ROOT_PATH; +import static com.tiki.server.folder.constant.Constant.SEPARATOR; import static jakarta.persistence.GenerationType.IDENTITY; import com.tiki.server.common.entity.BaseTime; +import com.tiki.server.document.exception.DocumentException; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -25,16 +29,26 @@ public class Folder extends BaseTime { private long teamId; - public Folder(String name, Folder parentFolder, long teamId) { + public Folder(final String name, final Folder parentFolder, final long teamId) { this.name = name; this.path = generatePath(parentFolder); this.teamId = teamId; } - private String generatePath(Folder parentFolder) { + public void validateTeamId(final long teamId) { + if (this.teamId != teamId) { + throw new DocumentException(INVALID_AUTHORIZATION); + } + } + + public String getChildPath() { + return path + SEPARATOR + id; + } + + private String generatePath(final Folder parentFolder) { if (parentFolder == null) { - return ""; + return ROOT_PATH; } - return parentFolder.getPath() + "/" + parentFolder.getId(); + return parentFolder.getPath() + SEPARATOR + parentFolder.getId(); } } diff --git a/src/main/java/com/tiki/server/folder/message/ErrorCode.java b/src/main/java/com/tiki/server/folder/message/ErrorCode.java index 2ffa4988..57bd9afe 100644 --- a/src/main/java/com/tiki/server/folder/message/ErrorCode.java +++ b/src/main/java/com/tiki/server/folder/message/ErrorCode.java @@ -1,5 +1,6 @@ package com.tiki.server.folder.message; +import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.NOT_FOUND; import org.springframework.http.HttpStatus; @@ -12,7 +13,10 @@ public enum ErrorCode { /* 404 NOT_FOUND : 자원을 찾을 수 없음 */ - INVALID_FOLDER(NOT_FOUND, "유효하지 않은 폴더입니다."); + INVALID_FOLDER(NOT_FOUND, "유효하지 않은 폴더입니다."), + + /* 409 CONFLICT : 중복된 자원 */ + FOLDER_NAME_DUPLICATE(CONFLICT, "중복된 폴더 이름입니다.");; private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/tiki/server/folder/repository/FolderRepository.java b/src/main/java/com/tiki/server/folder/repository/FolderRepository.java index 34a11781..beb0c697 100644 --- a/src/main/java/com/tiki/server/folder/repository/FolderRepository.java +++ b/src/main/java/com/tiki/server/folder/repository/FolderRepository.java @@ -1,12 +1,19 @@ package com.tiki.server.folder.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.tiki.server.folder.entity.Folder; public interface FolderRepository extends JpaRepository { + List findAllByTeamIdAndPathOrderByCreatedAtDesc(long teamId, String path); + List findAllByPathOrderByCreatedAtDesc(String path); + + Optional findByIdAndTeamId(long id, long teamId); + + List findAllByPathStartsWith(String path); } diff --git a/src/main/java/com/tiki/server/folder/service/FolderService.java b/src/main/java/com/tiki/server/folder/service/FolderService.java index 8358976c..2be875a0 100644 --- a/src/main/java/com/tiki/server/folder/service/FolderService.java +++ b/src/main/java/com/tiki/server/folder/service/FolderService.java @@ -1,16 +1,25 @@ package com.tiki.server.folder.service; +import static com.tiki.server.folder.constant.Constant.ROOT_PATH; +import static com.tiki.server.folder.message.ErrorCode.FOLDER_NAME_DUPLICATE; + import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.tiki.server.document.adapter.DeletedDocumentAdapter; +import com.tiki.server.document.adapter.DocumentDeleter; +import com.tiki.server.document.adapter.DocumentFinder; +import com.tiki.server.document.entity.Document; +import com.tiki.server.folder.adapter.FolderDeleter; import com.tiki.server.folder.adapter.FolderFinder; import com.tiki.server.folder.adapter.FolderSaver; import com.tiki.server.folder.dto.request.FolderCreateRequest; import com.tiki.server.folder.dto.response.FolderCreateResponse; import com.tiki.server.folder.dto.response.FoldersGetResponse; import com.tiki.server.folder.entity.Folder; +import com.tiki.server.folder.exception.FolderException; import com.tiki.server.memberteammanager.adapter.MemberTeamManagerFinder; import lombok.RequiredArgsConstructor; @@ -23,26 +32,76 @@ public class FolderService { private final FolderFinder folderFinder; private final FolderSaver folderSaver; private final MemberTeamManagerFinder memberTeamManagerFinder; + private final DocumentFinder documentFinder; + private final DocumentDeleter documentDeleter; + private final DeletedDocumentAdapter deletedDocumentAdapter; + private final FolderDeleter folderDeleter; - public FoldersGetResponse get(final long memberId, final long teamId, final String path) { - memberTeamManagerFinder.findByMemberIdAndTeamId(memberId, teamId); + public FoldersGetResponse get(final long memberId, final long teamId, + final Long folderId) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + Folder folder = getFolder(teamId, folderId); + String path = getChildFolderPath(folder); List folders = folderFinder.findByTeamIdAndPath(teamId, path); return FoldersGetResponse.from(folders); } @Transactional - public FolderCreateResponse create(long memberId, long teamId, FolderCreateRequest request) { - // 같은 레벨 파일명 중복 방지 로직 추가 필요 + public FolderCreateResponse create(final long memberId, final long teamId, + final Long folderId, final FolderCreateRequest request) { memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); - Folder parentFolder = getFolder(request.parentId()); + Folder parentFolder = getFolder(teamId, folderId); + String path = getChildFolderPath(parentFolder); + validateFolderName(teamId, path, request); Folder folder = folderSaver.save(new Folder(request.name(), parentFolder, teamId)); return FolderCreateResponse.from(folder.getId()); } - private Folder getFolder(Long folderId) { + @Transactional + public void delete(final long memberId, final long teamId, final List folderIds) { + memberTeamManagerFinder.findByMemberIdAndTeamIdOrElseThrow(memberId, teamId); + List folders = folderFinder.findAllById(folderIds, teamId); + deleteFolders(folders); + } + + private Folder getFolder(final long teamId, final Long folderId) { if (folderId == null) { return null; } - return folderFinder.findById(folderId); + Folder folder = folderFinder.findById(folderId); + folder.validateTeamId(teamId); + return folder; + } + + private String getChildFolderPath(final Folder folder) { + if (folder == null) { + return ROOT_PATH; + } + return folder.getChildPath(); + } + + private void validateFolderName(final long teamId, final String path, final FolderCreateRequest request) { + List folders = folderFinder.findByTeamIdAndPath(teamId, path); + if (folders.stream().anyMatch(folder -> folder.getName().equals(request.name()))) { + throw new FolderException(FOLDER_NAME_DUPLICATE); + } + } + + private void deleteFolders(final List folders) { + folders.forEach(this::deleteChildFolders); + folders.forEach(this::deleteDocuments); + folderDeleter.deleteAll(folders); + } + + private void deleteChildFolders(final Folder folder) { + List childFolders = folderFinder.findAllStartWithPath(folder.getChildPath()); + childFolders.forEach(this::deleteDocuments); + folderDeleter.deleteAll(childFolders); + } + + private void deleteDocuments(final Folder folder) { + List documents = documentFinder.findAllByFolderId(folder.getId()); + deletedDocumentAdapter.save(documents); + documentDeleter.deleteAll(documents); } }