Feat: 폴더 구현- S11P21S002-38

This commit is contained in:
kimtaesoo7 2024-08-30 14:31:31 +09:00
parent 31ac845933
commit d11bd157ba
10 changed files with 354 additions and 6 deletions

View File

@ -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<FolderResponse> 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<FolderResponse> 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<FolderResponse> 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<Void> 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();
}
}

View File

@ -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,12 +26,12 @@ 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;
/**
@ -36,10 +39,18 @@ public class Folder extends BaseEntity {
* 없다면 최상위 폴더
*/
@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<Image> 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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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<ImageResponse> images;
@Schema(description = "하위 폴더 목록")
private List<FolderIdResponse> children;
public static FolderResponse from(final Folder folder) {
List<ImageResponse> images = folder.getImageList().stream()
.map(ImageResponse::from)
.toList();
List<FolderIdResponse> children = folder.getChildren().stream()
.map(FolderIdResponse::from)
.toList();
return new FolderResponse(
folder.getId(),
folder.getTitle(),
images,
children
);
}
public static FolderResponse from(final List<Folder> topFolders) {
List<FolderIdResponse> list = topFolders.stream()
.map(FolderIdResponse::from)
.toList();
return new FolderResponse(0, "root", List.of(), list);
}
}

View File

@ -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()
);
}
}

View File

@ -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<Folder, Integer> {
List<Folder> findAllByParentIsNull();
}

View File

@ -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);
}
}
}

View File

@ -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<Participant, Intege
boolean existsByMemberIdAndProjectId(Integer memberId, Integer projectId);
boolean existsByProjectIdAndMemberIdAndPrivilege(Integer projectId, Integer memberId, PrivilegeType privilege);
@Query("SELECT NOT EXISTS (" +
"SELECT 1 FROM Participant p " +
"WHERE p.project.id = :projectId " +
"AND p.member.id = :memberId " +
"AND (p.privilege = 'ADMIN' OR p.privilege = 'EDITOR'))")
boolean doesParticipantUnauthorizedExistByMemberIdAndProjectId(
@Param("memberId") Integer memberId,
@Param("projectId") Integer projectId);
}

View File

@ -38,7 +38,9 @@ public enum ErrorCode {
// Participant - 5000,
PARTICIPANT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 5000, "해당 프로젝트에 접근 권한이 없습니다."),
// Folder - 600,
FOLDER_NOT_FOUND(HttpStatus.NOT_FOUND, 6000, "해당 폴더를 찾을 수 없습니다."),
FOLDER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 6001, "해당 폴더에 접근 권한이 없습니다."),
;
private final HttpStatus status;