Skip to content

Commit

Permalink
Merge branch 'backend-main' of https://github.com/woowacourse-teams/2…
Browse files Browse the repository at this point in the history
…023-emmsale into Feature/#664-행사_DTO의_imageUrl을_S3의_데이터로_대체
  • Loading branch information
amaran-th committed Oct 9, 2023
2 parents 965b078 + 8bb0fb2 commit 1978ad0
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 40 deletions.
12 changes: 12 additions & 0 deletions backend/emm-sale/src/documentTest/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ include::{snippets}/delete-interest-tag/http-response.adoc[]
.HTTP response 설명
include::{snippets}/delete-interest-tag/response-fields.adoc[]

=== `PATCH` : 사용자의 프로필을 변경한다.

.HTTP request
```http
POST /members/2/profile HTTP/1.1
Content-Type: multipart/form-data; boundary=6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm
Host: localhost:8080
```

.HTTP response
include::{snippets}/update-profile/http-response.adoc[]

== Event

=== `GET` : 행사 상세정보 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand All @@ -23,16 +24,21 @@
import com.emmsale.member.application.dto.MemberActivityResponses;
import com.emmsale.member.application.dto.MemberProfileResponse;
import com.emmsale.member.application.dto.OpenProfileUrlRequest;
import com.emmsale.member.domain.Member;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.RequestFieldsSnippet;
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.web.multipart.MultipartFile;

