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.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.worlabel.domain.image.entity.Image;
|
import com.worlabel.domain.image.entity.Image;
|
||||||
|
import com.worlabel.domain.project.entity.Project;
|
||||||
import com.worlabel.global.common.BaseEntity;
|
import com.worlabel.global.common.BaseEntity;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.OnDelete;
|
||||||
|
import org.hibernate.annotations.OnDeleteAction;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -23,12 +26,12 @@ public class Folder extends BaseEntity {
|
|||||||
@Id
|
@Id
|
||||||
@Column(name = "folder_id", nullable = false)
|
@Column(name = "folder_id", nullable = false)
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Integer id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 폴더 이름
|
* 폴더 이름
|
||||||
*/
|
*/
|
||||||
@Column(name = "title",nullable = false)
|
@Column(name = "title", nullable = false)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +39,18 @@ public class Folder extends BaseEntity {
|
|||||||
* 없다면 최상위 폴더
|
* 없다면 최상위 폴더
|
||||||
*/
|
*/
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "parent_id")
|
@JoinColumn(name = "parent_id", nullable = true)
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Folder parent;
|
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<>();
|
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.Participant;
|
||||||
import com.worlabel.domain.participant.entity.PrivilegeType;
|
import com.worlabel.domain.participant.entity.PrivilegeType;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@ -11,6 +13,15 @@ public interface ParticipantRepository extends JpaRepository<Participant, Intege
|
|||||||
boolean existsByMemberIdAndProjectId(Integer memberId, Integer projectId);
|
boolean existsByMemberIdAndProjectId(Integer memberId, Integer projectId);
|
||||||
|
|
||||||
boolean existsByProjectIdAndMemberIdAndPrivilege(Integer projectId, Integer memberId, PrivilegeType privilege);
|
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 - 5000,
|
||||||
PARTICIPANT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 5000, "해당 프로젝트에 접근 권한이 없습니다."),
|
PARTICIPANT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 5000, "해당 프로젝트에 접근 권한이 없습니다."),
|
||||||
|
|
||||||
|
// Folder - 600,
|
||||||
|
FOLDER_NOT_FOUND(HttpStatus.NOT_FOUND, 6000, "해당 폴더를 찾을 수 없습니다."),
|
||||||
|
FOLDER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 6001, "해당 폴더에 접근 권한이 없습니다."),
|
||||||
;
|
;
|
||||||
|
|
||||||
private final HttpStatus status;
|
private final HttpStatus status;
|
||||||
|
Loading…
Reference in New Issue
Block a user