Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/main/java/eatda/client/file/FileClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package eatda.client.file;

import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;

@Component
public class FileClient {

private final S3Client s3Client;
private final String bucket;
private final S3Presigner s3Presigner;

public FileClient(S3Client s3Client,
@Value("${spring.cloud.aws.s3.bucket}") String bucket,
S3Presigner s3Presigner) {
this.s3Client = s3Client;
this.bucket = bucket;
this.s3Presigner = s3Presigner;
}

public String upload(MultipartFile file, String fileKey) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(fileKey)
.contentType(file.getContentType())
.build();

try {
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
return fileKey;
} catch (Exception exception) {
throw new BusinessException(BusinessErrorCode.FILE_UPLOAD_FAILED);
}
// TODO 발생 예외 별로 처리하기
// InvalidRequestException, InvalidWriteOffsetException, TooManyPartsException, EncryptionTypeMismatchException,
// AwsServiceException, SdkClientException, S3Exception
}

public String getPreSignedUrl(String fileKey, Duration signatureDuration) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key(fileKey)
.build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.getObjectRequest(getObjectRequest)
.signatureDuration(signatureDuration)
.build();

try {
return s3Presigner.presignGetObject(presignRequest).url().toString();
} catch (Exception exception) {
throw new BusinessException(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED);
}
}
}
50 changes: 50 additions & 0 deletions src/main/java/eatda/domain/Image.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package eatda.domain;

import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import java.util.Set;
import lombok.Getter;
import org.springframework.lang.Nullable;
import org.springframework.web.multipart.MultipartFile;

@Getter
public class Image {

private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of("image/jpg", "image/jpeg", "image/png");
private static final String EXTENSION_DELIMITER = ".";
private static final String DEFAULT_CONTENT_TYPE = "bin";

private final ImageDomain domain;
private final MultipartFile file;

public Image(ImageDomain domain, @Nullable MultipartFile file) {
validateContentType(file);
this.domain = domain;
this.file = file;
}

private void validateContentType(MultipartFile file) {
if (file != null && !ALLOWED_CONTENT_TYPES.contains(file.getContentType())) {
throw new BusinessException(BusinessErrorCode.INVALID_IMAGE_TYPE);
}
}

public String getExtension() {
String filename = file.getOriginalFilename();
if (filename == null
|| filename.lastIndexOf(EXTENSION_DELIMITER) == -1
|| filename.startsWith(EXTENSION_DELIMITER)
|| filename.endsWith(EXTENSION_DELIMITER)) {
return DEFAULT_CONTENT_TYPE;
}
return filename.substring(filename.lastIndexOf(EXTENSION_DELIMITER) + 1);
}

public String getDomainName() {
return domain.getName();
}

public boolean isEmpty() {
return file == null || file.isEmpty();
}
}
42 changes: 42 additions & 0 deletions src/main/java/eatda/domain/ImageKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package eatda.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ImageKey {

@Column(name = "image_key", length = 511)
private String value;

public ImageKey(String value) {
this.value = value;
}

public boolean isEmpty() {
return value == null || value.isBlank();
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
ImageKey imageKey = (ImageKey) object;
return Objects.equals(value, imageKey.value);
}

@Override
public int hashCode() {
return Objects.hashCode(value);
}
}
10 changes: 7 additions & 3 deletions src/main/java/eatda/domain/article/Article.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package eatda.domain.article;

