Skip to content

Commit

Permalink
Feat/#386 image (#391)
Browse files Browse the repository at this point in the history
* chore : S3 ์˜์กด์„ฑ ์ถ”๊ฐ€

* feat : Amazon S3 Component ์ถ”๊ฐ€

* feat : S3 ์— ์—…๋กœ๋“œ ๋  Image ์˜ ์ด๋ฆ„์„ ์„ค์ •ํ•ด์ฃผ๋Š” ImageName ๊ณผ UploadFile ์ถ”๊ฐ€

* feat : S3 ์— ์—…๋กœ๋“œ ๋  Image ์˜ ์ด๋ฆ„์„ ์„ค์ •ํ•ด์ฃผ๋Š” ImageNae ๊ณผ UploadFile ์ถ”๊ฐ€

* feat : ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  ํ•ด๋‹นํ•˜๋Š” URL ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” Service ๊ตฌํ˜„

* chore : s3 ํ™˜๊ฒฝ ์„ค์ • ์ถ”๊ฐ€

* feat : String image -> MultipartFile image ๋กœ ๋ณ€๊ฒฝ

* feat : @RequestPart ์ ์šฉ ๋ฐ S3 upload ๋กœ์ง ์ถ”๊ฐ€

* chore : Test ์‹œ Profile ์„ค์ •

* test : ํ…Œ์ŠคํŠธ ์‹œ S3 ์— ์ ‘๊ทผํ•˜์ง€ ์•Š๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด Profile ๋ณ„๋กœ S3Service Bean ๊ตฌ๋ถ„

* test : ์ถ”๊ฐ€๋œ Image ์ €์žฅ ๊ธฐ๋Šฅ์— ๋งž์ถฐ ์ผ๋ถ€ Test ์ˆ˜์ •

* chore : S3, CloudFront ํ™˜๊ฒฝ์„ค์ • ์ ์šฉ

* test : RestDocs ์ˆ˜์ •์ค‘
  • Loading branch information
