Skip to content

Commit

Permalink
[Feat]: 스티커 이미지 업로드 API 구현 (#148)
Browse files Browse the repository at this point in the history
* [Feat]: 스티커 이미지 업로드 API 구현

S3Service 추가
스티커 이미지 업로드 API 구현

Related to: #146

* [Feat]: 스티커 이미지 업로드 API 구현

Related to: #146
  • Loading branch information
onpyeong committed Jul 14, 2024
1 parent e480e3d commit 79a7b3a
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 36 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class SecurityConfig {
"/auth/login",
"/auth/refresh",
"/user",
"/pill/count/**"
"/pill/count/**",
"/sticker/image"
};

private final JwtProvider jwtProvider;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/io/sobok/SobokSobok/external/aws/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -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<String> 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("."));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -49,18 +51,18 @@ public class StickerService {
@Transactional
public List<StickerResponse> 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());
}

@Transactional
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()) {
Expand All @@ -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)) {
Expand All @@ -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<ReceivedStickerResponse> 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());
}
}
}
6 changes: 6 additions & 0 deletions src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,4 +24,9 @@ public class Sticker {
@Column(nullable = false)
private String stickerImg;

@Builder
public Sticker(String stickerImg) {
this.stickerImg = stickerImg;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,4 +99,20 @@ public ResponseEntity<ApiResponse<List<ReceivedStickerResponse>>> getReceivedSti
stickerService.getReceivedStickerList(user.getId(), scheduleId)
));
}

@PostMapping("/image")
@Operation(
summary = "스티커 이미지 등록 API 메서드",
description = "스티커 이미지를 업로드하는 메서드입니다."
)
public ResponseEntity<ApiResponse<StickerActionResponse>> uploadStickerImage(
@Valid @ModelAttribute MultipartFile stickerImage
) {
stickerService.uploadStickerImage(stickerImage);
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.success(
SuccessCode.UPLOAD_STICKER_IMAGE_SUCCESS
));
}
}

0 comments on commit 79a7b3a

Please sign in to comment.