Skip to content

Commit

Permalink
Merge pull request #23 from Na-o-man/feature/#20/photo-save-api
Browse files Browse the repository at this point in the history
[FEAT] DB에 업로드한 사진 저장 API 구현
  • Loading branch information
jjeongdong authored Jul 30, 2024
2 parents d2c7b33 + a5c48af commit df3e479
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.umc.naoman.domain.photo.dto.PhotoRequest;
import com.umc.naoman.domain.photo.dto.PhotoResponse;
import com.umc.naoman.domain.photo.service.PhotoService;
import com.umc.naoman.domain.photo.service.PhotoServiceImpl;
import com.umc.naoman.global.result.ResultResponse;
import com.umc.naoman.global.result.code.PhotoResultCode;
import jakarta.validation.Valid;
Expand All @@ -26,7 +25,13 @@ public class PhotoController {

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

@PostMapping("/upload")
public ResultResponse<PhotoResponse.PhotoUploadInfo> upload(@Valid @RequestBody PhotoRequest.PhotoUploadRequest request) {
PhotoResponse.PhotoUploadInfo photoUploadInfo = photoService.uploadPhotoList(request);
return ResultResponse.of(PhotoResultCode.UPLOAD_PHOTO, photoUploadInfo);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.umc.naoman.domain.photo.converter;

import com.umc.naoman.domain.photo.dto.PhotoResponse;
import com.umc.naoman.domain.photo.entity.Photo;
import com.umc.naoman.domain.shareGroup.entity.ShareGroup;
import org.springframework.stereotype.Component;

import java.util.List;
Expand All @@ -13,8 +15,8 @@ public PhotoResponse.PreSignedUrlListInfo toPreSignedUrlListInfo(List<PhotoRespo
List<PhotoResponse.PreSignedUrlInfo> preSignedUrlInfoList = preSignedUrlList.stream()
.map(preSignedUrlInfo -> toPreSignedUrlInfo(
preSignedUrlInfo.getPreSignedUrl(),
preSignedUrlInfo.getImageUrl(),
preSignedUrlInfo.getImageName()
preSignedUrlInfo.getPhotoUrl(),
preSignedUrlInfo.getPhotoName()
))
.collect(Collectors.toList());

Expand All @@ -23,12 +25,19 @@ public PhotoResponse.PreSignedUrlListInfo toPreSignedUrlListInfo(List<PhotoRespo
.build();
}

public PhotoResponse.PreSignedUrlInfo toPreSignedUrlInfo(String preSignedUrl, String imageUrl, String imageName) {
public PhotoResponse.PreSignedUrlInfo toPreSignedUrlInfo(String preSignedUrl, String photoUrl, String photoName) {
return PhotoResponse.PreSignedUrlInfo.builder()
.preSignedUrl(preSignedUrl)
.imageUrl(imageUrl)
.imageName(imageName)
.photoUrl(photoUrl)
.photoName(photoName)
.build();
}

public Photo toEntity(String photoUrl, String photoName, ShareGroup shareGroup) {
return Photo.builder()
.url(photoUrl)
.name(photoName)
.shareGroup(shareGroup)
.build();
}
}
17 changes: 15 additions & 2 deletions src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.umc.naoman.domain.photo.dto;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -16,9 +17,21 @@ public abstract class PhotoRequest {
@AllArgsConstructor
public static class PreSignedUrlRequest {

@NotEmpty(message = "이미지의 이름은 하나 이상이어야 합니다.")
private List<String> imageNameList;
@NotEmpty(message = "사진의 이름은 하나 이상이어야 합니다.")
private List<String> photoNameList;

}

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

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

}
13 changes: 11 additions & 2 deletions src/main/java/com/umc/naoman/domain/photo/dto/PhotoResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ public static class PreSignedUrlListInfo {
@NoArgsConstructor
public static class PreSignedUrlInfo {
private String preSignedUrl;
private String imageUrl;
private String imageName;
private String photoUrl;
private String photoName;
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PhotoUploadInfo {
private Long shareGroupId;
private int uploadCount;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.umc.naoman.domain.photo.service;

import com.umc.naoman.domain.photo.dto.PhotoRequest;
import com.umc.naoman.domain.photo.dto.PhotoResponse;

import java.util.List;

public interface PhotoService {

List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(List<String> imageNameList);
List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request);

PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@
import com.amazonaws.services.s3.Headers;
import com.amazonaws.services.s3.model.CannedAccessControlList;
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.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.ShareGroup;
import com.umc.naoman.domain.shareGroup.service.ShareGroupService;
import com.umc.naoman.global.error.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
Expand All @@ -18,11 +26,16 @@
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.ShareGroupErrorCode.SHARE_GROUP_NOT_FOUND;

@Service
@RequiredArgsConstructor
public class PhotoServiceImpl implements PhotoService {

private final AmazonS3 amazonS3;
private final PhotoRepository photoRepository;
private final ShareGroupService shareGroupService;
private final PhotoConverter photoConverter;

@Value("${spring.cloud.aws.s3.bucket}")
Expand All @@ -35,23 +48,43 @@ public class PhotoServiceImpl implements PhotoService {

@Override
@Transactional
public List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(List<String> imageNameList) {
return imageNameList.stream()
public List<PhotoResponse.PreSignedUrlInfo> getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request) {
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());
if (shareGroup == null) {
throw new BusinessException(SHARE_GROUP_NOT_FOUND);
}

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

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

String imageName = fileName.split("/")[1];
String imageUrl = generateFileAccessUrl(fileName);
String photoName = fileName.split("/")[1];
String photoUrl = generateFileAccessUrl(fileName);

URL preSignedUrl = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucketName, fileName));
return photoConverter.toPreSignedUrlInfo(preSignedUrl.toString(), imageUrl, imageName);
return photoConverter.toPreSignedUrlInfo(preSignedUrl.toString(), photoUrl, photoName);
}

// 파일 업로드용(PUT) PreSigned URL 생성
// 사진 업로드용(PUT) PreSigned URL 생성
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
Expand All @@ -70,19 +103,37 @@ private Date getPreSignedUrlExpiration() {
return expiration;
}

// 이미지 고유 ID 생성
// 사진 고유 ID 생성
private String createFileId() {
return UUID.randomUUID().toString();
}

// 원본 이미지 전체 경로 생성
// 원본 사진 전체 경로 생성
private String createPath(String fileName) {
String fileId = createFileId();
return String.format("%s/%s", RAW_PATH_PREFIX, fileId + fileName);
}

// 원본 이미지의 접근 URL 생성
// 원본 사진의 접근 URL 생성
private String generateFileAccessUrl(String fileName) {
return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName);
}

// 사진 URL에서 사진 이름을 추출하는 메서드
private String extractPhotoNameFromUrl(String photoUrl) {
int lastSlashIndex = photoUrl.lastIndexOf('/');
return photoUrl.substring(lastSlashIndex + 1);
}

// S3에 객체의 존재 여부 확인 및 저장하는 메서드
private boolean checkAndSavePhoto(String photoUrl, String photoName, ShareGroup shareGroup) {
S3Object s3Object = amazonS3.getObject(new GetObjectRequest(bucketName + "/" + RAW_PATH_PREFIX, photoName));
if (s3Object != null) {
Photo photo = photoConverter.toEntity(photoUrl, photoName, shareGroup);
photoRepository.save(photo);
return true;
} else {
throw new BusinessException(PHOTO_NOT_FOUND_S3);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package com.umc.naoman.domain.shareGroup.controller;

import com.umc.naoman.domain.member.converter.MemberConverter;
import com.umc.naoman.domain.member.dto.MemberResponse;
import com.umc.naoman.domain.member.entity.Member;
import com.umc.naoman.domain.shareGroup.converter.ShareGroupConverter;
import com.umc.naoman.domain.shareGroup.dto.ShareGroupRequest;
import com.umc.naoman.domain.shareGroup.dto.ShareGroupResponse;
import com.umc.naoman.domain.shareGroup.entity.Profile;
import com.umc.naoman.domain.shareGroup.entity.ShareGroup;
import com.umc.naoman.domain.shareGroup.service.ShareGroupService;
import com.umc.naoman.global.result.ResultResponse;
import com.umc.naoman.global.result.code.MemberResultCode;
import com.umc.naoman.global.result.code.ShareGroupResultCode;
import com.umc.naoman.global.security.annotation.LoginMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
Expand All @@ -23,11 +32,27 @@ public class ShareGroupController {
private final ShareGroupService shareGroupService;

@PostMapping
@Operation(summary = "공유그룹 생성 API", description = "새로운 공유그룹을 생성하는 API입니다.")
public ResultResponse<ShareGroupResponse
.ShareGroupInfo> createShareGroup(@Valid @RequestBody ShareGroupRequest.createShareGroupRequest request) {
.ShareGroupInfo> createShareGroup(@Valid @RequestBody ShareGroupRequest.createShareGroupRequest request,
@LoginMember Member member) {

ShareGroup shareGroup = shareGroupService.createShareGroup(request);
ShareGroup shareGroup = shareGroupService.createShareGroup(request, member);
return ResultResponse.of(ShareGroupResultCode.CREATE_SHARE_GROUP, ShareGroupConverter.toShareGroupInfoDTO(shareGroup));
}

@GetMapping("/{shareGroupId}")
@Operation(summary = "공유그룹 조회 API", description = "shareGroupId로 특정 공유그룹을 조회하는 API입니다.")
@Parameters(value = {
@Parameter(name = "shareGroupId", description = "특정 공유그룹 id를 입력해 주세요.")
})
public ResultResponse<ShareGroupResponse.ShareGroupInfo> getShareGroupInfo(@PathVariable(name = "shareGroupId") Long shareGroupId) {

ShareGroup shareGroup = shareGroupService.findShareGroup(shareGroupId);
List<Profile> profileList = shareGroupService.findProfileList(shareGroupId);

return ResultResponse.of(ShareGroupResultCode.SHARE_GROUP_INFO,
ShareGroupConverter.toShareGroupDetailInfoDTO(shareGroup, profileList));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import com.umc.naoman.domain.shareGroup.dto.ShareGroupRequest;
import com.umc.naoman.domain.shareGroup.dto.ShareGroupResponse;
import com.umc.naoman.domain.shareGroup.entity.Profile;
import com.umc.naoman.domain.shareGroup.entity.ShareGroup;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

public class ShareGroupConverter {

Expand All @@ -16,6 +19,7 @@ public static ShareGroup toEntity(ShareGroupRequest.createShareGroupRequest requ

private static final String baseUrl = "https://na0man/invite/"; //baseUrl 상수

// 그룹 생성 시 반환하는 정보 DTO
public static ShareGroupResponse.ShareGroupInfo toShareGroupInfoDTO(ShareGroup shareGroup) {
return ShareGroupResponse.ShareGroupInfo.builder()
.shareGroupId(shareGroup.getId())
Expand All @@ -25,4 +29,29 @@ public static ShareGroupResponse.ShareGroupInfo toShareGroupInfoDTO(ShareGroup s
.build();
}

// 조회 시, 디테일한 그룹 정보를 반환하는 DTO
public static ShareGroupResponse.ShareGroupInfo toShareGroupDetailInfoDTO(ShareGroup shareGroup, List<Profile> profiles) {
List<ShareGroupResponse.ProfileInfo> profileInfoList = profiles.stream()
.map(ShareGroupConverter::toProfileInfo)
.toList();

return ShareGroupResponse.ShareGroupInfo.builder()
.shareGroupId(shareGroup.getId())
.name("임시 공유 그룹 이름") //임시 공유 그룹 이름 (고유 코드 출력)
.image(shareGroup.getImage())
.memberCount(shareGroup.getMemberCount())
.profileInfoList(profileInfoList) //프로필 리스트 출력
.createdAt(shareGroup.getCreatedAt())
.build();
}

public static ShareGroupResponse.ProfileInfo toProfileInfo(Profile profile) {
return ShareGroupResponse.ProfileInfo.builder()
.profileId(profile.getId())
.name(profile.getName())
.image(profile.getImage())
.memberId(profile.getMember() != null ? profile.getMember().getId() : null)
.build();
}

}
Loading

0 comments on commit df3e479

Please sign in to comment.