From 1a076e5e50a8ece0275d0ac59af358849304f9a5 Mon Sep 17 00:00:00 2001 From: Suhyeon <70002218+onpyeong@users.noreply.github.com> Date: Sun, 14 Jul 2024 12:11:45 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]:=20=EC=8A=A4=ED=8B=B0=EC=BB=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat]: 스티커 이미지 업로드 API 구현 S3Service 추가 스티커 이미지 업로드 API 구현 Related to: #146 * [Feat]: 스티커 이미지 업로드 API 구현 Related to: #146 --- build.gradle | 2 + .../SobokSobok/config/SecurityConfig.java | 3 +- .../sobok/SobokSobok/exception/ErrorCode.java | 3 + .../SobokSobok/exception/SuccessCode.java | 1 + .../SobokSobok/external/aws/s3/S3Service.java | 91 +++++++++++++++++++ .../sticker/application/StickerService.java | 81 ++++++++++------- .../SobokSobok/sticker/domain/Sticker.java | 6 ++ .../sticker/ui/StickerController.java | 19 ++++ 8 files changed, 170 insertions(+), 36 deletions(-) create mode 100644 src/main/java/io/sobok/SobokSobok/external/aws/s3/S3Service.java diff --git a/build.gradle b/build.gradle index f2f0577..3f02ec2 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,8 @@ dependencies { // AWS implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + } tasks.named('bootBuildImage') { diff --git a/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java b/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java index efaef6f..cc59e47 100644 --- a/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java +++ b/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java @@ -37,7 +37,8 @@ public class SecurityConfig { "/auth/login", "/auth/refresh", "/user", - "/pill/count/**" + "/pill/count/**", + "/sticker/image" }; private final JwtProvider jwtProvider; diff --git a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java index ec28ab7..74d21de 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java @@ -58,6 +58,9 @@ public enum ErrorCode { UNREGISTERED_STICKER(HttpStatus.NOT_FOUND, "등록되지 않은 스티커입니다."), ALREADY_SEND_STICKER(HttpStatus.CONFLICT, "이미 스티커를 전송했습니다."), UNREGISTERED_LIKE_SCHEDULE(HttpStatus.NOT_FOUND, "스티커 전송기록이 존재하지 않습니다."), + NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 저장에 실패했습니다."), + NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 이름을 찾을 수 없습니다."), + INVALID_MULTIPART_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "허용되지 않은 타입의 파일입니다.") ; private final HttpStatus code; diff --git a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java index 6b3a602..2c6ab81 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java @@ -53,6 +53,7 @@ public enum SuccessCode { SEND_STICKER_SUCCESS(HttpStatus.OK, "스티커 전송에 성공했습니다."), UPDATE_STICKER_SUCCESS(HttpStatus.OK, "보낸 스티커 수정에 성공했습니다."), GET_RECEIVED_STICKER_SUCCESS(HttpStatus.OK, "받은 스티커 전체 조회에 성공했습니다."), + UPLOAD_STICKER_IMAGE_SUCCESS(HttpStatus.OK, "스티커 이미지 등록에 성공했습니다."), ; private final HttpStatus code; diff --git a/src/main/java/io/sobok/SobokSobok/external/aws/s3/S3Service.java b/src/main/java/io/sobok/SobokSobok/external/aws/s3/S3Service.java new file mode 100644 index 0000000..2601aa7 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/external/aws/s3/S3Service.java @@ -0,0 +1,91 @@ +package io.sobok.SobokSobok.external.aws.s3; + + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.BadRequestException; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private AmazonS3 amazonS3; + + @Value("${spring.cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${spring.cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + @PostConstruct + public void amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + amazonS3 = AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + public String uploadImage(MultipartFile multipartFile, String folder) { + String fileName = createFileName(multipartFile.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + try (InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject( + new PutObjectRequest(bucket + "/" + folder + "/image", fileName, inputStream, + objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return amazonS3.getUrl(bucket + "/" + folder + "/image", fileName).toString(); + } catch (IOException e) { + throw new NotFoundException(ErrorCode.NOT_FOUND_SAVE_IMAGE_EXCEPTION); + } + } + + // 파일명 (중복 방지) + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + // 파일 유효성 검사 + private String getFileExtension(String fileName) { + if (fileName.length() == 0) { + throw new NotFoundException(ErrorCode.NOT_FOUND_IMAGE_EXCEPTION); + } + ArrayList fileValidate = new ArrayList<>(); + fileValidate.add(".jpg"); + fileValidate.add(".jpeg"); + fileValidate.add(".png"); + fileValidate.add(".JPG"); + fileValidate.add(".JPEG"); + fileValidate.add(".PNG"); + String idxFileName = fileName.substring(fileName.lastIndexOf(".")); + if (!fileValidate.contains(idxFileName)) { + throw new BadRequestException(ErrorCode.INVALID_MULTIPART_EXTENSION_EXCEPTION); + } + return fileName.substring(fileName.lastIndexOf(".")); + } + +} \ No newline at end of file diff --git a/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java index d1d21a9..db22284 100644 --- a/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java +++ b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java @@ -8,6 +8,7 @@ import io.sobok.SobokSobok.exception.model.ConflictException; import io.sobok.SobokSobok.exception.model.ForbiddenException; import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.external.aws.s3.S3Service; import io.sobok.SobokSobok.external.firebase.FCMPushService; import io.sobok.SobokSobok.external.firebase.dto.PushNotificationRequest; import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository; @@ -18,25 +19,26 @@ import io.sobok.SobokSobok.pill.infrastructure.PillRepository; import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; import io.sobok.SobokSobok.sticker.domain.LikeSchedule; +import io.sobok.SobokSobok.sticker.domain.Sticker; import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleQueryRepository; import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleRepository; import io.sobok.SobokSobok.sticker.infrastructure.StickerRepository; import io.sobok.SobokSobok.sticker.ui.dto.ReceivedStickerResponse; import io.sobok.SobokSobok.sticker.ui.dto.StickerActionResponse; import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse; - import java.util.List; import java.util.stream.Collectors; - import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor public class StickerService { private final FCMPushService fcmPushService; + private final S3Service s3Service; private final StickerRepository stickerRepository; private final UserRepository userRepository; @@ -49,10 +51,10 @@ public class StickerService { @Transactional public List getStickerList() { return stickerRepository.findAll().stream().map( - sticker -> StickerResponse.builder() - .stickerId(sticker.getId()) - .stickerImg(sticker.getStickerImg()) - .build() + sticker -> StickerResponse.builder() + .stickerId(sticker.getId()) + .stickerImg(sticker.getStickerImg()) + .build() ).collect(Collectors.toList()); } @@ -60,7 +62,7 @@ public List getStickerList() { public StickerActionResponse sendSticker(Long userId, Long scheduleId, Long stickerId) { UserServiceUtil.existsUserById(userRepository, userId); PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById( - pillScheduleRepository, scheduleId); + pillScheduleRepository, scheduleId); StickerServiceUtil.existsStickerById(stickerRepository, stickerId); if (!pillSchedule.getIsCheck()) { @@ -79,36 +81,36 @@ public StickerActionResponse sendSticker(Long userId, Long scheduleId, Long stic } LikeSchedule likeSchedule = likeScheduleRepository.save( - LikeSchedule.builder() - .scheduleId(scheduleId) - .senderId(userId) - .stickerId(stickerId) - .build() + LikeSchedule.builder() + .scheduleId(scheduleId) + .senderId(userId) + .stickerId(stickerId) + .build() ); fcmPushService.sendNotificationByToken(PushNotificationRequest.builder() - .userId(receiver.getId()) - .title(senderUsername + "님이 " + pill.getPillName() + " 복약에 반응을 남겼어요!") - .body("받은 스티커를 확인해보세요") - .type("main") - .build()); + .userId(receiver.getId()) + .title(senderUsername + "님이 " + pill.getPillName() + " 복약에 반응을 남겼어요!") + .body("받은 스티커를 확인해보세요") + .type("main") + .build()); return StickerActionResponse.builder() - .likeScheduleId(likeSchedule.getId()) - .scheduleId(likeSchedule.getScheduleId()) - .senderId(likeSchedule.getSenderId()) - .stickerId(likeSchedule.getStickerId()) - .createdAt(likeSchedule.getCreatedAt()) - .updatedAt(likeSchedule.getUpdatedAt()) - .build(); + .likeScheduleId(likeSchedule.getId()) + .scheduleId(likeSchedule.getScheduleId()) + .senderId(likeSchedule.getSenderId()) + .stickerId(likeSchedule.getStickerId()) + .createdAt(likeSchedule.getCreatedAt()) + .updatedAt(likeSchedule.getUpdatedAt()) + .build(); } @Transactional public StickerActionResponse updateSendSticker(Long userId, Long likeScheduleId, - Long stickerId) { + Long stickerId) { UserServiceUtil.existsUserById(userRepository, userId); LikeSchedule likeSchedule = likeScheduleRepository.findById(likeScheduleId) - .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_LIKE_SCHEDULE)); + .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_LIKE_SCHEDULE)); StickerServiceUtil.existsStickerById(stickerRepository, stickerId); if (!likeSchedule.isLikeScheduleSender(userId)) { @@ -118,26 +120,35 @@ public StickerActionResponse updateSendSticker(Long userId, Long likeScheduleId, likeSchedule.changeSticker(stickerId); return StickerActionResponse.builder() - .likeScheduleId(likeSchedule.getId()) - .scheduleId(likeSchedule.getScheduleId()) - .senderId(likeSchedule.getSenderId()) - .stickerId(likeSchedule.getStickerId()) - .createdAt(likeSchedule.getCreatedAt()) - .updatedAt(likeSchedule.getUpdatedAt()) - .build(); + .likeScheduleId(likeSchedule.getId()) + .scheduleId(likeSchedule.getScheduleId()) + .senderId(likeSchedule.getSenderId()) + .stickerId(likeSchedule.getStickerId()) + .createdAt(likeSchedule.getCreatedAt()) + .updatedAt(likeSchedule.getUpdatedAt()) + .build(); } @Transactional public List getReceivedStickerList(Long userId, Long scheduleId) { UserServiceUtil.existsUserById(userRepository, userId); PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById( - pillScheduleRepository, scheduleId); + pillScheduleRepository, scheduleId); Pill pill = PillServiceUtil.findPillById(pillRepository, pillSchedule.getPillId()); - if (!pill.isPillUser(userId) && !friendQueryRepository.isAlreadyFriend(userId, pill.getUserId())) { + if (!pill.isPillUser(userId) && !friendQueryRepository.isAlreadyFriend(userId, + pill.getUserId())) { throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); } return likeScheduleQueryRepository.getReceivedStickerList(scheduleId, userId); } + + @Transactional + public void uploadStickerImage(MultipartFile stickerImage) { + if (!stickerImage.isEmpty()) { + String stickerImageUrl = s3Service.uploadImage(stickerImage, "sticker"); + stickerRepository.save(Sticker.builder().stickerImg(stickerImageUrl).build()); + } + } } diff --git a/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java b/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java index ee6b0d7..85fb83c 100644 --- a/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java +++ b/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java @@ -6,6 +6,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; @@ -23,4 +24,9 @@ public class Sticker { @Column(nullable = false) private String stickerImg; + @Builder + public Sticker(String stickerImg) { + this.stickerImg = stickerImg; + } + } diff --git a/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java b/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java index 4dc29d4..926ec37 100644 --- a/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java +++ b/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java @@ -9,18 +9,21 @@ import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -96,4 +99,20 @@ public ResponseEntity>> getReceivedSti stickerService.getReceivedStickerList(user.getId(), scheduleId) )); } + + @PostMapping("/image") + @Operation( + summary = "스티커 이미지 등록 API 메서드", + description = "스티커 이미지를 업로드하는 메서드입니다." + ) + public ResponseEntity> uploadStickerImage( + @Valid @ModelAttribute MultipartFile stickerImage + ) { + stickerService.uploadStickerImage(stickerImage); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.UPLOAD_STICKER_IMAGE_SUCCESS + )); + } }