From d11bd157bae420d4c7c3f07f5b74c0436c369c65 Mon Sep 17 00:00:00 2001 From: kimtaesoo7 Date: Fri, 30 Aug 2024 14:31:31 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?-=20S11P21S002-38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../folder/controller/FolderController.java | 74 ++++++++++++ .../worlabel/domain/folder/entity/Folder.java | 36 +++++- .../folder/entity/dto/FolderIdResponse.java | 22 ++++ .../folder/entity/dto/FolderRequest.java | 19 ++++ .../folder/entity/dto/FolderResponse.java | 51 +++++++++ .../folder/entity/dto/ImageResponse.java | 25 +++++ .../folder/repository/FolderRepository.java | 13 +++ .../domain/folder/service/FolderService.java | 105 ++++++++++++++++++ .../repository/ParticipantRepository.java | 11 ++ .../worlabel/global/exception/ErrorCode.java | 4 +- 10 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/com/worlabel/domain/folder/controller/FolderController.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderIdResponse.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderRequest.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderResponse.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/entity/dto/ImageResponse.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java create mode 100644 backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java diff --git a/backend/src/main/java/com/worlabel/domain/folder/controller/FolderController.java b/backend/src/main/java/com/worlabel/domain/folder/controller/FolderController.java new file mode 100644 index 0000000..cc64d30 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/controller/FolderController.java @@ -0,0 +1,74 @@ +package com.worlabel.domain.folder.controller; + +import com.worlabel.domain.folder.entity.dto.FolderRequest; +import com.worlabel.domain.folder.entity.dto.FolderResponse; +import com.worlabel.domain.folder.service.FolderService; +import com.worlabel.global.annotation.CurrentUser; +import com.worlabel.global.config.swagger.SwaggerApiError; +import com.worlabel.global.config.swagger.SwaggerApiSuccess; +import com.worlabel.global.exception.ErrorCode; +import com.worlabel.global.response.BaseResponse; +import com.worlabel.global.response.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "폴더 관련 API") +@RestController +@RequestMapping("/api/projects/{project_id}/folders") +@RequiredArgsConstructor +public class FolderController { + + private final FolderService folderService; + + + @Operation(summary = "폴더 생성", description = "프로젝트에 폴더를 생성합니다.") + @SwaggerApiSuccess(description = "폴더를 성공적으로 생성합니다.") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @PostMapping + public BaseResponse createFolder( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId, + @RequestBody final FolderRequest folderRequest) { + FolderResponse folderResponse = folderService.createFolder(memberId, projectId, folderRequest); + return SuccessResponse.of(folderResponse); + } + + @Operation(summary = "폴더 조회", description = "폴더의 내용을 조회합니다.") + @SwaggerApiSuccess(description = "폴더를 성공적으로 조회합니다.") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @GetMapping("/{folder_id}") + public BaseResponse getFolderById( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId, + @PathVariable("folder_id") final Integer folderId) { + FolderResponse folderResponse = folderService.getFolderById(memberId, projectId, folderId); + return SuccessResponse.of(folderResponse); + } + + @Operation(summary = "폴더 수정", description = "폴더 정보를 수정합니다.") + @SwaggerApiSuccess(description = "폴더를 성공적으로 수정합니다.") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @PutMapping("/{folder_id}") + public BaseResponse updateFolder( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId, + @PathVariable("folder_id") final Integer folderId, + @RequestBody FolderRequest folderRequest) { + FolderResponse folderResponse = folderService.updateFolder(memberId, projectId, folderId, folderRequest); + return SuccessResponse.of(folderResponse); + } + + @Operation(summary = "폴더 삭제", description = "폴더를 삭제합니다.") + @SwaggerApiSuccess(description = "폴더를 성공적으로 삭제합니다.") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @DeleteMapping("/{folder_id}") + public BaseResponse deleteFolder( + @CurrentUser final Integer memberId, + @PathVariable("project_id") final Integer projectId, + @PathVariable("folder_id") final Integer folderId) { + folderService.deleteFolder(memberId, projectId, folderId); + return SuccessResponse.empty(); + } +} diff --git a/backend/src/main/java/com/worlabel/domain/folder/entity/Folder.java b/backend/src/main/java/com/worlabel/domain/folder/entity/Folder.java index 4a642c7..d7ef3b8 100644 --- a/backend/src/main/java/com/worlabel/domain/folder/entity/Folder.java +++ b/backend/src/main/java/com/worlabel/domain/folder/entity/Folder.java @@ -2,11 +2,14 @@ package com.worlabel.domain.folder.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import com.worlabel.domain.image.entity.Image; +import com.worlabel.domain.project.entity.Project; import com.worlabel.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.util.ArrayList; import java.util.List; @@ -23,23 +26,31 @@ public class Folder extends BaseEntity { @Id @Column(name = "folder_id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Integer id; /** * 폴더 이름 */ - @Column(name = "title",nullable = false) + @Column(name = "title", nullable = false) private String title; /** - * 상위 폴더 + * 상위 폴더 * 없다면 최상위 폴더 */ @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id") + @JoinColumn(name = "parent_id", nullable = true) @JsonIgnore private Folder parent; + /** + * 프로젝트 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Project project; + /** * 하위 폴더 리스트 */ @@ -49,6 +60,21 @@ public class Folder extends BaseEntity { /** * 폴더에 속한 이미지 */ - @OneToMany(mappedBy = "folder", fetch = FetchType.LAZY, cascade = CascadeType.ALL,orphanRemoval = true) + @OneToMany(mappedBy = "folder", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List imageList = new ArrayList<>(); + + public Folder(final String title, final Folder parent, final Project project) { + this.title = title; + this.parent = parent; + this.project = project; + } + + public static Folder of(final String title, final Folder parent, final Project project) { + return new Folder(title, parent, project); + } + + public void updateFolder(final String title, final Folder parent) { + this.title = title; + this.parent = parent; + } } diff --git a/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderIdResponse.java b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderIdResponse.java new file mode 100644 index 0000000..33287bf --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderIdResponse.java @@ -0,0 +1,22 @@ +package com.worlabel.domain.folder.entity.dto; + +import com.worlabel.domain.folder.entity.Folder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(name = "폴더 ID 응답 DTO", description = "하위 폴더의 ID만 포함하는 DTO") +@Getter +@AllArgsConstructor +public class FolderIdResponse { + + @Schema(description = "폴더 ID", example = "1") + private Integer id; + + @Schema(description = "폴더 이름", example = "car") + private String title; + + public static FolderIdResponse from(final Folder folder) { + return new FolderIdResponse(folder.getId(), folder.getTitle()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderRequest.java b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderRequest.java new file mode 100644 index 0000000..9ce5714 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderRequest.java @@ -0,0 +1,19 @@ +package com.worlabel.domain.folder.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(name = "폴더 요청 DTO", description = "폴더 생성 및 수정을 위한 요청 DTO") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class FolderRequest { + + @Schema(description = "폴더 이름", example = "My Folder") + private String title; + + @Schema(description = "상위 폴더 ID", example = "1") + private Integer parentId; +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderResponse.java b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderResponse.java new file mode 100644 index 0000000..14d2433 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/FolderResponse.java @@ -0,0 +1,51 @@ +package com.worlabel.domain.folder.entity.dto; + +import com.worlabel.domain.folder.entity.Folder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Schema(name = "폴더 응답 DTO", description = "폴더 조회 응답 DTO") +@Getter +@AllArgsConstructor +public class FolderResponse { + + @Schema(description = "폴더 ID", example = "1") + private Integer id; + + @Schema(description = "폴더 이름", example = "My Folder") + private String title; + + @Schema(description = "폴더에 속한 이미지 목록") + private List images; + + @Schema(description = "하위 폴더 목록") + private List children; + + public static FolderResponse from(final Folder folder) { + List images = folder.getImageList().stream() + .map(ImageResponse::from) + .toList(); + + List children = folder.getChildren().stream() + .map(FolderIdResponse::from) + .toList(); + + return new FolderResponse( + folder.getId(), + folder.getTitle(), + images, + children + ); + } + + public static FolderResponse from(final List topFolders) { + List list = topFolders.stream() + .map(FolderIdResponse::from) + .toList(); + + return new FolderResponse(0, "root", List.of(), list); + } +} diff --git a/backend/src/main/java/com/worlabel/domain/folder/entity/dto/ImageResponse.java b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/ImageResponse.java new file mode 100644 index 0000000..893c6c1 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/entity/dto/ImageResponse.java @@ -0,0 +1,25 @@ +package com.worlabel.domain.folder.entity.dto; + +import com.worlabel.domain.image.entity.Image; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(name = "이미지 응답 DTO", description = "폴더 내 이미지에 대한 응답 DTO") +@Getter +@AllArgsConstructor +public class ImageResponse { + + @Schema(description = "이미지 ID", example = "1") + private Long id; + + @Schema(description = "이미지 URL", example = "https://example.com/image.jpg") + private String imageUrl; + + public static ImageResponse from(final Image image) { + return new ImageResponse( + image.getId(), + image.getImageUrl() + ); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java new file mode 100644 index 0000000..072affc --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/repository/FolderRepository.java @@ -0,0 +1,13 @@ +package com.worlabel.domain.folder.repository; + +import com.worlabel.domain.folder.entity.Folder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FolderRepository extends JpaRepository { + + List findAllByParentIsNull(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java new file mode 100644 index 0000000..5a5ca4a --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/folder/service/FolderService.java @@ -0,0 +1,105 @@ +package com.worlabel.domain.folder.service; + +import com.worlabel.domain.folder.entity.Folder; +import com.worlabel.domain.folder.repository.FolderRepository; +import com.worlabel.domain.folder.entity.dto.FolderRequest; +import com.worlabel.domain.folder.entity.dto.FolderResponse; +import com.worlabel.domain.participant.entity.PrivilegeType; +import com.worlabel.domain.participant.repository.ParticipantRepository; +import com.worlabel.domain.project.entity.Project; +import com.worlabel.domain.project.repository.ProjectRepository; +import com.worlabel.global.exception.CustomException; +import com.worlabel.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class FolderService { + + private final FolderRepository folderRepository; + private final ProjectRepository projectRepository; + private final ParticipantRepository participantRepository; + + /** + * 폴더 생성 + */ + public FolderResponse createFolder(final Integer memberId, final Integer projectId, final FolderRequest folderRequest) { + checkUnauthorized(memberId, projectId); + + Project project = getProject(projectId); + + Folder parent = null; + if (folderRequest.getParentId() != 0) { + parent = getFolder(folderRequest.getParentId()); + } + + Folder folder = Folder.of(folderRequest.getTitle(), parent, project); + folderRepository.save(folder); + + return FolderResponse.from(folder); + } + + /** + * 폴더 조회 + */ + @Transactional(readOnly = true) + public FolderResponse getFolderById(final Integer memberId, final Integer projectId, final Integer folderId) { + checkExistParticipant(memberId, projectId); + + // 최상위 폴더 + if (folderId == 0) { + return FolderResponse.from(folderRepository.findAllByParentIsNull()); + } else { + return FolderResponse.from(getFolder(folderId)); + } + } + + /** + * 폴더 수정 + */ + public FolderResponse updateFolder(final Integer memberId, final Integer projectId, final Integer folderId, final FolderRequest updatedFolderRequest) { + checkUnauthorized(memberId, projectId); + Folder folder = getFolder(folderId); + + Folder parentFolder = folderRepository.findById(updatedFolderRequest.getParentId()) + .orElse(null); + + folder.updateFolder(updatedFolderRequest.getTitle(), parentFolder); + + return FolderResponse.from(folder); + } + + /** + * 폴더 삭제 + */ + public void deleteFolder(final Integer memberId, final Integer projectId, final Integer folderId) { + checkUnauthorized(memberId, projectId); + Folder folder = getFolder(folderId); + folderRepository.delete(folder); + } + + private Project getProject(final Integer projectId) { + return projectRepository.findById(projectId) + .orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND)); + } + + private Folder getFolder(final Integer folderId) { + return folderRepository.findById(folderId) + .orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND)); + } + + private void checkUnauthorized(final Integer memberId, final Integer projectId) { + if (participantRepository.doesParticipantUnauthorizedExistByMemberIdAndProjectId(memberId, projectId)) { + throw new CustomException(ErrorCode.FOLDER_UNAUTHORIZED); + } + } + + private void checkExistParticipant(final Integer memberId, final Integer projectId) { + if (participantRepository.existsByMemberIdAndProjectId(memberId, projectId)) { + throw new CustomException(ErrorCode.BAD_REQUEST); + } + } +} diff --git a/backend/src/main/java/com/worlabel/domain/participant/repository/ParticipantRepository.java b/backend/src/main/java/com/worlabel/domain/participant/repository/ParticipantRepository.java index 8f188ea..efd8c1f 100644 --- a/backend/src/main/java/com/worlabel/domain/participant/repository/ParticipantRepository.java +++ b/backend/src/main/java/com/worlabel/domain/participant/repository/ParticipantRepository.java @@ -3,6 +3,8 @@ package com.worlabel.domain.participant.repository; import com.worlabel.domain.participant.entity.Participant; import com.worlabel.domain.participant.entity.PrivilegeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -11,6 +13,15 @@ public interface ParticipantRepository extends JpaRepository