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') { 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 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(); + } +} + 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); + } +}