import eatda.domain.AuditingEntity;
import eatda.domain.ImageKey;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -30,10 +33,11 @@ public class Article extends AuditingEntity {
@Column(name = "article_url", nullable = false, length = 511)
private String articleUrl;

@Column(name = "image_key", nullable = false, length = 511)
private String imageKey;
@NotNull
@Embedded
private ImageKey imageKey;

public Article(String title, String subtitle, String articleUrl, String imageKey) {
public Article(String title, String subtitle, String articleUrl, ImageKey imageKey) {
this.title = title;
this.subtitle = subtitle;
this.articleUrl = articleUrl;
Expand Down
17 changes: 6 additions & 11 deletions src/main/java/eatda/domain/store/Cheer.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package eatda.domain.store;

import eatda.domain.AuditingEntity;
import eatda.domain.ImageKey;
import eatda.domain.member.Member;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -38,15 +40,14 @@ public class Cheer extends AuditingEntity {
@Column(nullable = false, columnDefinition = "TEXT")
private String description;

@Column(name = "image_key", length = 511)
private String imageKey;
@Embedded
private ImageKey imageKey;

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

public Cheer(Member member, Store store, String description, String imageKey) {
public Cheer(Member member, Store store, String description, ImageKey imageKey) {
validateDescription(description);
validateImageKey(imageKey);
this.member = member;
this.store = store;
this.description = description;
Expand All @@ -55,7 +56,7 @@ public Cheer(Member member, Store store, String description, String imageKey) {
this.isAdmin = false;
}

public Cheer(Member member, Store store, String description, String imageKey, boolean isAdmin) {
public Cheer(Member member, Store store, String description, ImageKey imageKey, boolean isAdmin) {
this(member, store, description, imageKey);
this.isAdmin = isAdmin;
}
Expand All @@ -65,10 +66,4 @@ private void validateDescription(String description) {
throw new BusinessException(BusinessErrorCode.INVALID_CHEER_DESCRIPTION);
}
}

private void validateImageKey(String imageKey) {
if (imageKey != null && imageKey.isBlank()) {
throw new BusinessException(BusinessErrorCode.INVALID_CHEER_IMAGE_KEY);
}
}
}
18 changes: 11 additions & 7 deletions src/main/java/eatda/domain/story/Story.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package eatda.domain.story;

import eatda.domain.AuditingEntity;
import eatda.domain.ImageKey;
import eatda.domain.member.Member;
import eatda.domain.store.StoreCategory;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand All @@ -16,6 +18,7 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -54,8 +57,9 @@ public class Story extends AuditingEntity {
@Column(name = "description", nullable = false)
private String description;

@Column(name = "image_key", nullable = false)
private String imageKey;
@NotNull
@Embedded
private ImageKey imageKey;

@Builder
private Story(
Expand All @@ -66,7 +70,7 @@ private Story(
String storeRoadAddress,
String storeLotNumberAddress,
String description,
String imageKey
ImageKey imageKey
) {
validateMember(member);
validateStore(storeKakaoId, storeCategory, storeName, storeRoadAddress, storeLotNumberAddress);
Expand Down Expand Up @@ -102,7 +106,7 @@ private void validateStore(
validateStoreLotNumberAddress(lotNumberAddress);
}

private void validateStory(String description, String imageKey) {
private void validateStory(String description, ImageKey imageKey) {
validateDescription(description);
validateImage(imageKey);
}
Expand All @@ -120,7 +124,7 @@ private void validateStoreName(String storeName) {
}

private void validateStoreRoadAddress(String roadAddress) {
if (roadAddress == null || roadAddress.isBlank()) {
if (roadAddress == null) {
throw new BusinessException(BusinessErrorCode.INVALID_STORE_ADDRESS);
}
}
Expand All @@ -143,8 +147,8 @@ private void validateDescription(String description) {
}
}

private void validateImage(String imageKey) {
if (imageKey == null || imageKey.isBlank()) {
private void validateImage(ImageKey imageKey) {
if (imageKey == null || imageKey.isEmpty()) {
throw new BusinessException(BusinessErrorCode.INVALID_STORY_IMAGE_KEY);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/eatda/repository/store/CheerRepository.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eatda.repository.store;

import eatda.domain.ImageKey;
import eatda.domain.member.Member;
import eatda.domain.store.Cheer;
import eatda.domain.store.Store;
Expand All @@ -20,7 +21,7 @@ public interface CheerRepository extends Repository<Cheer, Long> {
WHERE c.store = :store AND c.imageKey IS NOT NULL
ORDER BY c.createdAt DESC
LIMIT 1""")
Optional<String> findRecentImageKey(Store store);
Optional<ImageKey> findRecentImageKey(Store store);

int countByMember(Member member);

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/eatda/service/article/ArticleService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public ArticlesResponse getAllArticles(int size) {
article.getTitle(),
article.getSubtitle(),
article.getArticleUrl(),
imageStorage.getPresignedUrl(article.getImageKey())
imageStorage.getPreSignedUrl(article.getImageKey())
))
.toList();

Expand Down
10 changes: 6 additions & 4 deletions src/main/java/eatda/service/store/CheerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import eatda.controller.store.CheerRegisterRequest;
import eatda.controller.store.CheerResponse;
import eatda.controller.store.CheersResponse;
import eatda.domain.Image;
import eatda.domain.ImageDomain;
import eatda.domain.ImageKey;
import eatda.domain.member.Member;
import eatda.domain.store.Cheer;
import eatda.domain.store.Store;
Expand Down Expand Up @@ -37,18 +39,18 @@ public class CheerService {
private final ImageStorage imageStorage;

@Transactional
public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile image, long memberId) {
public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile imageFile, long memberId) {
Member member = memberRepository.getById(memberId);
validateRegisterCheer(member, request.storeKakaoId());

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

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

private void validateRegisterCheer(Member member, String storeKakaoId) {
Expand All @@ -69,7 +71,7 @@ public CheersResponse getCheers(int size) {
private CheersResponse toCheersResponse(List<Cheer> cheers) {
return new CheersResponse(cheers.stream()
.map(cheer -> new CheerPreviewResponse(cheer, cheer.getStore(),
imageStorage.getPresignedUrl(cheer.getImageKey())))
imageStorage.getPreSignedUrl(cheer.getImageKey())))
.toList());
}
}
2 changes: 1 addition & 1 deletion src/main/java/eatda/service/store/StoreService.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public StoresResponse getStores(int size) {

private Optional<String> getStoreImageUrl(Store store) {
return cheerRepository.findRecentImageKey(store)
.map(imageStorage::getPresignedUrl);
.map(imageStorage::getPreSignedUrl);
}

public StoreSearchResponses searchStores(String query) {
Expand Down
Loading