build.gradle
// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
S3Config
@Configuration
public class S3Config {
@Value("${s3.credentials.access-key}")
private String accessKey;
@Value("${s3.credentials.secret-key}")
private String secretKey;
@Value("${s3.credentials.region}")
private String region;
@Bean
public AmazonS3Client s3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3Client.builder()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
Bean 주입을 위한 S3Config 를 만들어준다.
application.yml
cloud:
aws:
s3:
bucket: 버켓 이름
credentials:
access-key: 액세스키
secret-key: 시크릿키
region:
static: ap-northeast-2
stack:
auto: false
menuController
@PutMapping("/{storeId}/menus/{menuId}")
public ResponseEntity<ApiResponse<PutMenuResponseDto>> updateMenu(
@Auth AuthUser authUser,
@PathVariable(name = "storeId") Long storeId,
@PathVariable(name = "menuId") Long menuId,
@RequestPart(name = "requestDto") PutMenuRequestDto requestDto,
@RequestPart(name = "image", required = false) MultipartFile image
) {
return ResponseEntity.ok(ApiResponse.success(service.updateMenu(authUser, storeId, menuId, requestDto, image)));
}
이미지를 저장시키기 위해서 @RequestPart , MultipartFile 클래스로 image 를 넣었다.
MultipartFile 에서는 contentType, Size, Filename, Byte 등의 필드를 갖고 있다.
menuService
@Value("${s3.bucket}")
private String bucket;
bucket 변수를 사용하기 위해 초기화를 시켜준다.
// S3
private final AmazonS3Client s3Client;
AmazonS3Client 객체를 사용하기 위해 초기화.
메뉴 생성
/* 메뉴 생성 */
@Transactional
public PostMenuResponseDto addMenu(
AuthUser authUser, Long storeId, PostMenuRequestDto requestDto, MultipartFile image
) {
// 유저 확인
User user = findUserOrElseThrow(authUser.getId());
// 삭제된 유저인지 확인 및 권한 확인
checkDeletedUserAndPermissions(user.getUserStatus(), user.getUserRole());
// 가게 조회
Store store = findStoreOrElseThrow(storeId);
// 업로드한 파일의 S3 URL 주소
String imageUrl = uploadImageToS3(image, bucket);
// Entity 변환
Menu menu = new Menu(requestDto, store, imageUrl);
// DB 저장 하면서 responseDto 반환
return new PostMenuResponseDto(menuRepository.save(menu));
}
/* 이미지 파일 이름 변경 */
private String changeFileName(String originalFileName) {
// 이미지 등록 날짜를 붙여서 리턴
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
return LocalDateTime.now().format(formatter) + "_" + originalFileName;
}
// 이미지 등록 후 URL 호출 메서드
public String uploadImageToS3(MultipartFile image, String bucket) {
try {
// 이미지 이름 변경
String originalFileName = image.getOriginalFilename();
String fileName = changeFileName(originalFileName);
// S3에 파일을 보낼 때 파일의 종류와 크기를 알려주기
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
metadata.setContentDisposition("inline");
// S3에 파일 업로드
s3Client.putObject(bucket, fileName, image.getInputStream(), metadata);
return s3Client.getUrl(bucket, fileName).toString();
} catch (IOException e) {
throw new FileUploadException();
}
}
메뉴 삭제
// 등록된 사진 기존 URL 원본 파일이름으로 디코딩
public String extractFileNameFromUrl(String url) {
// URL 마지막 슬래시의 위치를 찾아서 인코딩된 파일 이름 가져오기
String encodedFileName = url.substring(url.lastIndexOf("/") + 1);
// 인코딩된 파일 이름을 디코딩 해서 진짜 원본 파일 이름 가져오기
return URLDecoder.decode(encodedFileName, StandardCharsets.UTF_8);
}
}
aws s3 로 저장될 때 파일명이 인코딩 되는데 반대로 디코딩해서 해당 파일을 타겟해서 삭제시킨다.
POSTMAN 입력 타입
전체파일
더보기
package com.sparta.sweethoney.domain.menu.service;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.util.IOUtils;
import com.sparta.sweethoney.domain.common.dto.AuthUser;
import com.sparta.sweethoney.domain.common.exception.menu.NotFoundMenuException;
import com.sparta.sweethoney.domain.common.exception.menu.ProductAlreadyStoppedException;
import com.sparta.sweethoney.domain.common.exception.order.UnauthorizedAccessException;
import com.sparta.sweethoney.domain.common.exception.store.NotFoundStoreException;
import com.sparta.sweethoney.domain.common.exception.user.NotFoundUserException;
import com.sparta.sweethoney.domain.menu.dto.request.PostMenuRequestDto;
import com.sparta.sweethoney.domain.menu.dto.request.PutMenuRequestDto;
import com.sparta.sweethoney.domain.menu.dto.response.DeleteMenuResponseDto;
import com.sparta.sweethoney.domain.menu.dto.response.PostMenuResponseDto;
import com.sparta.sweethoney.domain.menu.dto.response.PutMenuResponseDto;
import com.sparta.sweethoney.domain.menu.entity.Menu;
import com.sparta.sweethoney.domain.menu.entity.MenuStatus;
import com.sparta.sweethoney.domain.menu.repository.MenuRepository;
import com.sparta.sweethoney.domain.store.entity.Store;
import com.sparta.sweethoney.domain.store.repository.StoreRepository;
import com.sparta.sweethoney.domain.user.entity.User;
import com.sparta.sweethoney.domain.user.entity.UserRole;
import com.sparta.sweethoney.domain.user.entity.UserStatus;
import com.sparta.sweethoney.domain.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuRepository menuRepository;
private final StoreRepository storeRepository;
private final UserRepository userRepository;
// S3
private final AmazonS3Client s3Client;
// S3 버킷
@Value("${s3.bucket}")
private String bucket;
/* 메뉴 생성 */
@Transactional
public PostMenuResponseDto addMenu(
AuthUser authUser, Long storeId, PostMenuRequestDto requestDto, MultipartFile image
) {
// 유저 확인
User user = findUserOrElseThrow(authUser.getId());
// 삭제된 유저인지 확인 및 권한 확인
checkDeletedUserAndPermissions(user.getUserStatus(), user.getUserRole());
// 가게 조회
Store store = findStoreOrElseThrow(storeId);
// 업로드한 파일의 S3 URL 주소
String imageUrl = uploadImageToS3(image, bucket);
// Entity 변환
Menu menu = new Menu(requestDto, store, imageUrl);
// DB 저장 하면서 responseDto 반환
return new PostMenuResponseDto(menuRepository.save(menu));
}
/* 메뉴 수정 */
@Transactional
public PutMenuResponseDto updateMenu(
AuthUser authUser, Long storeId, Long menuId, PutMenuRequestDto requestDto, MultipartFile image
) {
// 유저 확인
User user = findUserOrElseThrow(authUser.getId());
// 삭제된 유저인지 확인 및 권한 확인
checkDeletedUserAndPermissions(user.getUserStatus(), user.getUserRole());
// 가게 조회
Store store = findStoreOrElseThrow(storeId);
// 메뉴 조회
Menu menu = findMenuOrElseThrow(menuId, store.getId());
// 업로드한 파일의 S3 URL 주소
String imageUrl = uploadImageToS3(image, bucket);
// 메뉴 수정
menu.update(requestDto, imageUrl);
// Dto 반환
return new PutMenuResponseDto(menu);
}
/* 메뉴 삭제 */
@Transactional
public DeleteMenuResponseDto deleteMenu(AuthUser authUser, Long storeId, Long menuId) {
// 유저 확인
User user = findUserOrElseThrow(authUser.getId());
// 삭제된 유저인지 확인 및 권한 확인
checkDeletedUserAndPermissions(user.getUserStatus(), user.getUserRole());
// 가게 조회
Store store = findStoreOrElseThrow(storeId);
// 메뉴 조회
Menu menu = findMenuOrElseThrow(menuId, store.getId());
// 미판매인지 확인
if (menu.getStatus() == MenuStatus.INACTIVE) {
throw new ProductAlreadyStoppedException();
}
// 기존 등록된 URL 가지고 이미지 원본 이름 가져오기
String menuImageName = extractFileNameFromUrl(menu.getImageUrl());
// 가져온 이미지 원본 이름으로 S3 이미지 삭제
s3Client.deleteObject(bucket, menuImageName);
// 메뉴 삭제(미판매로 변환)
menu.delete(MenuStatus.INACTIVE);
// Dto 반환
return new DeleteMenuResponseDto(menu);
}
/* 유저 확인 */
private User findUserOrElseThrow(Long userId) {
return userRepository.findById(userId).orElseThrow(() ->
new NotFoundUserException());
}
/* 삭제된 유저인지 확인 및 권한 확인 */
private void checkDeletedUserAndPermissions(UserStatus status, UserRole role) {
// 유저 삭제된 유저인지 확인
if (status == UserStatus.DELETED) {
throw new NotFoundUserException();
}
// 유저 권한 확인
if (role == UserRole.GUEST) {
throw new UnauthorizedAccessException();
}
}
/* 가게 조회 */
private Store findStoreOrElseThrow(Long storeId) {
return storeRepository.findById(storeId).orElseThrow(() ->
new NotFoundStoreException());
}
/* 메뉴 조회 */
private Menu findMenuOrElseThrow(Long menuId, Long storeId) {
return menuRepository.findByIdAndStoreId(menuId, storeId).orElseThrow(() ->
new NotFoundMenuException());
}
/* 이미지 파일 이름 변경 */
private String changeFileName(String originalFileName) {
// 이미지 등록 날짜를 붙여서 리턴
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
return LocalDateTime.now().format(formatter) + "_" + originalFileName;
}
/* 이미지 등록 후 URL 호출 메서드 */
public String uploadImageToS3(MultipartFile image, String bucket) {
try {
// 이미지 이름 변경
String originalFileName = image.getOriginalFilename();
String fileName = changeFileName(originalFileName);
// S3에 파일을 보낼 때 파일의 종류와 크기를 알려주기
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
metadata.setContentDisposition("inline");
// S3에 파일 업로드
s3Client.putObject(bucket, fileName, image.getInputStream(), metadata);
return s3Client.getUrl(bucket, fileName).toString();
} catch (IOException e) {
throw new FileUploadException();
}
}
/* 등록된 메뉴 기존 URL 원본 파일이름으로 디코딩 */
private String extractFileNameFromUrl(String url) {
try {
// URL 마지막 슬래시의 위치를 찾아서 인코딩된 파일 이름 가져오기
String encodedFileName = url.substring(url.lastIndexOf("/") + 1);
// 인코딩된 파일 이름을 디코딩 해서 진짜 원본 파일 이름 가져오기
return URLDecoder.decode(encodedFileName, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// This shouldn't happen with UTF-8, but we need to handle the exception
throw new RuntimeException("원본 파일 이름 변경 에러", e);
}
}
}
'컴퓨터 프로그래밍 > Spring' 카테고리의 다른 글
[Spring] Password Encoder 사용법 (0) | 2024.09.29 |
---|---|
[Spring] OAuth2.0 Kakao 소셜 로그인 기능 구현 (0) | 2024.09.29 |
[Spring] Entity 연관관계 설정 코드 (0) | 2024.09.27 |
[Spring] GlobalExceptionHandler (0) | 2024.09.22 |
[Spring] ApiResponse 예시 코드 분석 (0) | 2024.09.21 |