-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] 이미지 업로드 기능을 구현한다. #91
Changes from 30 commits
ebdffb6
7e7b427
1b8ef9e
8708ff9
0b1e150
bdcde5e
9000875
9e8670b
5c856cc
ffa88f0
b8294f4
ef54292
a30e24f
4dbc676
7c00b25
63462d2
6e32b5e
779d50f
1b4c001
ee1f309
8d906ba
d7ec1c4
e62eab7
d6b59a7
a511c70
67e220b
dc6954c
3138399
4ec7201
6b8b16b
0150da5
648259b
9f5140b
a61e0cf
610d7f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package kr.co.conceptbe.common.config; | ||
|
||
import com.amazonaws.auth.InstanceProfileCredentialsProvider; | ||
import com.amazonaws.regions.Regions; | ||
import com.amazonaws.services.s3.AmazonS3; | ||
import com.amazonaws.services.s3.AmazonS3ClientBuilder; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class S3Config { | ||
|
||
@Bean | ||
public InstanceProfileCredentialsProvider instanceProfileCredentialsProvider() { | ||
return InstanceProfileCredentialsProvider.getInstance(); | ||
} | ||
|
||
@Bean | ||
public AmazonS3 amazonS3() { | ||
return AmazonS3ClientBuilder.standard() | ||
.withRegion(Regions.AP_NORTHEAST_2) | ||
.withCredentials(instanceProfileCredentialsProvider()) | ||
.build(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,8 @@ | |
import kr.co.conceptbe.idea.dto.IdeaHitResponse; | ||
import kr.co.conceptbe.idea.exception.AlreadyIdeaLikeException; | ||
import kr.co.conceptbe.idea.exception.NotFoundIdeaLikeException; | ||
import kr.co.conceptbe.image.domain.Image; | ||
import kr.co.conceptbe.image.domain.ImageRepository; | ||
import kr.co.conceptbe.member.domain.Member; | ||
import kr.co.conceptbe.member.exception.UnAuthorizedMemberException; | ||
import kr.co.conceptbe.member.persistence.MemberRepository; | ||
|
@@ -52,6 +54,7 @@ public class IdeaService { | |
private final HitRepository hitRepository; | ||
private final SkillCategoryRepository skillCategoryRepository; | ||
private final CommentRepository commentRepository; | ||
private final ImageRepository imageRepository; | ||
|
||
public Long save(AuthCredentials authCredentials, IdeaRequest request) { | ||
validateMember(authCredentials); | ||
|
@@ -122,11 +125,13 @@ private Set<Idea> getIdeasBookmarkedByMember(Member member) { | |
|
||
public IdeaDetailResponse getDetailIdeaResponse(Long tokenMemberId, Long ideaId) { | ||
Idea idea = ideaRepository.getById(ideaId); | ||
IdeaDetailResponse ideaDetailResponse = IdeaDetailResponse.of(tokenMemberId, idea); | ||
List<Image> images = imageRepository.findAllByIdeaId(ideaId); | ||
IdeaDetailResponse ideaDetailResponse = IdeaDetailResponse.of(tokenMemberId, idea, images); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요것만 바뀐거에요! 아래는 option+command+L눌러서 바뀌었어용 호호호 |
||
|
||
Member member = memberRepository.getById(tokenMemberId); | ||
|
||
Optional<Hit> hitOptional = hitRepository.findFirstByMemberAndIdeaOrderByCreatedAtDesc(member, idea); | ||
Optional<Hit> hitOptional = hitRepository.findFirstByMemberAndIdeaOrderByCreatedAtDesc( | ||
member, idea); | ||
if (hitOptional.isEmpty() || hitOptional.get().isBeforeLocalDate()) { | ||
Hit.ofIdeaAndMember(idea, member); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package kr.co.conceptbe.idea.domain; | ||
|
||
import kr.co.conceptbe.idea.domain.persistence.IdeaRepository; | ||
import kr.co.conceptbe.image.domain.IdeaValidator; | ||
import kr.co.conceptbe.member.exception.NotOwnerException; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class IdeaValidatorImpl implements IdeaValidator { | ||
|
||
private final IdeaRepository ideaRepository; | ||
|
||
public IdeaValidatorImpl(IdeaRepository ideaRepository) { | ||
this.ideaRepository = ideaRepository; | ||
} | ||
|
||
@Override | ||
public void validateIdea(Long ideaId, Long memberId) { | ||
Idea savedIdea = ideaRepository.getById(ideaId); | ||
if (savedIdea.isOwner(memberId)) { | ||
return; | ||
} | ||
throw new NotOwnerException(memberId); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package kr.co.conceptbe.image.application; | ||
|
||
import com.amazonaws.services.s3.AmazonS3; | ||
import com.amazonaws.services.s3.model.DeleteObjectRequest; | ||
import com.amazonaws.services.s3.model.PutObjectRequest; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import kr.co.conceptbe.auth.presentation.dto.AuthCredentials; | ||
import kr.co.conceptbe.image.domain.IdeaValidator; | ||
import kr.co.conceptbe.image.domain.Image; | ||
import kr.co.conceptbe.image.domain.ImageChecker; | ||
import kr.co.conceptbe.image.domain.ImageRepository; | ||
import kr.co.conceptbe.image.domain.UploadFile; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
@Service | ||
@Transactional | ||
@RequiredArgsConstructor | ||
public class ImageService { | ||
|
||
@Value("${s3.bucket}") | ||
private String bucket; | ||
private final AmazonS3 amazonS3; | ||
private final ImageRepository imageRepository; | ||
private final IdeaValidator ideaValidator; | ||
private final ImageChecker imageChecker; | ||
|
||
public void save(Long ideaId, AuthCredentials authCredentials, List<MultipartFile> files) { | ||
ideaValidator.validateIdea(ideaId, authCredentials.id()); | ||
imageChecker.validateAdditionImagesSize(files.size()); | ||
uploadImages(ideaId, files); | ||
} | ||
|
||
private void uploadImages(Long ideaId, List<MultipartFile> files) { | ||
files.stream() | ||
.map(this::upload) | ||
.map(imageUrl -> new Image(ideaId, imageUrl)) | ||
.forEach(imageRepository::save); | ||
} | ||
|
||
private String upload(MultipartFile multipartFile) { | ||
try { | ||
UploadFile uploadFile = UploadFile.from(multipartFile); | ||
fileUpload(uploadFile); | ||
return uploadFile.getOriginalFilename(); | ||
} catch (IOException exception) { | ||
throw new RuntimeException(exception); | ||
} | ||
} | ||
|
||
private void fileUpload(MultipartFile multipartFile) throws IOException { | ||
File tempFile = null; | ||
|
||
try { | ||
tempFile = File.createTempFile("upload_", ".tmp"); | ||
multipartFile.transferTo(tempFile); | ||
amazonS3.putObject(new PutObjectRequest( | ||
bucket, | ||
multipartFile.getOriginalFilename(), | ||
tempFile | ||
)); | ||
} catch (IOException exception) { | ||
throw new IOException(exception); | ||
} finally { | ||
removeTempFileIfExists(tempFile); | ||
} | ||
} | ||
|
||
private void removeTempFileIfExists(File tempFile) { | ||
if (Objects.nonNull(tempFile) && tempFile.exists()) { | ||
tempFile.delete(); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 얘네는 서비스에말고 저장소(s3,.. )에 저장하는 책임을 담당하는 객체 만들어주면 더 좋겠네영 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 처음에 그렇게 하려고 했었는데, Service가 다른 Service를 참조하는 모양이 조금 그랬었어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 분리하면 걔는 service라기 보다는 하나의 역할을 하는 객체인데 주입할 수 있는 컴포넌트에 가깝다고 생각함당(한단계 낮은) |
||
|
||
public void update( | ||
Long ideaId, | ||
AuthCredentials authCredentials, | ||
List<Long> imageIds, | ||
List<MultipartFile> additionFiles | ||
) { | ||
ideaValidator.validateIdea(ideaId, authCredentials.id()); | ||
List<Image> imagesToDeleted = getImagesToDeleted(ideaId, imageIds, additionFiles.size()); | ||
imagesToDeleted.forEach(this::deleteImage); | ||
additionFiles.forEach(this::upload); | ||
} | ||
|
||
private List<Image> getImagesToDeleted( | ||
Long ideaId, | ||
List<Long> imageIds, | ||
int additionFilesSize | ||
) { | ||
List<Image> savedImages = imageRepository.findAllByIdeaId(ideaId); | ||
List<Long> imageIdsToDeleted = imageChecker.getImageIdsToDeleted( | ||
extractIds(savedImages), | ||
imageIds | ||
); | ||
imageChecker.validateTotalImageSize( | ||
savedImages.size(), | ||
imageIdsToDeleted.size(), | ||
additionFilesSize | ||
); | ||
return savedImages.stream() | ||
.filter(image -> imageIdsToDeleted.contains(image.getId())) | ||
.toList(); | ||
} | ||
|
||
private List<Long> extractIds(List<Image> savedImages) { | ||
return savedImages.stream() | ||
.map(Image::getId) | ||
.toList(); | ||
} | ||
|
||
private void deleteImage(Image image) { | ||
amazonS3.deleteObject(new DeleteObjectRequest(bucket, image.getImageUrl())); | ||
imageRepository.delete(image); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 얘도 |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package kr.co.conceptbe.image.application.response; | ||
|
||
import kr.co.conceptbe.image.domain.Image; | ||
import org.springframework.beans.factory.annotation.Value; | ||
|
||
public record ImageResponse( | ||
Long id, | ||
Long ideaId, | ||
String imageUrl | ||
) { | ||
|
||
@Value("${cloud-front-url}") | ||
private static String cloudFrontUrl; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 얘는 밖에서 만들어주는게 좋지 않나용 이렇게하면 이거 필요한 레코드마다 다 넣어줘야 될거같아서영 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 레코드에서 말고 Service 딴에서 말씀하시는건가용? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 |
||
|
||
public static ImageResponse from(Image image) { | ||
return new ImageResponse( | ||
image.getId(), | ||
image.getIdeaId(), | ||
String.join("/", cloudFrontUrl, image.getImageUrl()) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package kr.co.conceptbe.image.domain; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public interface IdeaValidator { | ||
|
||
void validateIdea(Long ideaId, Long memberId); | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요것만 추가했어요! 의존성에서는용