@WebMvcTest(MemberApi.class)
class MemberApiTest extends MockMvcTestHelper {
Expand Down Expand Up @@ -255,4 +261,46 @@ void deleteMemberTest() throws Exception {
.andDo(print())
.andDo(document("delete-member"));
}

@Test
@DisplayName("멤버 프로필을 변경할 수 있다.")
void updateProfile() throws Exception {
//given
final String imageUrl = "http://imageUrl.png";
final Long memberId = 1L;
final String accessToken = "access_token";
final MockMultipartHttpServletRequestBuilder builder = createUpdateProfileBuilder(memberId);

when(memberUpdateService.updateMemberProfile
(any(MultipartFile.class), anyLong(), any(Member.class)))
.thenReturn(imageUrl);

//when
mockMvc.perform(builder
.header("Authorization", accessToken))
.andExpect(status().isOk())
.andDo(print())
.andDo(document("update-profile"));
}

private MockMultipartHttpServletRequestBuilder createUpdateProfileBuilder(final Long memberId)
throws IOException {
final MockMultipartFile image = new MockMultipartFile(
"picture",
"picture.jpg",
MediaType.TEXT_PLAIN_VALUE,
"test data".getBytes()
);

final MockMultipartHttpServletRequestBuilder builder =
multipart("/members/{member-id}/profile", memberId)
.file("image", image.getBytes());

builder.with((mockHttpServletRequest) -> {
mockHttpServletRequest.setMethod("PATCH");
return mockHttpServletRequest;
});

return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,32 @@
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
@Component
public class S3Client {

private static final String EXTENSION_DELIMITER = ".";
private static final List<String> ALLOWED_FILE_EXTENSIONS = List.of(".jpg", ".png", ".jpeg");
private static final int MIN_EXTENSION_SEPARATOR_INDEX = 0;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

private static final String URL_DELIMITER = "/";

private final String bucket;
private final AmazonS3 amazonS3;


public S3Client(@Value("${cloud.aws.s3.bucket}") final String bucket, final AmazonS3 amazonS3) {
this.bucket = bucket;
this.amazonS3 = amazonS3;
}

public List<String> uploadImages(final List<MultipartFile> multipartFiles) {
return multipartFiles.stream().map(this::uploadImage)
.collect(Collectors.toList());
}

private String uploadImage(final MultipartFile file) {
public String uploadImage(final MultipartFile file) {
final String fileExtension = extractFileExtension(file);
final String newFileName = UUID.randomUUID().toString().concat(fileExtension);
final ObjectMetadata objectMetadata = configureObjectMetadata(file);
Expand Down Expand Up @@ -80,4 +82,12 @@ public void deleteImages(final List<String> fileNames) {
throw new ImageException(ImageExceptionType.FAIL_S3_DELETE_IMAGE);
}
}

public String convertImageUrl(final String imageName) {
return String.join(URL_DELIMITER, bucket, imageName);
}

public String convertImageName(final String imageUrl) {
return imageUrl.split(bucket + URL_DELIMITER, 2)[1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
Expand Down Expand Up @@ -95,4 +97,14 @@ public ResponseEntity<Void> deleteMember(
memberUpdateService.deleteMember(member, memberId);
return ResponseEntity.noContent().build();
}

@PatchMapping("/members/{memberId}/profile")
public ResponseEntity<String> updateProfile(
@PathVariable final Long memberId,
final MultipartFile image,
final Member member
) {
final String imageUrl = memberUpdateService.updateMemberProfile(image, memberId, member);
return ResponseEntity.ok(imageUrl);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package com.emmsale.member.application;

import com.emmsale.image.application.S3Client;
import com.emmsale.member.application.dto.DescriptionRequest;
import com.emmsale.member.application.dto.OpenProfileUrlRequest;
import com.emmsale.member.domain.Member;
import com.emmsale.member.domain.MemberRepository;
import com.emmsale.member.exception.MemberException;
import com.emmsale.member.exception.MemberExceptionType;
import java.util.List;
import javax.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberUpdateService {

private final MemberRepository memberRepository;
private final S3Client s3Client;

public void updateOpenProfileUrl(
final Member member,
Expand Down Expand Up @@ -43,4 +47,25 @@ public void deleteMember(final Member member, final Long memberId) {

memberRepository.deleteById(memberId);
}

public String updateMemberProfile(
final MultipartFile image,
final Long memberId,
final Member member
) {
if (member.isNotMe(memberId)) {
throw new MemberException(MemberExceptionType.FORBIDDEN_UPDATE_PROFILE_IMAGE);
}

if (member.isNotGithubProfile()) {
final String imageName = s3Client.convertImageName(member.getImageUrl());
s3Client.deleteImages(List.of(imageName));
}

final String imageName = s3Client.uploadImage(image);
final String imageUrl = s3Client.convertImageUrl(imageName);
member.updateProfile(imageUrl);

return imageUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {

private static final String GITHUB_PROFILE_DOMAIN = "https://avatars.githubusercontent.com";
private static final int MAX_DESCRIPTION_LENGTH = 100;
private static final String DEFAULT_DESCRIPTION = "";

Expand All @@ -37,7 +38,6 @@ public class Member extends BaseEntity {
@Column(nullable = false)
private String githubUsername;


public Member(final Long id, final Long githubId, final String imageUrl, final String name,
final String githubUsername) {
this.id = id;
Expand Down Expand Up @@ -73,6 +73,10 @@ public void updateDescription(final String description) {
this.description = description;
}

public void updateProfile(final String imageUrl) {
this.imageUrl = imageUrl;
}

private void validateDescriptionNull(final String description) {
if (description == null) {
throw new MemberException(MemberExceptionType.NULL_DESCRIPTION);
Expand All @@ -97,7 +101,6 @@ public boolean isNotMe(final Member member) {
return isNotMe(member.getId());
}


public boolean isNotMe(final Long id) {
return !this.id.equals(id);
}
Expand All @@ -106,6 +109,10 @@ public boolean isOnboarded() {
return name != null;
}

public boolean isNotGithubProfile() {
return !imageUrl.startsWith(GITHUB_PROFILE_DOMAIN);
}

public Optional<String> getOptionalOpenProfileUrl() {
return Optional.ofNullable(openProfileUrl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public enum MemberExceptionType implements BaseExceptionType {
NOT_MATCHING_TOKEN_AND_LOGIN_MEMBER(
HttpStatus.UNAUTHORIZED,
"사용자가 일치하지 않습니다."
),

FORBIDDEN_UPDATE_PROFILE_IMAGE(
HttpStatus.FORBIDDEN,
"해당하는 멤버의 프로필을 바꿀 권한이 없습니다."
);

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.emmsale.helper;

import com.emmsale.image.application.ImageCommandService;
import com.emmsale.image.application.S3Client;
import com.emmsale.login.utils.GithubClient;
import com.emmsale.notification.application.FirebaseCloudMessageClient;
import org.springframework.boot.test.context.SpringBootTest;
Expand All @@ -18,4 +19,6 @@ public abstract class ServiceIntegrationTestHelper {
protected FirebaseCloudMessageClient firebaseCloudMessageClient;
@MockBean
protected ImageCommandService imageCommandService;
@MockBean
protected S3Client s3Client;
}
Loading

0 comments on commit 1978ad0

Please sign in to comment.