Skip to content

Commit

Permalink
Merge pull request #41 from Na-o-man/feat/#40/delete-photo-api
Browse files Browse the repository at this point in the history
[FEAT] 특정 사진 삭제 API 구현
  • Loading branch information
bflykky authored Aug 1, 2024
2 parents 8dc8343 + 85f9a9a commit 445f22c
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package com.umc.naoman.domain.photo.controller;

import com.umc.naoman.domain.member.entity.Member;
import com.umc.naoman.domain.photo.converter.PhotoConverter;
import com.umc.naoman.domain.photo.dto.PhotoRequest;
import com.umc.naoman.domain.photo.dto.PhotoResponse;
import com.umc.naoman.domain.photo.entity.Photo;
import com.umc.naoman.domain.photo.service.PhotoService;
import com.umc.naoman.global.result.ResultResponse;
import com.umc.naoman.global.security.annotation.LoginMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -32,22 +35,30 @@ public class PhotoController {
private final PhotoConverter photoConverter;

@PostMapping("/preSignedUrl")
public ResultResponse<PhotoResponse.PreSignedUrlListInfo> getPreSignedUrlList(@Valid @RequestBody PhotoRequest.PreSignedUrlRequest request) {
List<PhotoResponse.PreSignedUrlInfo> preSignedUrlList = photoService.getPreSignedUrlList(request);
public ResultResponse<PhotoResponse.PreSignedUrlListInfo> getPreSignedUrlList(@Valid @RequestBody PhotoRequest.PreSignedUrlRequest request,
@LoginMember Member member) {
List<PhotoResponse.PreSignedUrlInfo> preSignedUrlList = photoService.getPreSignedUrlList(request, member);
return ResultResponse.of(CREATE_PRESIGNED_URL, photoConverter.toPreSignedUrlListInfo(preSignedUrlList));
}

@PostMapping("/upload")
public ResultResponse<PhotoResponse.PhotoUploadInfo> uploadPhotoList(@Valid @RequestBody PhotoRequest.PhotoUploadRequest request) {
PhotoResponse.PhotoUploadInfo photoUploadInfo = photoService.uploadPhotoList(request);
public ResultResponse<PhotoResponse.PhotoUploadInfo> uploadPhotoList(@Valid @RequestBody PhotoRequest.PhotoUploadRequest request,
@LoginMember Member member) {
PhotoResponse.PhotoUploadInfo photoUploadInfo = photoService.uploadPhotoList(request, member);
return ResultResponse.of(UPLOAD_PHOTO, photoUploadInfo);
}

@GetMapping("/all")
public ResultResponse<PhotoResponse.PagedPhotoInfo> getAllPhotoListByShareGroup(@RequestParam Long shareGroupId,
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<Photo> allPhotoListByShareGroup = photoService.getAllPhotoList(shareGroupId, pageable);
public ResultResponse<PhotoResponse.PagedPhotoInfo> getAllPhotoListByShareGroup(@RequestParam Long shareGroupId, @LoginMember Member member,
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<Photo> allPhotoListByShareGroup = photoService.getAllPhotoList(shareGroupId, member, pageable);
return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPhotoListInfo(allPhotoListByShareGroup));
}

@DeleteMapping
public ResultResponse<PhotoResponse.PhotoDeleteInfo> deletePhotoList(@Valid @RequestBody PhotoRequest.PhotoDeletedRequest request,
@LoginMember Member member) {
List<Photo> photoList = photoService.deletePhotoList(request, member);
return ResultResponse.of(DELETE_PHOTO, photoConverter.toPhotoDeleteInfo(photoList));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import java.util.List;
import java.util.stream.Collectors;

import static com.umc.naoman.domain.photo.service.PhotoServiceImpl.*;

@Component
public class PhotoConverter {

Expand Down Expand Up @@ -61,8 +63,8 @@ public PhotoResponse.PhotoInfo toPhotoInfo(Photo photo) {

return PhotoResponse.PhotoInfo.builder()
.rawPhotoUrl(rawUrl)
.w200PhotoUrl(createResizedPhotoUrl(rawUrl, "w200"))
.w400PhotoUrl(createResizedPhotoUrl(rawUrl, "w400"))
.w200PhotoUrl(createResizedPhotoUrl(rawUrl, W200_PATH_PREFIX))
.w400PhotoUrl(createResizedPhotoUrl(rawUrl, W400_PATH_PREFIX))
.createdAt(photo.getCreatedAt())
.build();
}
Expand All @@ -74,12 +76,22 @@ private String createResizedPhotoUrl(String photoUrl, String size) {

// 리사이즈된 사진의 URL로 변환하는 메서드
private String getResizedUrl(String photoUrl, String size) {
return photoUrl.replace("/raw/", "/" + size + "/");
return photoUrl.replace("/" + RAW_PATH_PREFIX + "/", "/" + size + "/");
}

// 확장자 변환 메서드
private String convertExtension(String photoUrl) {
public String convertExtension(String photoUrl) {
return photoUrl.replace(".HEIC", ".jpg");
}

public PhotoResponse.PhotoDeleteInfo toPhotoDeleteInfo(List<Photo> photoList) {
List<Long> photoIdList = photoList.stream()
.map(Photo::getId)
.collect(Collectors.toList());

return PhotoResponse.PhotoDeleteInfo.builder()
.photoIdList(photoIdList)
.build();
}

}
13 changes: 13 additions & 0 deletions src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public abstract class PhotoRequest {
@AllArgsConstructor
public static class PreSignedUrlRequest {

@NotNull(message = "공유 그룹의 아이디 값을 입력해야 합니다.")
private Long shareGroupId;
@NotEmpty(message = "사진의 이름은 하나 이상이어야 합니다.")
private List<String> photoNameList;

Expand All @@ -34,4 +36,15 @@ public static class PhotoUploadRequest {
private List<String> photoUrlList;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PhotoDeletedRequest {

@NotNull(message = "삭제할 사진이 존재하는 공유 그룹의 아이디 값을 입력해야 합니다.")
private Long shareGroupId;
@NotEmpty(message = "삭제할 사진을 하나 이상 선택해야 합니다.")
private List<Long> photoIdList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,13 @@ public static class PhotoInfo {
private String w400PhotoUrl;
private LocalDateTime createdAt;
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PhotoDeleteInfo {
private List<Long> photoIdList;
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.umc.naoman.domain.photo.repository;

import com.umc.naoman.domain.photo.entity.Photo;
import com.umc.naoman.domain.shareGroup.entity.ShareGroup;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PhotoRepository extends JpaRepository<Photo, Long> {
Page<Photo> findAllByShareGroupId(Long shareGroupId, Pageable pageable);
List<Photo> findByIdInAndShareGroupId(List<Long> photoIdList, Long shareGroupId);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.umc.naoman.domain.photo.service;

import com.umc.naoman.domain.member.entity.Member;
import com.umc.naoman.domain.photo.dto.PhotoRequest;
import com.umc.naoman.domain.photo.dto.PhotoResponse;
import com.umc.naoman.domain.photo.entity.Photo;
Expand All @@ -10,9 +11,11 @@

public interface PhotoService {

List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request);
List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member);

PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request);
PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request, Member member);

Page<Photo> getAllPhotoList(Long shareGroupId, Pageable pageable);
Page<Photo> getAllPhotoList(Long shareGroupId, Member member, Pageable pageable);

List<Photo> deletePhotoList(PhotoRequest.PhotoDeletedRequest request, Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.umc.naoman.domain.member.entity.Member;
import com.umc.naoman.domain.photo.converter.PhotoConverter;
import com.umc.naoman.domain.photo.dto.PhotoRequest;
import com.umc.naoman.domain.photo.dto.PhotoResponse;
import com.umc.naoman.domain.photo.entity.Photo;
import com.umc.naoman.domain.photo.repository.PhotoRepository;
import com.umc.naoman.domain.shareGroup.entity.Profile;
import com.umc.naoman.domain.shareGroup.entity.ShareGroup;
import com.umc.naoman.domain.shareGroup.repository.ProfileRepository;
import com.umc.naoman.domain.shareGroup.service.ShareGroupService;
import com.umc.naoman.global.error.BusinessException;
import io.awspring.cloud.s3.S3Template;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
Expand All @@ -23,61 +27,46 @@
import org.springframework.transaction.annotation.Transactional;

import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.umc.naoman.global.error.code.S3ErrorCode.PHOTO_NOT_FOUND_S3;
import static com.umc.naoman.global.error.code.S3ErrorCode.*;

@Service
@RequiredArgsConstructor
public class PhotoServiceImpl implements PhotoService {

private final AmazonS3 amazonS3;
private final S3Template s3Template;
private final PhotoRepository photoRepository;
private final ShareGroupService shareGroupService;
private final PhotoConverter photoConverter;
private final ProfileRepository profileRepository;

@Value("${spring.cloud.aws.s3.bucket}")
private String bucketName;

@Value("${spring.cloud.aws.region.static}")
private String region;

private static final String RAW_PATH_PREFIX = "raw";
public static final String RAW_PATH_PREFIX = "raw";
public static final String W200_PATH_PREFIX = "w200";
public static final String W400_PATH_PREFIX = "w400";

@Override
@Transactional
public List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request) {
public List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member) {
shareGroupService.findProfile(request.getShareGroupId(), member.getId());

return request.getPhotoNameList().stream()
.map(this::getPreSignedUrl)
.collect(Collectors.toList());
}

@Override
@Transactional
public PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request) {
ShareGroup shareGroup = shareGroupService.findShareGroup(request.getShareGroupId());
int uploadCount = 0;

for (String photoUrl : request.getPhotoUrlList()) {
String photoName = extractPhotoNameFromUrl(photoUrl);
if (checkAndSavePhoto(photoUrl, photoName, shareGroup)) {
uploadCount++;
}
}

return new PhotoResponse.PhotoUploadInfo(shareGroup.getId(), uploadCount);
}

@Override
@Transactional(readOnly = true)
public Page<Photo> getAllPhotoList(Long shareGroupId, Pageable pageable) {
ShareGroup shareGroup = shareGroupService.findShareGroup(shareGroupId);
return photoRepository.findAllByShareGroupId(shareGroup.getId(), pageable);
}

private PhotoResponse.PreSignedUrlInfo getPreSignedUrl(String originalFilename) {
String fileName = createPath(originalFilename);

Expand Down Expand Up @@ -123,6 +112,23 @@ private String generateFileAccessUrl(String fileName) {
return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName);
}

@Override
@Transactional
public PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request, Member member) {
shareGroupService.findProfile(member.getId(), request.getShareGroupId());
ShareGroup shareGroup = shareGroupService.findShareGroup(request.getShareGroupId());
int uploadCount = 0;

for (String photoUrl : request.getPhotoUrlList()) {
String photoName = extractPhotoNameFromUrl(photoUrl);
if (checkAndSavePhoto(photoUrl, photoName, shareGroup)) {
uploadCount++;
}
}

return new PhotoResponse.PhotoUploadInfo(shareGroup.getId(), uploadCount);
}

// 사진 URL에서 사진 이름을 추출하는 메서드
private String extractPhotoNameFromUrl(String photoUrl) {
int lastSlashIndex = photoUrl.lastIndexOf('/');
Expand All @@ -140,4 +146,46 @@ private boolean checkAndSavePhoto(String photoUrl, String photoName, ShareGroup
throw new BusinessException(PHOTO_NOT_FOUND_S3);
}
}

@Override
@Transactional(readOnly = true)
public Page<Photo> getAllPhotoList(Long shareGroupId, Member member, Pageable pageable) {
ShareGroup shareGroup = shareGroupService.findShareGroup(shareGroupId);
shareGroupService.findProfile(shareGroup.getId(), member.getId());

return photoRepository.findAllByShareGroupId(shareGroup.getId(), pageable);
}

@Override
@Transactional
public List<Photo> deletePhotoList(PhotoRequest.PhotoDeletedRequest request, Member member) {
// 멤버가 해당 공유 그룹에 대한 권한이 있는지 확인
shareGroupService.findProfile(member.getId(), request.getShareGroupId());

// 요청된 사진 ID 목록과 공유 그룹 ID를 기반으로 사진 목록 조회
List<Photo> photoList = photoRepository.findByIdInAndShareGroupId(request.getPhotoIdList(), request.getShareGroupId());

// 사진 목록 크기 검증
if (photoList.size() != request.getPhotoIdList().size()) {
throw new BusinessException(PHOTO_NOT_FOUND); // 요청한 사진이 일부 또는 전부 없을 경우 예외 발생
}

// 각 사진에 대해 S3에서 객체 삭제 및 데이터베이스에서 삭제
for (Photo photo : photoList) {
deletePhoto(photo);
}

return photoList; // 삭제된 사진 목록 반환
}

private void deletePhoto(Photo photo) {
// S3에서 원본 및 변환된 이미지 삭제
s3Template.deleteObject(bucketName, RAW_PATH_PREFIX + "/" + photo.getName());
s3Template.deleteObject(bucketName, W200_PATH_PREFIX + "/" + photoConverter.convertExtension(photo.getName()));
s3Template.deleteObject(bucketName, W400_PATH_PREFIX + "/" + photoConverter.convertExtension(photo.getName()));

// 데이터베이스에서 사진 삭제
photoRepository.delete(photo);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface ProfileRepository extends JpaRepository<Profile, Long> {
List<Profile> findByShareGroupId(Long shareGroupId);
Optional<Profile> findByShareGroupIdAndMemberId(Long shareGroupId, Long memberId);
List<Profile> findByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public interface ShareGroupService {
List<Profile> findProfileList(Long shareGroupId);
ShareGroup joinShareGroup(Long shareGroupId, Long profileId, Member member);
Profile findProfile(Long profileId);
Profile findProfile(Long shareGroupId, Long memberID);
Page<ShareGroup> getMyShareGroupList(Member member, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@ public Profile findProfile(Long profileId) {
.orElseThrow(() -> new BusinessException(ShareGroupErrorCode.PROFILE_NOT_FOUND));
}

@Override
public Profile findProfile(Long shareGroupId, Long memberId) {
return profileRepository.findByShareGroupIdAndMemberId(shareGroupId, memberId)
.orElseThrow(() -> new BusinessException(ShareGroupErrorCode.PROFILE_NOT_FOUND));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
public enum S3ErrorCode implements ErrorCode {
FAILED_UPLOAD_S3(500, "ES3000", "S3에 업로드를 실패하였습니다."),
PHOTO_NOT_FOUND_S3(404, "ES3000", "S3에서 파일을 찾을 수 없습니다."),
UNAUTHORIZED_GET(403, "ES3000", "사진을 조회할 권한이 없습니다."),
UNAUTHORIZED_DELETE(403, "ES3000", "사진을 삭제할 권한이 없습니다."),
UNAUTHORIZED_UPLOAD(403, "ES3000", "사진을 업로드할 권한이 없습니다."),
PHOTO_NOT_FOUND(404, "ES3005", "요청한 사진이 존재하지 않습니다."),

;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
public enum PhotoResultCode implements ResultCode {
CREATE_PRESIGNED_URL(200, "SP000", "성공적으로 Presigned URL을 요청하였습니다."),
UPLOAD_PHOTO(200, "SP000", "성공적으로 이미지를 업로드하였습니다."),
RETRIEVE_PHOTO(200, "SP000", "성공적으로 이미지를 조회하였습니다.")
RETRIEVE_PHOTO(200, "SP000", "성공적으로 이미지를 조회하였습니다."),
DELETE_PHOTO(200, "SP000", "성공적으로 이미지를 삭제하였습니다.")

;
private final int status;
Expand Down

0 comments on commit 445f22c

Please sign in to comment.