kpeel5839 authored Sep 11, 2023
1 parent 34f646e commit cc91595
Show file tree
Hide file tree
Showing 28 changed files with 492 additions and 53 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ dependencies {
testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.assertj:assertj-core:3.19.0'

// S3
implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000')
implementation 'com.amazonaws:aws-java-sdk-s3'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mapbefine.mapbefine.common.config;

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 AmazonS3 amazonS3() {
return AmazonS3ClientBuilder.standard()
.withRegion(Regions.AP_NORTHEAST_2)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest;
import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException;
import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException;
import com.mapbefine.mapbefine.s3.application.S3Service;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException;
Expand All @@ -40,22 +41,24 @@ public class PinCommandService {
private final TopicRepository topicRepository;
private final MemberRepository memberRepository;
private final PinImageRepository pinImageRepository;
private final S3Service s3Service;

public PinCommandService(
PinRepository pinRepository,
LocationRepository locationRepository,
TopicRepository topicRepository,
MemberRepository memberRepository,
PinImageRepository pinImageRepository
PinImageRepository pinImageRepository,
S3Service s3Service
) {
this.pinRepository = pinRepository;
this.locationRepository = locationRepository;
this.topicRepository = topicRepository;
this.memberRepository = memberRepository;
this.pinImageRepository = pinImageRepository;
this.s3Service = s3Service;
}


public long save(AuthMember authMember, PinCreateRequest request) {
Topic topic = findTopic(request.topicId());
validatePinCreateOrUpdate(authMember, topic);
Expand Down Expand Up @@ -138,8 +141,9 @@ public void removeById(AuthMember authMember, Long pinId) {
public void addImage(AuthMember authMember, PinImageCreateRequest request) {
Pin pin = findPin(request.pinId());
validatePinCreateOrUpdate(authMember, pin.getTopic());
String image = s3Service.upload(request.image());

PinImage pinImage = PinImage.createPinImageAssociatedWithPin(request.imageUrl(), pin);
PinImage pinImage = PinImage.createPinImageAssociatedWithPin(image, pin);
pinImageRepository.save(pinImage);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.mapbefine.mapbefine.pin.dto.request;

import org.springframework.web.multipart.MultipartFile;

public record PinImageCreateRequest(
Long pinId,
String imageUrl
MultipartFile image
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.net.URI;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -21,7 +22,9 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/pins")
Expand Down Expand Up @@ -92,9 +95,16 @@ public ResponseEntity<List<PinResponse>> findAllPinsByMemberId(
}

@LoginRequired
@PostMapping("/images")
public ResponseEntity<Void> addImage(AuthMember member, @RequestBody PinImageCreateRequest request) {
pinCommandService.addImage(member, request);
@PostMapping(
value = "/images",
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}
)
public ResponseEntity<Void> addImage(
AuthMember member,
@RequestPart Long pinId,
@RequestPart MultipartFile image
) {
pinCommandService.addImage(member, new PinImageCreateRequest(pinId, image));

return ResponseEntity.status(HttpStatus.CREATED).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mapbefine.mapbefine.s3.application;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public interface S3Service {

String upload(MultipartFile multipartFile);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.mapbefine.mapbefine.s3.application;

import com.mapbefine.mapbefine.s3.domain.S3Client;
import com.mapbefine.mapbefine.s3.domain.UploadFile;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Profile("!test")
public class S3ServiceImpl implements S3Service {

@Value("${prefix.upload.path}")
private String prefixUploadPath;
private final S3Client s3Client;

public S3ServiceImpl(S3Client s3Client) {
this.s3Client = s3Client;
}

@Override
public String upload(MultipartFile multipartFile) {
try {
UploadFile uploadFile = UploadFile.of(multipartFile);
s3Client.upload(uploadFile);
return getUploadPath(uploadFile);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}

private String getUploadPath(final UploadFile uploadFile) {
return String.join(
"/",
prefixUploadPath,
uploadFile.getOriginalFilename()
);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.mapbefine.mapbefine.s3.domain;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ImageName {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS");
private static final String EXTENSION_DELIMITER = ".";

private final String fileName;

private ImageName(String fileName) {
this.fileName = fileName;
}

public static ImageName from(String originalFileName) {
String fileName = FORMATTER.format(LocalDateTime.now());
String extension = getExtension(originalFileName);

return new ImageName(fileName + extension);
}

private static String getExtension(String originalFileName) {
return originalFileName.substring(
originalFileName.lastIndexOf(EXTENSION_DELIMITER)
);
}

public String getFileName() {
return fileName;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mapbefine.mapbefine.s3.domain;

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 org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class S3Client {

@Value("${s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;

public S3Client(AmazonS3 amazonS3) {
this.amazonS3 = amazonS3;
}

public void upload(MultipartFile multipartFile) {
File tempFile = null;

try {
tempFile = File.createTempFile("upload_", ".tmp");
multipartFile.transferTo(tempFile);
amazonS3.putObject(new PutObjectRequest(bucket, multipartFile.getOriginalFilename(), tempFile));
} catch (IOException e) { // TODO: 2023/09/07 Exception ์„ ์ˆ˜์ •
throw new RuntimeException(e);
} finally {
removeTempFileIfExists(tempFile);
}
}

private void removeTempFileIfExists(final File tempFile) {
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}

public void delete(String key) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.mapbefine.mapbefine.s3.domain;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

public class UploadFile implements MultipartFile {

private final String fileName;
private final byte[] bytes;

private UploadFile(
String fileName,
byte[] bytes
) {
this.fileName = fileName;
this.bytes = bytes;
}

public static UploadFile of(
MultipartFile multipartFile
) throws IOException {
ImageName imageName = ImageName.from(multipartFile.getOriginalFilename());
byte[] multipartFileBytes = multipartFile.getBytes();

return new UploadFile(imageName.getFileName(), multipartFileBytes);
}

@Override
public String getName() {
return fileName;
}

@Override
public String getOriginalFilename() {
return fileName;
}

@Override
public String getContentType() {
return null;
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public long getSize() {
return 0;
}

@Override
public byte[] getBytes() throws IOException {
return bytes;
}

@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(bytes);
}

@Override
public Resource getResource() {
return MultipartFile.super
.getResource();
}

@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
try (FileOutputStream fileOutputStream = new FileOutputStream(dest)) {
fileOutputStream.write(bytes);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.mapbefine.mapbefine.pin.domain.PinRepository;
import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException;
import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException;
import com.mapbefine.mapbefine.s3.application.S3Service;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest;
Expand All @@ -35,15 +36,18 @@ public class TopicCommandService {
private final TopicRepository topicRepository;
private final PinRepository pinRepository;
private final MemberRepository memberRepository;
private final S3Service s3Service;

public TopicCommandService(
TopicRepository topicRepository,
PinRepository pinRepository,
MemberRepository memberRepository
MemberRepository memberRepository,
S3Service s3Service
) {
this.topicRepository = topicRepository;
this.pinRepository = pinRepository;
this.memberRepository = memberRepository;
this.s3Service = s3Service;
}

public Long saveTopic(AuthMember member, TopicCreateRequest request) {
Expand All @@ -61,11 +65,12 @@ public Long saveTopic(AuthMember member, TopicCreateRequest request) {

private Topic convertToTopic(AuthMember member, TopicCreateRequest request) {
Member creator = findCreatorByAuthMember(member);
String image = s3Service.upload(request.image());

return Topic.createTopicAssociatedWithCreator(
request.name(),
request.description(),
request.image(),
image,
request.publicity(),
request.permissionType(),
creator
Expand Down
Loading

0 comments on commit cc91595

Please sign in to comment.