Skip to content

Commit 1a076e5

Browse files
authored
[Feat]: 스티커 이미지 업로드 API 구현 (#148)
* [Feat]: 스티커 이미지 업로드 API 구현 S3Service 추가 스티커 이미지 업로드 API 구현 Related to: #146 * [Feat]: 스티커 이미지 업로드 API 구현 Related to: #146
1 parent 0d39410 commit 1a076e5

File tree

8 files changed

+170
-36
lines changed

8 files changed

+170
-36
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ dependencies {
6161
// AWS
6262
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")
6363
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs'
64+
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
65+
6466
}
6567

6668
tasks.named('bootBuildImage') {

src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public class SecurityConfig {
3737
"/auth/login",
3838
"/auth/refresh",
3939
"/user",
40-
"/pill/count/**"
40+
"/pill/count/**",
41+
"/sticker/image"
4142
};
4243

4344
private final JwtProvider jwtProvider;

src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public enum ErrorCode {
5858
UNREGISTERED_STICKER(HttpStatus.NOT_FOUND, "등록되지 않은 스티커입니다."),
5959
ALREADY_SEND_STICKER(HttpStatus.CONFLICT, "이미 스티커를 전송했습니다."),
6060
UNREGISTERED_LIKE_SCHEDULE(HttpStatus.NOT_FOUND, "스티커 전송기록이 존재하지 않습니다."),
61+
NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 저장에 실패했습니다."),
62+
NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 이름을 찾을 수 없습니다."),
63+
INVALID_MULTIPART_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "허용되지 않은 타입의 파일입니다.")
6164
;
6265

6366
private final HttpStatus code;

src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public enum SuccessCode {
5353
SEND_STICKER_SUCCESS(HttpStatus.OK, "스티커 전송에 성공했습니다."),
5454
UPDATE_STICKER_SUCCESS(HttpStatus.OK, "보낸 스티커 수정에 성공했습니다."),
5555
GET_RECEIVED_STICKER_SUCCESS(HttpStatus.OK, "받은 스티커 전체 조회에 성공했습니다."),
56+
UPLOAD_STICKER_IMAGE_SUCCESS(HttpStatus.OK, "스티커 이미지 등록에 성공했습니다."),
5657
;
5758

5859
private final HttpStatus code;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.sobok.SobokSobok.external.aws.s3;
2+
3+
4+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
5+
import com.amazonaws.auth.BasicAWSCredentials;
6+
import com.amazonaws.services.s3.AmazonS3;
7+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
8+
import com.amazonaws.services.s3.model.CannedAccessControlList;
9+
import com.amazonaws.services.s3.model.ObjectMetadata;
10+
import com.amazonaws.services.s3.model.PutObjectRequest;
11+
import io.sobok.SobokSobok.exception.ErrorCode;
12+
import io.sobok.SobokSobok.exception.model.BadRequestException;
13+
import io.sobok.SobokSobok.exception.model.NotFoundException;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.util.ArrayList;
17+
import java.util.UUID;
18+
import javax.annotation.PostConstruct;
19+
import lombok.RequiredArgsConstructor;
20+
import org.springframework.beans.factory.annotation.Value;
21+
import org.springframework.stereotype.Service;
22+
import org.springframework.web.multipart.MultipartFile;
23+
24+
@Service
25+
@RequiredArgsConstructor
26+
public class S3Service {
27+
28+
private AmazonS3 amazonS3;
29+
30+
@Value("${spring.cloud.aws.credentials.access-key}")
31+
private String accessKey;
32+
33+
@Value("${spring.cloud.aws.credentials.secret-key}")
34+
private String secretKey;
35+
36+
@Value("${spring.cloud.aws.s3.bucket}")
37+
private String bucket;
38+
39+
@Value("${spring.cloud.aws.region.static}")
40+
private String region;
41+
42+
@PostConstruct
43+
public void amazonS3Client() {
44+
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
45+
amazonS3 = AmazonS3ClientBuilder.standard()
46+
.withRegion(region)
47+
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
48+
.build();
49+
}
50+
51+
public String uploadImage(MultipartFile multipartFile, String folder) {
52+
String fileName = createFileName(multipartFile.getOriginalFilename());
53+
ObjectMetadata objectMetadata = new ObjectMetadata();
54+
objectMetadata.setContentLength(multipartFile.getSize());
55+
objectMetadata.setContentType(multipartFile.getContentType());
56+
try (InputStream inputStream = multipartFile.getInputStream()) {
57+
amazonS3.putObject(
58+
new PutObjectRequest(bucket + "/" + folder + "/image", fileName, inputStream,
59+
objectMetadata)
60+
.withCannedAcl(CannedAccessControlList.PublicRead));
61+
return amazonS3.getUrl(bucket + "/" + folder + "/image", fileName).toString();
62+
} catch (IOException e) {
63+
throw new NotFoundException(ErrorCode.NOT_FOUND_SAVE_IMAGE_EXCEPTION);
64+
}
65+
}
66+
67+
// 파일명 (중복 방지)
68+
private String createFileName(String fileName) {
69+
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
70+
}
71+
72+
// 파일 유효성 검사
73+
private String getFileExtension(String fileName) {
74+
if (fileName.length() == 0) {
75+
throw new NotFoundException(ErrorCode.NOT_FOUND_IMAGE_EXCEPTION);
76+
}
77+
ArrayList<String> fileValidate = new ArrayList<>();
78+
fileValidate.add(".jpg");
79+
fileValidate.add(".jpeg");
80+
fileValidate.add(".png");
81+
fileValidate.add(".JPG");
82+
fileValidate.add(".JPEG");
83+
fileValidate.add(".PNG");
84+
String idxFileName = fileName.substring(fileName.lastIndexOf("."));
85+
if (!fileValidate.contains(idxFileName)) {
86+
throw new BadRequestException(ErrorCode.INVALID_MULTIPART_EXTENSION_EXCEPTION);
87+
}
88+
return fileName.substring(fileName.lastIndexOf("."));
89+
}
90+
91+
}

src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.sobok.SobokSobok.exception.model.ConflictException;
99
import io.sobok.SobokSobok.exception.model.ForbiddenException;
1010
import io.sobok.SobokSobok.exception.model.NotFoundException;
11+
import io.sobok.SobokSobok.external.aws.s3.S3Service;
1112
import io.sobok.SobokSobok.external.firebase.FCMPushService;
1213
import io.sobok.SobokSobok.external.firebase.dto.PushNotificationRequest;
1314
import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository;
@@ -18,25 +19,26 @@
1819
import io.sobok.SobokSobok.pill.infrastructure.PillRepository;
1920
import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository;
2021
import io.sobok.SobokSobok.sticker.domain.LikeSchedule;
22+
import io.sobok.SobokSobok.sticker.domain.Sticker;
2123
import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleQueryRepository;
2224
import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleRepository;
2325
import io.sobok.SobokSobok.sticker.infrastructure.StickerRepository;
2426
import io.sobok.SobokSobok.sticker.ui.dto.ReceivedStickerResponse;
2527
import io.sobok.SobokSobok.sticker.ui.dto.StickerActionResponse;
2628
import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse;
27-
2829
import java.util.List;
2930
import java.util.stream.Collectors;
30-
3131
import lombok.RequiredArgsConstructor;
3232
import org.springframework.stereotype.Service;
3333
import org.springframework.transaction.annotation.Transactional;
34+
import org.springframework.web.multipart.MultipartFile;
3435

3536
@Service
3637
@RequiredArgsConstructor
3738
public class StickerService {
3839

3940
private final FCMPushService fcmPushService;
41+
private final S3Service s3Service;
4042

4143
private final StickerRepository stickerRepository;
4244
private final UserRepository userRepository;
@@ -49,18 +51,18 @@ public class StickerService {
4951
@Transactional
5052
public List<StickerResponse> getStickerList() {
5153
return stickerRepository.findAll().stream().map(
52-
sticker -> StickerResponse.builder()
53-
.stickerId(sticker.getId())
54-
.stickerImg(sticker.getStickerImg())
55-
.build()
54+
sticker -> StickerResponse.builder()
55+
.stickerId(sticker.getId())
56+
.stickerImg(sticker.getStickerImg())
57+
.build()
5658
).collect(Collectors.toList());
5759
}
5860

5961
@Transactional
6062
public StickerActionResponse sendSticker(Long userId, Long scheduleId, Long stickerId) {
6163
UserServiceUtil.existsUserById(userRepository, userId);
6264
PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById(
63-
pillScheduleRepository, scheduleId);
65+
pillScheduleRepository, scheduleId);
6466
StickerServiceUtil.existsStickerById(stickerRepository, stickerId);
6567

6668
if (!pillSchedule.getIsCheck()) {
@@ -79,36 +81,36 @@ public StickerActionResponse sendSticker(Long userId, Long scheduleId, Long stic
7981
}
8082

8183
LikeSchedule likeSchedule = likeScheduleRepository.save(
82-
LikeSchedule.builder()
83-
.scheduleId(scheduleId)
84-
.senderId(userId)
85-
.stickerId(stickerId)
86-
.build()
84+
LikeSchedule.builder()
85+
.scheduleId(scheduleId)
86+
.senderId(userId)
87+
.stickerId(stickerId)
88+
.build()
8789
);
8890

8991
fcmPushService.sendNotificationByToken(PushNotificationRequest.builder()
90-
.userId(receiver.getId())
91-
.title(senderUsername + "님이 " + pill.getPillName() + " 복약에 반응을 남겼어요!")
92-
.body("받은 스티커를 확인해보세요")
93-
.type("main")
94-
.build());
92+
.userId(receiver.getId())
93+
.title(senderUsername + "님이 " + pill.getPillName() + " 복약에 반응을 남겼어요!")
94+
.body("받은 스티커를 확인해보세요")
95+
.type("main")
96+
.build());
9597

9698
return StickerActionResponse.builder()
97-
.likeScheduleId(likeSchedule.getId())
98-
.scheduleId(likeSchedule.getScheduleId())
99-
.senderId(likeSchedule.getSenderId())
100-
.stickerId(likeSchedule.getStickerId())
101-
.createdAt(likeSchedule.getCreatedAt())
102-
.updatedAt(likeSchedule.getUpdatedAt())
103-
.build();
99+
.likeScheduleId(likeSchedule.getId())
100+
.scheduleId(likeSchedule.getScheduleId())
101+
.senderId(likeSchedule.getSenderId())
102+
.stickerId(likeSchedule.getStickerId())
103+
.createdAt(likeSchedule.getCreatedAt())
104+
.updatedAt(likeSchedule.getUpdatedAt())
105+
.build();
104106
}
105107

106108
@Transactional
107109
public StickerActionResponse updateSendSticker(Long userId, Long likeScheduleId,
108-
Long stickerId) {
110+
Long stickerId) {
109111
UserServiceUtil.existsUserById(userRepository, userId);
110112
LikeSchedule likeSchedule = likeScheduleRepository.findById(likeScheduleId)
111-
.orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_LIKE_SCHEDULE));
113+
.orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_LIKE_SCHEDULE));
112114
StickerServiceUtil.existsStickerById(stickerRepository, stickerId);
113115

114116
if (!likeSchedule.isLikeScheduleSender(userId)) {
@@ -118,26 +120,35 @@ public StickerActionResponse updateSendSticker(Long userId, Long likeScheduleId,
118120
likeSchedule.changeSticker(stickerId);
119121

120122
return StickerActionResponse.builder()
121-
.likeScheduleId(likeSchedule.getId())
122-
.scheduleId(likeSchedule.getScheduleId())
123-
.senderId(likeSchedule.getSenderId())
124-
.stickerId(likeSchedule.getStickerId())
125-
.createdAt(likeSchedule.getCreatedAt())
126-
.updatedAt(likeSchedule.getUpdatedAt())
127-
.build();
123+
.likeScheduleId(likeSchedule.getId())
124+
.scheduleId(likeSchedule.getScheduleId())
125+
.senderId(likeSchedule.getSenderId())
126+
.stickerId(likeSchedule.getStickerId())
127+
.createdAt(likeSchedule.getCreatedAt())
128+
.updatedAt(likeSchedule.getUpdatedAt())
129+
.build();
128130
}
129131

130132
@Transactional
131133
public List<ReceivedStickerResponse> getReceivedStickerList(Long userId, Long scheduleId) {
132134
UserServiceUtil.existsUserById(userRepository, userId);
133135
PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById(
134-
pillScheduleRepository, scheduleId);
136+
pillScheduleRepository, scheduleId);
135137
Pill pill = PillServiceUtil.findPillById(pillRepository, pillSchedule.getPillId());
136138

137-
if (!pill.isPillUser(userId) && !friendQueryRepository.isAlreadyFriend(userId, pill.getUserId())) {
139+
if (!pill.isPillUser(userId) && !friendQueryRepository.isAlreadyFriend(userId,
140+
pill.getUserId())) {
138141
throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION);
139142
}
140143

141144
return likeScheduleQueryRepository.getReceivedStickerList(scheduleId, userId);
142145
}
146+
147+
@Transactional
148+
public void uploadStickerImage(MultipartFile stickerImage) {
149+
if (!stickerImage.isEmpty()) {
150+
String stickerImageUrl = s3Service.uploadImage(stickerImage, "sticker");
151+
stickerRepository.save(Sticker.builder().stickerImg(stickerImageUrl).build());
152+
}
153+
}
143154
}

