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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/org/example/expert/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<Void> delete(@RequestParam String fileUrl) {
s3Service.deleteProfileImage(fileUrl);
return ResponseEntity.ok().build();
}
}

42 changes: 42 additions & 0 deletions src/main/java/org/example/expert/domain/s3/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -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);
}
}