Feat: 폴더 구현- S11P21S002-38
This commit is contained in:
parent
31ac845933
commit
d11bd157ba
@ -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();
|
||||
}
|
||||
}
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user