src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import jakarta.persistence.GenerationType;
77
import jakarta.persistence.Id;
88
import lombok.AccessLevel;
9+
import lombok.Builder;
910
import lombok.Getter;
1011
import lombok.NoArgsConstructor;
1112
import org.hibernate.annotations.DynamicInsert;
@@ -23,4 +24,9 @@ public class Sticker {
2324
@Column(nullable = false)
2425
private String stickerImg;
2526

27+
@Builder
28+
public Sticker(String stickerImg) {
29+
this.stickerImg = stickerImg;
30+
}
31+
2632
}

src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99
import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse;
1010
import io.swagger.v3.oas.annotations.Operation;
1111
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.validation.Valid;
1213
import java.util.List;
1314
import lombok.RequiredArgsConstructor;
1415
import org.springframework.http.HttpStatus;
1516
import org.springframework.http.ResponseEntity;
1617
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1718
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.ModelAttribute;
1820
import org.springframework.web.bind.annotation.PathVariable;
1921
import org.springframework.web.bind.annotation.PostMapping;
2022
import org.springframework.web.bind.annotation.PutMapping;
2123
import org.springframework.web.bind.annotation.RequestMapping;
2224
import org.springframework.web.bind.annotation.RequestParam;
2325
import org.springframework.web.bind.annotation.RestController;
26+
import org.springframework.web.multipart.MultipartFile;
2427

2528
@RestController
2629
@RequiredArgsConstructor
@@ -96,4 +99,20 @@ public ResponseEntity<ApiResponse<List<ReceivedStickerResponse>>> getReceivedSti
9699
stickerService.getReceivedStickerList(user.getId(), scheduleId)
97100
));
98101
}
102+
103+
@PostMapping("/image")
104+
@Operation(
105+
summary = "스티커 이미지 등록 API 메서드",
106+
description = "스티커 이미지를 업로드하는 메서드입니다."
107+
)
108+
public ResponseEntity<ApiResponse<StickerActionResponse>> uploadStickerImage(
109+
@Valid @ModelAttribute MultipartFile stickerImage
110+
) {
111+
stickerService.uploadStickerImage(stickerImage);
112+
return ResponseEntity
113+
.status(HttpStatus.OK)
114+
.body(ApiResponse.success(
115+
SuccessCode.UPLOAD_STICKER_IMAGE_SUCCESS
116+
));
117+
}
99118
}

0 commit comments

Comments
 (0)