Skip to content

Commit 193f9a1

Browse files
committed
feat(editUserProfile) : Presigned URL 발급 및 프로필 이미지 S3 업로드 구현
1 parent 270d93c commit 193f9a1

8 files changed

Lines changed: 184 additions & 35 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
39+
### Environment variables ###
40+
.env
41+
*.env

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies {
3737
annotationProcessor 'org.projectlombok:lombok'
3838
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3939
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
40+
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.538'
4041
}
4142

4243
tasks.named('test') {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.example.FixLog.config;
2+
3+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
4+
import com.amazonaws.auth.BasicAWSCredentials;
5+
import com.amazonaws.services.s3.AmazonS3;
6+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class AwsS3Config {
13+
14+
@Value("${cloud.aws.credentials.access-key}")
15+
private String accessKey;
16+
17+
@Value("${cloud.aws.credentials.secret-key}")
18+
private String secretKey;
19+
20+
@Value("${cloud.aws.region.static}")
21+
private String region;
22+
23+
@Bean
24+
public AmazonS3 amazonS3() {
25+
// 자격증명 생성
26+
BasicAWSCredentials creds = new BasicAWSCredentials(accessKey, secretKey);
27+
// 클라이언트 빌드
28+
return AmazonS3ClientBuilder.standard()
29+
.withRegion(region)
30+
.withCredentials(new AWSStaticCredentialsProvider(creds))
31+
.build();
32+
}
33+
}

src/main/java/com/example/FixLog/controller/MypageMemberController.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
package com.example.FixLog.controller;
22

3+
import com.example.FixLog.dto.PresignResponseDto;
34
import com.example.FixLog.dto.Response;
45
import com.example.FixLog.dto.member.edit.EditNicknameRequestDto;
56
import com.example.FixLog.dto.member.edit.EditPasswordRequestDto;
6-
import com.example.FixLog.dto.member.edit.EditProfileImageRequestDto;
77
import com.example.FixLog.dto.member.edit.EditBioRequestDto;
8+
import com.example.FixLog.exception.CustomException;
9+
import com.example.FixLog.exception.ErrorCode;
10+
import com.example.FixLog.service.S3Service;
811
import com.example.FixLog.service.MemberService;
912
import com.example.FixLog.domain.member.Member;
1013
import lombok.RequiredArgsConstructor;
1114
import org.springframework.http.ResponseEntity;
1215
import jakarta.validation.Valid;
13-
import org.springframework.web.bind.annotation.PatchMapping;
14-
import org.springframework.web.bind.annotation.RequestBody;
15-
import org.springframework.web.bind.annotation.RequestMapping;
16-
import org.springframework.web.bind.annotation.RestController;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
import java.util.Map;
1720

1821
@RestController
1922
@RequiredArgsConstructor
2023
@RequestMapping("/mypage")
2124
public class MypageMemberController {
2225

2326
private final MemberService memberService;
27+
private final S3Service s3Service;
2428

2529
@PatchMapping("/members/nickname")
2630
public ResponseEntity<Response<String>> editNickname(
@@ -40,13 +44,32 @@ public ResponseEntity<Response<String>> editPassword(
4044
return ResponseEntity.ok(Response.success("비밀번호 변경 성공", "SUCCESS"));
4145
}
4246

47+
@GetMapping("/members/profile-image/presign")
48+
public ResponseEntity<Response<PresignResponseDto>> presignProfileImage(
49+
@AuthenticationPrincipal Member member,
50+
@RequestParam String filename
51+
) {
52+
if (member == null) throw new CustomException(ErrorCode.UNAUTHORIZED);
53+
54+
String key = s3Service.generateKey("profile", filename);
55+
String uploadUrl = s3Service.generatePresignedUrl("profile", filename, 15);
56+
String fileUrl = s3Service.getObjectUrl(key);
57+
58+
PresignResponseDto dto = new PresignResponseDto(uploadUrl, fileUrl);
59+
return ResponseEntity.ok(Response.success("Presigned URL 발급 성공", dto));
60+
}
61+
4362
@PatchMapping("/members/profile-image")
44-
public ResponseEntity<Response<String>> editProfileImage(
45-
@RequestBody @Valid EditProfileImageRequestDto requestDto
63+
public ResponseEntity<Response<String>> updateProfileImageUrl(
64+
@AuthenticationPrincipal Member member,
65+
@RequestBody Map<String, String> body
4666
) {
47-
Member member = memberService.getCurrentMemberInfo();
48-
memberService.editProfileImage(member, requestDto.getProfileImageUrl());
49-
return ResponseEntity.ok(Response.success("프로필 이미지 수정 성공", "SUCCESS"));
67+
String imageUrl = body.get("imageUrl");
68+
if (imageUrl == null || imageUrl.isBlank()) {
69+
throw new CustomException(ErrorCode.INVALID_REQUEST);
70+
}
71+
memberService.editProfileImage(member, imageUrl);
72+
return ResponseEntity.ok(Response.success("프로필 이미지 저장 성공", "SUCCESS"));
5073
}
5174

5275
@PatchMapping("/members/bio")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.FixLog.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class PresignResponseDto {
9+
private final String uploadUrl; // PUT 전용 Presigned URL
10+
private final String fileUrl; // public하게 접근 가능한 URL
11+
}

src/main/java/com/example/FixLog/exception/ErrorCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ public enum ErrorCode {
2525
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."),
2626
REQUIRED_TAGS_MISSING(HttpStatus.BAD_REQUEST, "태그를 선택해주세요."),
2727
REQUIRED_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "필수 본문이 입력되지 않았습니다."),
28-
SAME_AS_OLD_PASSWORD(HttpStatus.BAD_REQUEST, "다른 비밀번호 입력 바랍니다.");
28+
SAME_AS_OLD_PASSWORD(HttpStatus.BAD_REQUEST, "다른 비밀번호 입력 바랍니다."),
29+
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다."),
30+
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "요청 데이터가 유효하지 않습니다."),
31+
S3_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "S3 파일 업로드에 실패했습니다.");
2932

3033
private final HttpStatus status;
3134
private final String message;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.example.FixLog.service;
2+
3+
import com.amazonaws.HttpMethod;
4+
import com.amazonaws.services.s3.AmazonS3;
5+
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
6+
import com.amazonaws.services.s3.model.ObjectMetadata;
7+
import com.example.FixLog.exception.CustomException;
8+
import com.example.FixLog.exception.ErrorCode;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.web.multipart.MultipartFile;
13+
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.net.URL;
17+
import java.util.Date;
18+
import java.util.UUID;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
public class S3Service {
23+
24+
private final AmazonS3 amazonS3;
25+
26+
@Value("${cloud.aws.s3.bucket}")
27+
private String bucket;
28+
29+
public String upload(MultipartFile file, String dirName) {
30+
String key = generateKey(dirName, file.getOriginalFilename());
31+
32+
ObjectMetadata metadata = new ObjectMetadata();
33+
metadata.setContentType(file.getContentType());
34+
metadata.setContentLength(file.getSize());
35+
36+
try (InputStream is = file.getInputStream()) {
37+
amazonS3.putObject(bucket, key, is, metadata);
38+
} catch (IOException e) {
39+
throw new CustomException(ErrorCode.S3_UPLOAD_FAILED);
40+
}
41+
42+
return getObjectUrl(key);
43+
}
44+
45+
public String generateKey(String dirName, String filename) {
46+
return dirName + "/" + UUID.randomUUID() + "_" + filename;
47+
}
48+
49+
public String generatePresignedUrl(String dirName, String filename, int minutes) {
50+
String key = generateKey(dirName, filename);
51+
Date expiration = new Date(System.currentTimeMillis() + minutes * 60L * 1000L);
52+
53+
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, key)
54+
.withMethod(HttpMethod.PUT)
55+
.withExpiration(expiration);
56+
57+
URL url = amazonS3.generatePresignedUrl(request);
58+
return url.toString();
59+
}
60+
61+
public String getObjectUrl(String key) {
62+
return amazonS3.getUrl(bucket, key).toString();
63+
}
64+
}
Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,43 @@
1+
# Correct property name
12
spring.application.name=FixLog
2-
# DB setting
3-
spring.h2.console.enabled=true
4-
''spring.h2.console.path=/h2-console
5-
6-
# DataBase Info
7-
spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog
8-
spring.datasource.driver-class-name=org.h2.Driver
9-
spring.datasource.username=sa
10-
spring.datasource.password=
11-
12-
spring.jpa.show-sql=true
13-
spring.jpa.hibernate.ddl-auto=update
14-
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
15-
16-
//# JWT
17-
//jwt.secret=fixlogfixlogfixlogfixlogfixlog1234
18-
//jwt.expiration-time=86400000
3+
## DB setting
4+
#spring.h2.console.enabled=true
5+
#spring.h2.console.path=/h2-console
6+
#
7+
## DataBase Info
8+
#spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog
9+
#spring.datasource.driver-class-name=org.h2.Driver
10+
#spring.datasource.username=sa
11+
#spring.datasource.password=
12+
#
13+
#spring.jpa.show-sql=true
14+
#spring.jpa.hibernate.ddl-auto=update
15+
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
1916

2017
##### dev #####
21-
#server.port=8083
22-
#
18+
server.port=8083
19+
2320
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
2421
#spring.datasource.url=${mysql_url}
2522
#spring.datasource.username=${mysql_username}
2623
#spring.datasource.password=${mysql_password}
27-
#
28-
#spring.jpa.hibernate.ddl-auto=create
29-
#spring.jpa.properties.hibernate.format_sql=true
30-
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
24+
25+
spring.datasource.url=jdbc:mysql://fixlog-db.c7cau8y2srl7.ap-northeast-2.rds.amazonaws.com:3306/fixlog?serverTimezone=Asia/Seoul
26+
spring.datasource.username=admin
27+
spring.datasource.password=${MYSQL_PASSWORD}
28+
29+
spring.jpa.hibernate.ddl-auto=update
30+
spring.jpa.show-sql=true
31+
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
32+
spring.jpa.properties.hibernate.format_sql=true
33+
34+
# AWS S3 configuration
35+
cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID}
36+
cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY}
37+
cloud.aws.region.static=${AWS_REGION}
38+
cloud.aws.s3.bucket=${AWS_S3_BUCKET}
3139

3240
##### jwt #####
33-
spring.jwt.secret=${jwt_key}
41+
jwt.secret=${jwt_key}
42+
43+
logging.level.org.springframework.security=DEBUG

0 commit comments

Comments
 (0)