From 4b7c225fbf21d1b69b72234c66b2c1f152e1cb3d Mon Sep 17 00:00:00 2001 From: yeeun0702 Date: Tue, 13 May 2025 19:55:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=91=B7=20[infra]=20#15=20S3Config=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/expert/config/S3Config.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/org/example/expert/config/S3Config.java diff --git a/src/main/java/org/example/expert/config/S3Config.java b/src/main/java/org/example/expert/config/S3Config.java new file mode 100644 index 0000000..bec1038 --- /dev/null +++ b/src/main/java/org/example/expert/config/S3Config.java @@ -0,0 +1,31 @@ +package org.example.expert.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} \ No newline at end of file From 17399a6600c04e6223dfeefbbca78c4b483cbd20 Mon Sep 17 00:00:00 2001 From: yeeun0702 Date: Tue, 13 May 2025 19:55:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=91=B7=20[infra]=20#15=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/domain/s3/service/S3Service.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/s3/service/S3Service.java diff --git a/src/main/java/org/example/expert/domain/s3/service/S3Service.java b/src/main/java/org/example/expert/domain/s3/service/S3Service.java new file mode 100644 index 0000000..6ed40e9 --- /dev/null +++ b/src/main/java/org/example/expert/domain/s3/service/S3Service.java @@ -0,0 +1,42 @@ +package org.example.expert.domain.s3.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String uploadProfileImage(MultipartFile file, String userId) { + String fileName = "profile/" + userId + "_" + file.getOriginalFilename(); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + + try { + // ACL 없이 업로드 (버킷 정책에 따라 접근 가능 여부 결정됨) + PutObjectRequest request = new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata); + amazonS3.putObject(request); + } catch (IOException e) { + throw new RuntimeException("S3 업로드 실패", e); + } + + return amazonS3.getUrl(bucket, fileName).toString(); + } + + public void deleteProfileImage(String fileUrl) { + String key = fileUrl.substring(fileUrl.indexOf("profile/")); // key 추출 + amazonS3.deleteObject(bucket, key); + } +} From efbf0bee26fcf0e4af921efc35543ab839bdb057 Mon Sep 17 00:00:00 2001 From: yeeun0702 Date: Tue, 13 May 2025 19:55:46 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=91=B7=20[infra]=20#15=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../s3/controller/ProfileController.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/s3/controller/ProfileController.java diff --git a/src/main/java/org/example/expert/domain/s3/controller/ProfileController.java b/src/main/java/org/example/expert/domain/s3/controller/ProfileController.java new file mode 100644 index 0000000..f284c92 --- /dev/null +++ b/src/main/java/org/example/expert/domain/s3/controller/ProfileController.java @@ -0,0 +1,46 @@ +package org.example.expert.domain.s3.controller; + +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.s3.service.S3Service; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/profile") +@RequiredArgsConstructor +public class ProfileController { + + private final S3Service s3Service; + + /** + * 프로필 이미지 업로드 API + * - 사용자로부터 파일과 userId를 form-data 형식으로 전달받아 S3에 저장 + * - 저장된 이미지의 URL을 응답으로 반환 + * + * @param file 업로드할 이미지 파일 (.jpg/.png) + * @param userId 해당 이미지를 업로드한 사용자 ID + * @return 업로드된 이미지의 URL (S3 공개 주소) + */ + @PostMapping("/upload") + public ResponseEntity upload(@RequestParam MultipartFile file, + @RequestParam String userId) { + String imageUrl = s3Service.uploadProfileImage(file, userId); + return ResponseEntity.ok(imageUrl); + } + + + /** + * 프로필 이미지 삭제 API + * - 클라이언트가 전달한 이미지 URL을 바탕으로 S3에서 해당 이미지 삭제 + * + * @param fileUrl 삭제할 이미지의 전체 URL + * @return HTTP 200 OK + */ + @DeleteMapping("/delete") + public ResponseEntity delete(@RequestParam String fileUrl) { + s3Service.deleteProfileImage(fileUrl); + return ResponseEntity.ok().build(); + } +} + From 417cbb14cc67141ecbf7f24f14419c2171c16aff Mon Sep 17 00:00:00 2001 From: yeeun0702 Date: Tue, 13 May 2025 19:56:22 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20[add]=20#15=20S3=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index ee5ea49..1f586e4 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,10 @@ dependencies { annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + + // s3 + implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE") + } tasks.named('test') {