Skip to content

Commit 2ee885b

Browse files
authored
Merge pull request #159 from JNU-econovation/feat-#63
[BE-63] feat: 프로필 사진 업데이트, 삭제 기능 구현
2 parents 5d08f21 + 5ae1ae1 commit 2ee885b

File tree

18 files changed

+490
-12
lines changed

18 files changed

+490
-12
lines changed

build.gradle

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ dependencies {
3434
runtimeOnly 'com.mysql:mysql-connector-j'
3535
annotationProcessor 'org.projectlombok:lombok'
3636
testImplementation 'org.springframework.boot:spring-boot-starter-test'
37+
testImplementation 'org.mockito:mockito-junit-jupiter'
38+
testImplementation 'org.assertj:assertj-core'
39+
testImplementation 'org.junit.jupiter:junit-jupiter-api'
40+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
3741
testImplementation 'org.springframework.security:spring-security-test'
38-
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
3942
implementation platform('org.springframework.cloud:spring-cloud-dependencies:2023.0.4')
4043
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
4144
implementation 'io.micrometer:micrometer-registry-prometheus'
@@ -61,6 +64,9 @@ dependencies {
6164

6265
//CoolSMS
6366
implementation 'com.solapi:sdk:1.0.3'
67+
68+
//AWS S3
69+
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
6470
}
6571

6672
tasks.named('test') {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.econo_4factorial.newproject.common.config;
2+
3+
import com.amazonaws.auth.AWSCredentials;
4+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
5+
import com.amazonaws.auth.BasicAWSCredentials;
6+
import com.amazonaws.services.s3.AmazonS3Client;
7+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
12+
@Configuration
13+
public class S3Config {
14+
15+
@Value("${cloud.aws.credentials.access-key}")
16+
private String accessKey;
17+
18+
@Value("${cloud.aws.credentials.secret-key}")
19+
private String secretKey;
20+
21+
@Value("${cloud.aws.region.static}")
22+
private String region;
23+
24+
@Bean
25+
public AmazonS3Client amazonS3Client() {
26+
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
27+
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
28+
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
29+
.withRegion(region).build();
30+
}
31+
}

src/main/java/com/econo_4factorial/newproject/common/exception/ValidationMessage.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ public class ValidationMessage {
3131
public static final String BLOOD_TYPE_IS_REQUIRED = "혈액형은 필수입니다.";
3232
public static final String INVALID_BLOOD_TYPE = "혈액형은 A, B, O, AB 중 하나여야 합니다.";
3333
public static final String ETC_LENGTH_INVALID = "기타사항은 200자 이내로 작성해야 합니다.";
34+
public static final String IMAGE_URL_REQUIRED = "ImageUrl이 누락되었습니다.";
35+
public static final String IMAGE_URL_INVALID = "ImageUrl형식이 올바르지 않습니다.";
36+
public static final String FILE_FORMAT_IS_REQUIRED = "이미지 파일 형식은 필수입니다.";
37+
public static final String FILE_NAME_IS_REQUIRED = "파일명은 필수입니다.";
3438
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.econo_4factorial.newproject.user.controller;
2+
3+
import com.econo_4factorial.newproject.common.annotation.UserId;
4+
import com.econo_4factorial.newproject.common.util.api.ApiResponse;
5+
import com.econo_4factorial.newproject.common.util.api.ApiResult;
6+
import com.econo_4factorial.newproject.user.dto.PresignedUrlDTO;
7+
import com.econo_4factorial.newproject.user.dto.ProfileImageUrlDTO;
8+
import com.econo_4factorial.newproject.user.dto.req.AddFileNameReq;
9+
import com.econo_4factorial.newproject.user.dto.req.IssuePresignedUrlReq;
10+
import com.econo_4factorial.newproject.user.dto.res.GetPresignedUrlRes;
11+
import com.econo_4factorial.newproject.user.dto.res.GetProfileImageUrlRes;
12+
import com.econo_4factorial.newproject.user.service.S3Service;
13+
import io.swagger.v3.oas.annotations.Operation;
14+
import io.swagger.v3.oas.annotations.Parameter;
15+
import io.swagger.v3.oas.annotations.tags.Tag;
16+
import jakarta.validation.Valid;
17+
import lombok.AllArgsConstructor;
18+
import org.springframework.http.HttpStatus;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@AllArgsConstructor
22+
@RestController
23+
@RequestMapping("api/v1/users/profile-image")
24+
@Tag(name = "Image", description = "이미지 업로드 관련 API")
25+
public class ImageController {
26+
27+
private final S3Service s3Service;
28+
29+
@Operation(summary = "S3 Presigned 주소 요청", description = "이미지 업로드할 presigned-url을 반환합니다.")
30+
@PostMapping
31+
public ApiResult<ApiResult.SuccessBody<GetPresignedUrlRes>> getPresignedUrl(
32+
@Parameter(hidden = true)
33+
@UserId Long userId,
34+
@RequestBody @Valid IssuePresignedUrlReq issuePresignedUrlReq
35+
) {
36+
PresignedUrlDTO presignedUrlDTO = s3Service.createPresignedUrl(userId,issuePresignedUrlReq.imageFileFormat());
37+
return ApiResponse.success(GetPresignedUrlRes.from(presignedUrlDTO), HttpStatus.OK);
38+
}
39+
40+
@Operation(summary = "프로필 이미지 조회", description = "S3에 업로드된 프로필 이미지 URL을 반환합니다.")
41+
@GetMapping
42+
public ApiResult<ApiResult.SuccessBody<GetProfileImageUrlRes>> getProfileImage(
43+
@Parameter(hidden = true)
44+
@UserId Long userId
45+
) {
46+
ProfileImageUrlDTO profileImageUrlDTO = s3Service.getImageUrl(userId);
47+
return ApiResponse.success(GetProfileImageUrlRes.from(profileImageUrlDTO), HttpStatus.OK);
48+
}
49+
50+
@Operation(summary = "프로필 이미지 삭제", description = "S3에 업로드된 프로필 이미지를 삭제합니다.")
51+
@DeleteMapping
52+
public ApiResult<ApiResult.SuccessBody<Void>> deleteProfileImage(
53+
@Parameter(hidden = true)
54+
@UserId Long userId
55+
) {
56+
s3Service.deleteImageUrl(userId);
57+
return ApiResponse.success(null, HttpStatus.OK);
58+
}
59+
60+
@Operation(summary = "ImageFileName DB저장", description = "파일을 Bucket에 업로드 후, 파일명을 DB에 저장합니다.")
61+
@PostMapping("/save")
62+
public ApiResult<ApiResult.SuccessBody<Void>> saveImageFileName(
63+
@Parameter(hidden = true)
64+
@UserId Long userId,
65+
@RequestBody @Valid AddFileNameReq addFileNameReq
66+
) {
67+
s3Service.saveFileNameToEntity(userId, addFileNameReq.fileName());
68+
return ApiResponse.success(null, HttpStatus.OK);
69+
}
70+
}

src/main/java/com/econo_4factorial/newproject/user/controller/UserController.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
import com.econo_4factorial.newproject.user.service.RandomNicknameService;
99
import com.econo_4factorial.newproject.user.dto.UserAlertSettingDTO;
1010
import com.econo_4factorial.newproject.user.dto.UserProfileDTO;
11+
import com.econo_4factorial.newproject.user.dto.*;
1112
import com.econo_4factorial.newproject.user.dto.req.*;
12-
import com.econo_4factorial.newproject.user.dto.ProfileStatusInfoDTO;
1313
import com.econo_4factorial.newproject.user.dto.req.AddPersonalInformationReq;
1414
import com.econo_4factorial.newproject.user.dto.req.AlertSettingReq;
1515
import com.econo_4factorial.newproject.user.dto.req.CheckNicknameReq;
1616
import com.econo_4factorial.newproject.user.dto.res.GetAlertSettingRes;
1717
import com.econo_4factorial.newproject.user.dto.res.GetNicknameAvailabilityRes;
1818
import com.econo_4factorial.newproject.user.dto.res.GetProfileRes;
19-
import com.econo_4factorial.newproject.user.dto.res.GetProfileStatusRes;
2019
import com.econo_4factorial.newproject.user.service.UserService;
2120
import io.swagger.v3.oas.annotations.Operation;
2221
import io.swagger.v3.oas.annotations.Parameter;
@@ -37,12 +36,6 @@
3736
public class UserController {
3837
private final UserService userService;
3938
private final RandomNicknameService randomNicknameService;
40-
41-
@GetMapping("/nickname/random")
42-
public ApiResult<ApiResult.SuccessBody<GetRandomNicknameRes>> getRandomNickname () {
43-
String nickname = randomNicknameService.getRandomNickname();
44-
return ApiResponse.success(GetRandomNicknameRes.from(nickname), HttpStatus.OK);
45-
}
4639

4740
@GetMapping("/profile")
4841
@Operation(summary = "프로필 조회", description = "사용자의 프로필을 조회합니다.")
@@ -119,6 +112,13 @@ public ApiResult<ApiResult.SuccessBody<Void>> updateAlert(
119112
return ApiResponse.success(null, HttpStatus.OK);
120113
}
121114

115+
@GetMapping("/nickname/random")
116+
@Operation(summary = "랜덤 닉네임 조회", description = "랜덤으로 생성된 닉네임을 조회합니다.")
117+
public ApiResult<ApiResult.SuccessBody<GetRandomNicknameRes>> getRandomNickname () {
118+
String nickname = randomNicknameService.getRandomNickname();
119+
return ApiResponse.success(GetRandomNicknameRes.from(nickname), HttpStatus.OK);
120+
}
121+
122122
@GetMapping("/nickname/check")
123123
@Operation(summary = "닉네임 중복 확인", description = "닉네임 중복을 체크합니다.")
124124
public ApiResult<ApiResult.SuccessBody<GetNicknameAvailabilityRes>> checkNicknameUnique (
@@ -128,5 +128,4 @@ public ApiResult<ApiResult.SuccessBody<GetNicknameAvailabilityRes>> checkNicknam
128128
boolean result = userService.isNicknameUnique(checkNicknameReq.nickname());
129129
return ApiResponse.success(GetNicknameAvailabilityRes.from(result), HttpStatus.OK);
130130
}
131-
132-
}
131+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.econo_4factorial.newproject.user.domain;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public enum ImageFileFormat
9+
{
10+
JPG("jpeg"),
11+
PNG("png"),
12+
HEIC("heic");
13+
14+
private final String uploadExtension;
15+
}

src/main/java/com/econo_4factorial/newproject/user/domain/User.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public class User extends BaseEntity {
4141
@Embedded
4242
private PhysicalInfo physicalInfo;
4343

44+
@Column
45+
private String profileFileName;
46+
4447
@Builder(builderMethodName = "kakaoUserBuilder", builderClassName = "kakaoUserBuilder")
4548
public User(String email, String name, Long kakaoId) {
4649
this.userInfo = new UserInfo(email, name);
@@ -81,6 +84,14 @@ public void updateUserProfile(String name, String email, String nickname, String
8184
}
8285
}
8386

87+
public void updateProfileFile(String profileFileName) {
88+
this.profileFileName = profileFileName;
89+
}
90+
91+
public void deleteProfileImage() {
92+
this.profileFileName = null;
93+
}
94+
8495
public boolean isBasicInfoSet() {
8596
return nickname != null && userInfo.isBasicInfoSet();
8697
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.econo_4factorial.newproject.user.dto;
2+
3+
public record PresignedUrlDTO(
4+
String presignedUrl,
5+
String fileName
6+
) {
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.econo_4factorial.newproject.user.dto;
2+
3+
public record ProfileImageUrlDTO(
4+
String profileImageUrl
5+
) {
6+
}

src/main/java/com/econo_4factorial/newproject/user/dto/UserProfileDTO.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ public record UserProfileDTO(
88
String name,
99
String nickname,
1010
String phoneNumber,
11-
//image_url
1211
String email,
1312
Long weight,
1413
Long height,

0 commit comments

Comments
 (0)