Skip to content

Commit

Permalink
[FEAT]프로필 이미지 변경 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
Hong0329 committed Feb 20, 2024
1 parent c58d363 commit c7cf4a3
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 1 deletion.
4 changes: 4 additions & 0 deletions DontBeServer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ dependencies {
//* Apple Social Login
implementation 'com.google.code.gson:gson:2.10.1'

// AWS sdk
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dontbe.www.DontBeServer.api.member.dto.request.MemberProfilePatchRequestDto;
import com.dontbe.www.DontBeServer.api.member.dto.request.MemberWithdrawRequestDto;
import com.dontbe.www.DontBeServer.api.member.dto.request.ProfilePatchRequestDto;
import com.dontbe.www.DontBeServer.api.member.dto.response.MemberDetailGetResponseDto;
import com.dontbe.www.DontBeServer.api.member.dto.response.MemberGetProfileResponseDto;
import com.dontbe.www.DontBeServer.api.member.service.MemberCommandService;
Expand All @@ -12,8 +13,11 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.security.Principal;
import static com.dontbe.www.DontBeServer.common.response.SuccessStatus.*;

Expand Down Expand Up @@ -65,6 +69,14 @@ public ResponseEntity<ApiResponse<Object>> updateMemberProfile(Principal princip
return ApiResponse.success(PATCH_MEMBER_PROFILE);
}

@PatchMapping(value = "user-profile2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "유저 프로필 수정 API입니다.(이미지 수정 버전 추가)",description = "UserProfilePatch(+ProfileImage)")
public ResponseEntity<ApiResponse<Object>> updateMemberProfile2(Principal principal, @RequestPart(value = "file", required = false) MultipartFile multipartFile, @RequestPart(value = "info", required = false) ProfilePatchRequestDto profilePatchRequestDto) {
Long memberId = MemberUtil.getMemberId(principal);
memberCommandService.updateMemberProfile2(memberId, multipartFile, profilePatchRequestDto);
return ApiResponse.success(PATCH_MEMBER_PROFILE);
}

@GetMapping("nickname-validation")
@Operation(summary = "유저 닉네임 사용 가능 확인 API 입니다.",description = "NicknameValidation")
public ResponseEntity<ApiResponse<Object>> checkMemberNickname(Principal principal, @RequestParam(value = "nickname") String nickname) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dontbe.www.DontBeServer.api.member.dto.request;

import org.springframework.web.multipart.MultipartFile;

public record ProfilePatchRequestDto (
String nickname,
Boolean isAlarmAllowed,
String memberIntro
){
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
import com.dontbe.www.DontBeServer.api.member.domain.Member;
import com.dontbe.www.DontBeServer.api.member.dto.request.MemberProfilePatchRequestDto;
import com.dontbe.www.DontBeServer.api.member.dto.request.MemberWithdrawRequestDto;
import com.dontbe.www.DontBeServer.api.member.dto.request.ProfilePatchRequestDto;
import com.dontbe.www.DontBeServer.api.member.repository.MemberRepository;
import com.dontbe.www.DontBeServer.api.notification.repository.NotificationRepository;
import com.dontbe.www.DontBeServer.api.notification.domain.Notification;
import com.dontbe.www.DontBeServer.external.s3.service.S3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@Service
Expand All @@ -33,8 +37,11 @@ public class MemberCommandService {
private final CommentRepository commentRepository;
private final CommentLikedRepository commentLikedRepository;
private final ContentLikedRepository contentLikedRepository;
private final S3Service s3Service;
private final String DEFAULT_PROFILE_URL = "https://github.com/TeamDon-tBe/SERVER/assets/97835512/fb3ea04c-661e-4221-a837-854d66cdb77e";
private final static String GHOST_IMAGE = "https://github.com/TeamDon-tBe/SERVER/assets/97835512/fb3ea04c-661e-4221-a837-854d66cdb77e";

private static final String S3_URL = "https://dontbe-s3.s3.ap-northeast-2.amazonaws.com/";
public void withdrawalMember(Long memberId, MemberWithdrawRequestDto memberWithdrawRequestDto) {
Member member = memberRepository.findMemberByIdOrThrow(memberId);
List<Ghost> ghosts = ghostRepository.findByGhostTargetMember(member);
Expand Down Expand Up @@ -111,8 +118,48 @@ public void updateMemberProfile(Long memberId, MemberProfilePatchRequestDto memb
if (memberProfilePatchRequestDto.is_alarm_allowed() != null) {
existingMember.updateMemberIsAlarmAllowed(memberProfilePatchRequestDto.is_alarm_allowed());
}

// 저장
Member savedMember = memberRepository.save(existingMember);
}

public void updateMemberProfile2(Long memberId, MultipartFile multipartFile, ProfilePatchRequestDto profilePatchRequestDto) {
Member existingMember = memberRepository.findMemberByIdOrThrow(memberId);

// 업데이트할 속성만 복사
if (profilePatchRequestDto.nickname() != null) {
existingMember.updateNickname(profilePatchRequestDto.nickname());
}
if (profilePatchRequestDto.memberIntro() != null) {
existingMember.updateMemberIntro(profilePatchRequestDto.memberIntro());
}
//이미지를 받았을 경우 S3에 업로드하고, memberTable값 수정하고, 이전에 올라갔던 이미지는 S3에서 삭제
//이때 기본 이미지였다면 삭제 과정은 스킵. 현재는 깃허브에 올라간 사진이라서 사라지지 않지만, 추후 기본 이미지를 우리 S3에 올릴 것을 대비.
if (!multipartFile.isEmpty()) {
String existedImage = existingMember.getProfileUrl();

try {
String s3ImageUrl = s3Service.uploadImage(memberId.toString(), multipartFile);
existingMember.updateProfileUrl(s3ImageUrl);

if(!existedImage.equals(GHOST_IMAGE)) {
String existedKey = removeBaseUrl(existedImage, S3_URL);
s3Service.deleteImage(existedKey);
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
if (profilePatchRequestDto.isAlarmAllowed() != null) {
existingMember.updateMemberIsAlarmAllowed(profilePatchRequestDto.isAlarmAllowed());
}
Member savedMember = memberRepository.save(existingMember);
}

private static String removeBaseUrl(String fullUrl, String baseUrl) {
if (fullUrl.startsWith(baseUrl)) {
return fullUrl.substring(baseUrl.length());
} else {
return fullUrl; // baseUrl이 존재하지 않는 경우 전체 URL 반환
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.dontbe.www.DontBeServer.external.s3.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class AWSConfig {

private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId";
private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey";

private final String accessKey;
private final String secretKey;
private final String regionString;

public AWSConfig(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}

@Bean
public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() {
System.setProperty(AWS_ACCESS_KEY_ID, accessKey);
System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey);
return SystemPropertyCredentialsProvider.create();
}

@Bean
public Region getRegion() {
return Region.of(regionString);
}

@Bean
public S3Client getS3Client() {
return S3Client.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.dontbe.www.DontBeServer.external.s3.service;

import com.dontbe.www.DontBeServer.external.s3.config.AWSConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@Component
public class S3Service {

private static final String S3_URL = "https://dontbe-s3.s3.ap-northeast-2.amazonaws.com/";
private static final List<String> IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp");
private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L;

private final String bucketName;
private final AWSConfig awsConfig;

public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AWSConfig awsConfig) {
this.bucketName = bucketName;
this.awsConfig = awsConfig;
}


public String uploadImage(String directoryPath, MultipartFile image) throws IOException {
validateExtension(image);
validateFileSize(image);

final String key = "ProfileImage/" + directoryPath + "/" + generateImageFileName();
final S3Client s3Client = awsConfig.getS3Client();

PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(image.getContentType())
.contentDisposition("inline")
.build();

RequestBody requestBody = RequestBody.fromBytes(image.getBytes());
s3Client.putObject(request, requestBody);
return S3_URL + key;
}

public void deleteImage(String key) {
final S3Client s3Client = awsConfig.getS3Client();

s3Client.deleteObject((DeleteObjectRequest.Builder builder) ->
builder.bucket(bucketName)
.key(key)
.build()
);
}


private String generateImageFileName() {
return UUID.randomUUID().toString() + ".jpg";
}

private void validateExtension(MultipartFile image) {
String contentType = image.getContentType();
if (!IMAGE_EXTENSIONS.contains(contentType)) {
throw new RuntimeException("이미지 확장자는 jpg, png, webp만 가능합니다.");
}
}

private void validateFileSize(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new RuntimeException("이미지 사이즈는 5MB를 넘을 수 없습니다.");
}
}

}

0 comments on commit c7cf4a3

Please sign in to comment.