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/controller/DocumentController.java b/src/main/java/com/tiki/server/document/controller/DocumentController.java index 47be9c77..20e0a09c 100644 --- a/src/main/java/com/tiki/server/document/controller/DocumentController.java +++ b/src/main/java/com/tiki/server/document/controller/DocumentController.java @@ -11,7 +11,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; @@ -36,9 +35,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,32 +47,33 @@ 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); 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/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/service/DocumentService.java b/src/main/java/com/tiki/server/document/service/DocumentService.java index 7d10c34c..0f2271ab 100644 --- a/src/main/java/com/tiki/server/document/service/DocumentService.java +++ b/src/main/java/com/tiki/server/document/service/DocumentService.java @@ -1,5 +1,7 @@ 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; @@ -11,10 +13,11 @@ 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.DocumentsGetResponse; 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; @@ -31,7 +34,7 @@ public class DocumentService { private final FolderFinder folderFinder; private final MemberTeamManagerFinder memberTeamManagerFinder; - 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 +42,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 +50,53 @@ 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) { + 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); + for (Document document : documents) { + 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/FolderFinder.java b/src/main/java/com/tiki/server/folder/adapter/FolderFinder.java index d74576ae..698cd55b 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)); } 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..9c8602ff 100644 --- a/src/main/java/com/tiki/server/folder/controller/FolderController.java +++ b/src/main/java/com/tiki/server/folder/controller/FolderController.java @@ -9,9 +9,9 @@ import org.springframework.http.ResponseEntity; 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,30 +27,31 @@ @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); + FolderCreateResponse response = folderService.create(memberId, teamId, folderId, request); return ResponseEntity.created(UriGenerator.getUri("api/v1/folders/" + response.folderId())) .body(success(SUCCESS_CREATE_FOLDER.getMessage(), response)); } 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/service/FolderService.java b/src/main/java/com/tiki/server/folder/service/FolderService.java index 8358976c..21f27a77 100644 --- a/src/main/java/com/tiki/server/folder/service/FolderService.java +++ b/src/main/java/com/tiki/server/folder/service/FolderService.java @@ -1,5 +1,8 @@ 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; @@ -11,6 +14,7 @@ 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; @@ -24,25 +28,46 @@ public class FolderService { private final FolderSaver folderSaver; private final MemberTeamManagerFinder memberTeamManagerFinder; - 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) { + 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); + } } }