Skip to content

Commit 376e12a

Browse files
committed
Merge branch 'refs/heads/develop' into feat/PRODUCT-198
2 parents 483b363 + 48ad47d commit 376e12a

36 files changed

+664
-398
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package eatda.client.file;
2+
3+
import eatda.exception.BusinessErrorCode;
4+
import eatda.exception.BusinessException;
5+
import java.time.Duration;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.multipart.MultipartFile;
9+
import software.amazon.awssdk.core.sync.RequestBody;
10+
import software.amazon.awssdk.services.s3.S3Client;
11+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
12+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
13+
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
14+
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
15+
16+
@Component
17+
public class FileClient {
18+
19+
private final S3Client s3Client;
20+
private final String bucket;
21+
private final S3Presigner s3Presigner;
22+
23+
public FileClient(S3Client s3Client,
24+
@Value("${spring.cloud.aws.s3.bucket}") String bucket,
25+
S3Presigner s3Presigner) {
26+
this.s3Client = s3Client;
27+
this.bucket = bucket;
28+
this.s3Presigner = s3Presigner;
29+
}
30+
31+
public String upload(MultipartFile file, String fileKey) {
32+
PutObjectRequest request = PutObjectRequest.builder()
33+
.bucket(bucket)
34+
.key(fileKey)
35+
.contentType(file.getContentType())
36+
.build();
37+
38+
try {
39+
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
40+
return fileKey;
41+
} catch (Exception exception) {
42+
throw new BusinessException(BusinessErrorCode.FILE_UPLOAD_FAILED);
43+
}
44+
// TODO 발생 예외 별로 처리하기
45+
// InvalidRequestException, InvalidWriteOffsetException, TooManyPartsException, EncryptionTypeMismatchException,
46+
// AwsServiceException, SdkClientException, S3Exception
47+
}
48+
49+
public String getPreSignedUrl(String fileKey, Duration signatureDuration) {
50+
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
51+
.bucket(bucket)
52+
.key(fileKey)
53+
.build();
54+
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
55+
.getObjectRequest(getObjectRequest)
56+
.signatureDuration(signatureDuration)
57+
.build();
58+
59+
try {
60+
return s3Presigner.presignGetObject(presignRequest).url().toString();
61+
} catch (Exception exception) {
62+
throw new BusinessException(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED);
63+
}
64+
}
65+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package eatda.domain;
2+
3+
import eatda.exception.BusinessErrorCode;
4+
import eatda.exception.BusinessException;
5+
import java.util.Set;
6+
import lombok.Getter;
7+
import org.springframework.lang.Nullable;
8+
import org.springframework.web.multipart.MultipartFile;
9+
10+
@Getter
11+
public class Image {
12+
13+
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of("image/jpg", "image/jpeg", "image/png");
14+
private static final String EXTENSION_DELIMITER = ".";
15+
private static final String DEFAULT_CONTENT_TYPE = "bin";
16+
17+
private final ImageDomain domain;
18+
private final MultipartFile file;
19+
20+
public Image(ImageDomain domain, @Nullable MultipartFile file) {
21+
validateContentType(file);
22+
this.domain = domain;
23+
this.file = file;
24+
}
25+
26+
private void validateContentType(MultipartFile file) {
27+
if (file != null && !ALLOWED_CONTENT_TYPES.contains(file.getContentType())) {
28+
throw new BusinessException(BusinessErrorCode.INVALID_IMAGE_TYPE);
29+
}
30+
}
31+
32+
public String getExtension() {
33+
String filename = file.getOriginalFilename();
34+
if (filename == null
35+
|| filename.lastIndexOf(EXTENSION_DELIMITER) == -1
36+
|| filename.startsWith(EXTENSION_DELIMITER)
37+
|| filename.endsWith(EXTENSION_DELIMITER)) {
38+
return DEFAULT_CONTENT_TYPE;
39+
}
40+
return filename.substring(filename.lastIndexOf(EXTENSION_DELIMITER) + 1);
41+
}
42+
43+
public String getDomainName() {
44+
return domain.getName();
45+
}
46+
47+
public boolean isEmpty() {
48+
return file == null || file.isEmpty();
49+
}
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package eatda.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Embeddable;
5+
import java.util.Objects;
6+
import lombok.AccessLevel;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Embeddable
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
public class ImageKey {
14+
15+
@Column(name = "image_key", length = 511)
16+
private String value;
17+
18+
public ImageKey(String value) {
19+
this.value = value;
20+
}
21+
22+
public boolean isEmpty() {
23+
return value == null || value.isBlank();
24+
}
25+
26+
@Override
27+
public boolean equals(Object object) {
28+
if (this == object) {
29+
return true;
30+
}
31+
if (object == null || getClass() != object.getClass()) {
32+
return false;
33+
}
34+
ImageKey imageKey = (ImageKey) object;
35+
return Objects.equals(value, imageKey.value);
36+
}
37+
38+
@Override
39+
public int hashCode() {
40+
return Objects.hashCode(value);
41+
}
42+
}

src/main/java/eatda/domain/article/Article.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package eatda.domain.article;
22

33
import eatda.domain.AuditingEntity;
4+
import eatda.domain.ImageKey;
45
import jakarta.persistence.Column;
6+
import jakarta.persistence.Embedded;
57
import jakarta.persistence.Entity;
68
import jakarta.persistence.GeneratedValue;
79
import jakarta.persistence.GenerationType;
810
import jakarta.persistence.Id;
911
import jakarta.persistence.Table;
12+
import jakarta.validation.constraints.NotNull;
1013
import lombok.AccessLevel;
1114
import lombok.Getter;
1215
import lombok.NoArgsConstructor;
@@ -30,10 +33,11 @@ public class Article extends AuditingEntity {
3033
@Column(name = "article_url", nullable = false, length = 511)
3134
private String articleUrl;
3235

33-
@Column(name = "image_key", nullable = false, length = 511)
34-
private String imageKey;
36+
@NotNull
37+
@Embedded
38+
private ImageKey imageKey;
3539

36-
public Article(String title, String subtitle, String articleUrl, String imageKey) {
40+
public Article(String title, String subtitle, String articleUrl, ImageKey imageKey) {
3741
this.title = title;
3842
this.subtitle = subtitle;
3943
this.articleUrl = articleUrl;

src/main/java/eatda/domain/store/Cheer.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package eatda.domain.store;
22

33
import eatda.domain.AuditingEntity;
4+
import eatda.domain.ImageKey;
45
import eatda.domain.member.Member;
56
import eatda.exception.BusinessErrorCode;
67
import eatda.exception.BusinessException;
78
import jakarta.persistence.Column;
9+
import jakarta.persistence.Embedded;
810
import jakarta.persistence.Entity;
911
import jakarta.persistence.FetchType;
1012
import jakarta.persistence.GeneratedValue;
@@ -38,15 +40,14 @@ public class Cheer extends AuditingEntity {
3840
@Column(nullable = false, columnDefinition = "TEXT")
3941
private String description;
4042

41-
@Column(name = "image_key", length = 511)
42-
private String imageKey;
43+
@Embedded
44+
private ImageKey imageKey;
4345

4446
@Column(name = "is_admin", nullable = false)
4547
private boolean isAdmin;
4648

47-
public Cheer(Member member, Store store, String description, String imageKey) {
49+
public Cheer(Member member, Store store, String description, ImageKey imageKey) {
4850
validateDescription(description);
49-
validateImageKey(imageKey);
5051
this.member = member;
5152
this.store = store;
5253
this.description = description;
@@ -55,7 +56,7 @@ public Cheer(Member member, Store store, String description, String imageKey) {
5556
this.isAdmin = false;
5657
}
5758

58-
public Cheer(Member member, Store store, String description, String imageKey, boolean isAdmin) {
59+
public Cheer(Member member, Store store, String description, ImageKey imageKey, boolean isAdmin) {
5960
this(member, store, description, imageKey);
6061
this.isAdmin = isAdmin;
6162
}
@@ -65,10 +66,4 @@ private void validateDescription(String description) {
6566
throw new BusinessException(BusinessErrorCode.INVALID_CHEER_DESCRIPTION);
6667
}
6768
}
68-
69-
private void validateImageKey(String imageKey) {
70-
if (imageKey != null && imageKey.isBlank()) {
71-
throw new BusinessException(BusinessErrorCode.INVALID_CHEER_IMAGE_KEY);
72-
}
73-
}
7469
}

src/main/java/eatda/domain/story/Story.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package eatda.domain.story;
22

33
import eatda.domain.AuditingEntity;
4+
import eatda.domain.ImageKey;
45
import eatda.domain.member.Member;
56
import eatda.domain.store.StoreCategory;
67
import eatda.exception.BusinessErrorCode;
78
import eatda.exception.BusinessException;
89
import jakarta.persistence.Column;
10+
import jakarta.persistence.Embedded;
911
import jakarta.persistence.Entity;
1012
import jakarta.persistence.EnumType;
1113
import jakarta.persistence.Enumerated;
@@ -16,6 +18,7 @@
1618
import jakarta.persistence.JoinColumn;
1719
import jakarta.persistence.ManyToOne;
1820
import jakarta.persistence.Table;
21+
import jakarta.validation.constraints.NotNull;
1922
import lombok.AccessLevel;
2023
import lombok.Builder;
2124
import lombok.Getter;
@@ -54,8 +57,9 @@ public class Story extends AuditingEntity {
5457
@Column(name = "description", nullable = false)
5558
private String description;
5659

57-
@Column(name = "image_key", nullable = false)
58-
private String imageKey;
60+
@NotNull
61+
@Embedded
62+
private ImageKey imageKey;
5963

6064
@Builder
6165
private Story(
@@ -66,7 +70,7 @@ private Story(
6670
String storeRoadAddress,
6771
String storeLotNumberAddress,
6872
String description,
69-
String imageKey
73+
ImageKey imageKey
7074
) {
7175
validateMember(member);
7276
validateStore(storeKakaoId, storeCategory, storeName, storeRoadAddress, storeLotNumberAddress);
@@ -102,7 +106,7 @@ private void validateStore(
102106
validateStoreLotNumberAddress(lotNumberAddress);
103107
}
104108

105-
private void validateStory(String description, String imageKey) {
109+
private void validateStory(String description, ImageKey imageKey) {
106110
validateDescription(description);
107111
validateImage(imageKey);
108112
}
@@ -120,7 +124,7 @@ private void validateStoreName(String storeName) {
120124
}
121125

122126
private void validateStoreRoadAddress(String roadAddress) {
123-
if (roadAddress == null || roadAddress.isBlank()) {
127+
if (roadAddress == null) {
124128
throw new BusinessException(BusinessErrorCode.INVALID_STORE_ADDRESS);
125129
}
126130
}
@@ -143,8 +147,8 @@ private void validateDescription(String description) {
143147
}
144148
}
145149

146-
private void validateImage(String imageKey) {
147-
if (imageKey == null || imageKey.isBlank()) {
150+
private void validateImage(ImageKey imageKey) {
151+
if (imageKey == null || imageKey.isEmpty()) {
148152
throw new BusinessException(BusinessErrorCode.INVALID_STORY_IMAGE_KEY);
149153
}
150154
}

src/main/java/eatda/repository/store/CheerRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package eatda.repository.store;
22

3+
import eatda.domain.ImageKey;
34
import eatda.domain.member.Member;
45
import eatda.domain.store.Cheer;
56
import eatda.domain.store.Store;
@@ -22,7 +23,7 @@ public interface CheerRepository extends Repository<Cheer, Long> {
2223
WHERE c.store = :store AND c.imageKey IS NOT NULL
2324
ORDER BY c.createdAt DESC
2425
LIMIT 1""")
25-
Optional<String> findRecentImageKey(Store store);
26+
Optional<ImageKey> findRecentImageKey(Store store);
2627

2728
int countByMember(Member member);
2829

src/main/java/eatda/service/article/ArticleService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public ArticlesResponse getAllArticles(int size) {
2424
article.getTitle(),
2525
article.getSubtitle(),
2626
article.getArticleUrl(),
27-
imageStorage.getPresignedUrl(article.getImageKey())
27+
imageStorage.getPreSignedUrl(article.getImageKey())
2828
))
2929
.toList();
3030

src/main/java/eatda/service/store/CheerService.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import eatda.controller.store.CheerResponse;
99
import eatda.controller.store.CheersInStoreResponse;
1010
import eatda.controller.store.CheersResponse;
11+
import eatda.domain.Image;
1112
import eatda.domain.ImageDomain;
13+
import eatda.domain.ImageKey;
1214
import eatda.domain.member.Member;
1315
import eatda.domain.store.Cheer;
1416
import eatda.domain.store.Store;
@@ -39,18 +41,18 @@ public class CheerService {
3941
private final ImageStorage imageStorage;
4042

4143
@Transactional
42-
public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile image, long memberId) {
44+
public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile imageFile, long memberId) {
4345
Member member = memberRepository.getById(memberId);
4446
validateRegisterCheer(member, request.storeKakaoId());
4547

4648
List<StoreSearchResult> searchResults = mapClient.searchShops(request.storeName());
4749
StoreSearchResult result = storeSearchFilter.filterStoreByKakaoId(searchResults, request.storeKakaoId());
48-
String imageKey = imageStorage.upload(image, ImageDomain.CHEER);
50+
ImageKey imageKey = imageStorage.upload(new Image(ImageDomain.CHEER, imageFile));
4951

5052
Store store = storeRepository.findByKakaoId(result.kakaoId())
5153
.orElseGet(() -> storeRepository.save(result.toStore())); // TODO 상점 조회/저장 동시성 이슈 해결
5254
Cheer cheer = cheerRepository.save(new Cheer(member, store, request.description(), imageKey));
53-
return new CheerResponse(cheer, imageStorage.getPresignedUrl(imageKey), store);
55+
return new CheerResponse(cheer, imageStorage.getPreSignedUrl(imageKey), store);
5456
}
5557

5658
private void validateRegisterCheer(Member member, String storeKakaoId) {
@@ -71,7 +73,7 @@ public CheersResponse getCheers(int size) {
7173
private CheersResponse toCheersResponse(List<Cheer> cheers) {
7274
return new CheersResponse(cheers.stream()
7375
.map(cheer -> new CheerPreviewResponse(cheer, cheer.getStore(),
74-
imageStorage.getPresignedUrl(cheer.getImageKey())))
76+
imageStorage.getPreSignedUrl(cheer.getImageKey())))
7577
.toList());
7678
}
7779

src/main/java/eatda/service/store/StoreService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public StoresResponse getStores(int size) {
3838

3939
private Optional<String> getStoreImageUrl(Store store) {
4040
return cheerRepository.findRecentImageKey(store)
41-
.map(imageStorage::getPresignedUrl);
41+
.map(imageStorage::getPreSignedUrl);
4242
}
4343

4444
public StoreSearchResponses searchStores(String query) {

0 commit comments

Comments
 (0)