diff --git a/src/docs/asciidoc/api/member.adoc b/src/docs/asciidoc/api/member.adoc index 494bd3833..189e2dd39 100644 --- a/src/docs/asciidoc/api/member.adoc +++ b/src/docs/asciidoc/api/member.adoc @@ -51,3 +51,30 @@ include::{snippets}/member-delete/request-fields.adoc[] include::{snippets}/member-delete/http-response.adoc[] include::{snippets}/member-delete/response-fields.adoc[] + +[[member-signup]] +=== 일반 회원가입 + +==== HTTP Request + +include::{snippets}/member-signup/http-request.adoc[] +include::{snippets}/member-signup/request-parts.adoc[] + +==== HTTP Response + +include::{snippets}/member-signup/http-response.adoc[] +include::{snippets}/member-signup/response-fields.adoc[] + +[[member-update]] +=== 회원 프로필 수정 + +==== HTTP Request + +include::{snippets}/member-update/http-request.adoc[] +include::{snippets}/member-update/request-headers.adoc[] +include::{snippets}/member-update/request-parts.adoc[] + +==== HTTP Response + +include::{snippets}/member-update/http-response.adoc[] +include::{snippets}/member-update/response-fields.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 376b06779..76d7235ba 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -10,30 +10,30 @@ endif::[] :sectlinks: [[Product-API]] -== 회원 API +== 회원 include::api/member.adoc[] -== 포트폴리오 API +== 포트폴리오 include::api/portfolio.adoc[] -== 포트폴리오 종목 API +== 포트폴리오 종목 include::api/holding.adoc[] -== FCM 토큰 API +== FCM 토큰 include::api/fcm.adoc[] -== 종목 지정가 알림 API +== 종목 지정가 알림 include::api/stock_target_price.adoc[] -== 알림 API +== 알림 include::api/notification.adoc[] -== 대시보드 API +== 대시보드 include::api/dashboard.adoc[] diff --git a/src/main/java/codesquad/fineants/domain/member/MemberRepository.java b/src/main/java/codesquad/fineants/domain/member/MemberRepository.java index 3410bbc5c..b39f9da6a 100644 --- a/src/main/java/codesquad/fineants/domain/member/MemberRepository.java +++ b/src/main/java/codesquad/fineants/domain/member/MemberRepository.java @@ -10,6 +10,10 @@ public interface MemberRepository extends JpaRepository { Optional findMemberByEmailAndProvider(String email, String provider); + @Query("select m from Member m where m.nickname = :nickname and m.id != :memberId") + Optional findMemberByNicknameAndNotMemberId(@Param("nickname") String nickname, + @Param("memberId") Long memberId); + boolean existsMemberByEmailAndProvider(String email, String provider); boolean existsByNickname(String nickname); diff --git a/src/main/java/codesquad/fineants/spring/api/S3/service/AmazonS3Service.java b/src/main/java/codesquad/fineants/spring/api/S3/service/AmazonS3Service.java index 6acc9042b..283b68e77 100644 --- a/src/main/java/codesquad/fineants/spring/api/S3/service/AmazonS3Service.java +++ b/src/main/java/codesquad/fineants/spring/api/S3/service/AmazonS3Service.java @@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.PutObjectRequest; @@ -58,4 +59,20 @@ private Optional convertMultiPartFileToFile(MultipartFile file) { return Optional.of(convertedFile); } + + public void deleteFile(String url) { + try { + String fileName = extractFileName(url); + amazonS3.deleteObject(bucketName, fileName); + } catch (AmazonServiceException e) { + log.error(e.getMessage()); + } + } + + // URL에서 파일 이름 추출하는 메소드 + private String extractFileName(String url) { + // 예시: https://fineants.s3.ap-northeast-2.amazonaws.com/9d07ee41-4404-414b-9ee7-12616aa6bedcprofile.jpeg + int lastSlashIndex = url.lastIndexOf('/'); + return url.substring(lastSlashIndex + 1); + } } diff --git a/src/main/java/codesquad/fineants/spring/api/errors/errorcode/MemberErrorCode.java b/src/main/java/codesquad/fineants/spring/api/errors/errorcode/MemberErrorCode.java index 4139da206..1b32432a5 100644 --- a/src/main/java/codesquad/fineants/spring/api/errors/errorcode/MemberErrorCode.java +++ b/src/main/java/codesquad/fineants/spring/api/errors/errorcode/MemberErrorCode.java @@ -22,7 +22,8 @@ public enum MemberErrorCode implements ErrorCode { IMAGE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지 사이즈 제한을 초과했습니다."), LOGIN_FAIL(HttpStatus.BAD_REQUEST, "로그인에 실패하였습니다."), FORBIDDEN_MEMBER(HttpStatus.FORBIDDEN, "권한이 없습니다."), - NO_PROFILE_CHANGES(HttpStatus.BAD_REQUEST, "변경할 회원 정보가 없습니다"); + NO_PROFILE_CHANGES(HttpStatus.BAD_REQUEST, "변경할 회원 정보가 없습니다"), + BAD_REQUEST_PROFILE_URL(HttpStatus.BAD_REQUEST, "회원의 프로필 URL과 요청 프로필 URL이 일치하지 않습니다"); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/codesquad/fineants/spring/api/member/controller/MemberRestController.java b/src/main/java/codesquad/fineants/spring/api/member/controller/MemberRestController.java index d6531d307..305659c09 100644 --- a/src/main/java/codesquad/fineants/spring/api/member/controller/MemberRestController.java +++ b/src/main/java/codesquad/fineants/spring/api/member/controller/MemberRestController.java @@ -126,13 +126,16 @@ public ApiResponse login(@RequestBody final LoginRequest request) return ApiResponse.success(MemberSuccessCode.OK_LOGIN, memberService.login(request)); } - @PutMapping(value = "/profile", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @PostMapping(value = "/profile", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ApiResponse changeProfile( @RequestPart(value = "profileImageFile", required = false) MultipartFile profileImageFile, @Valid @RequestPart(value = "profileInformation", required = false) ProfileChangeRequest request, @AuthPrincipalMember AuthMember authMember) { - ProfileChangeServiceRequest serviceRequest = new ProfileChangeServiceRequest(profileImageFile, - request, authMember); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + profileImageFile, + request, + authMember.getMemberId() + ); return ApiResponse.success(MemberSuccessCode.OK_MODIFIED_PROFILE, memberService.changeProfile(serviceRequest)); } diff --git a/src/main/java/codesquad/fineants/spring/api/member/service/MemberService.java b/src/main/java/codesquad/fineants/spring/api/member/service/MemberService.java index 8f3ed4fe6..09232a5c0 100644 --- a/src/main/java/codesquad/fineants/spring/api/member/service/MemberService.java +++ b/src/main/java/codesquad/fineants/spring/api/member/service/MemberService.java @@ -59,7 +59,6 @@ import codesquad.fineants.spring.api.member.request.OauthMemberLoginRequest; import codesquad.fineants.spring.api.member.request.OauthMemberLogoutRequest; import codesquad.fineants.spring.api.member.request.OauthMemberRefreshRequest; -import codesquad.fineants.spring.api.member.request.ProfileChangeRequest; import codesquad.fineants.spring.api.member.request.VerifyCodeRequest; import codesquad.fineants.spring.api.member.request.VerifyEmailRequest; import codesquad.fineants.spring.api.member.response.LoginResponse; @@ -226,7 +225,11 @@ public SignUpServiceResponse signup(SignUpServiceRequest request) { verifyPassword(request.getPassword(), request.getPasswordConfirm()); // 프로필 이미지 파일 S3에 업로드 - String profileUrl = uploadProfileImageFile(request.getProfileImageFile()); + String profileUrl = null; + if (request.getProfileImageFile() != null && !request.getProfileImageFile().isEmpty()) { + profileUrl = uploadProfileImageFile(request.getProfileImageFile()); + } + // 비밀번호 암호화 String encryptedPassword = encryptPassword(request.getPassword()); // 회원 데이터베이스 저장 @@ -260,7 +263,14 @@ private void verifyEmail(String email) { private void verifyNickname(String nickname) { if (memberRepository.existsByNickname(nickname)) { - throw new BadRequestException(MemberErrorCode.REDUNDANT_NICKNAME); + throw new FineAntsException(MemberErrorCode.REDUNDANT_NICKNAME); + } + } + + // memberId을 제외한 다른 nickname이 존재하는지 검증 + private void verifyNickname(String nickname, Long memberId) { + if (memberRepository.findMemberByNicknameAndNotMemberId(nickname, memberId).isPresent()) { + throw new FineAntsException(MemberErrorCode.REDUNDANT_NICKNAME); } } @@ -315,40 +325,51 @@ public LoginResponse login(LoginRequest request) { } @Transactional - public ProfileChangeResponse changeProfile(ProfileChangeServiceRequest serviceRequest) { - verifyNoProfileChanges(serviceRequest); - Member member = findMemberById(serviceRequest.getMemberId()); - extractMemberProfileImage(serviceRequest) - .map(amazonS3Service::upload) - .ifPresent(member::updateProfileUrl); - extractNickname(serviceRequest) - .ifPresent(member::updateNickname); - return ProfileChangeResponse.from(member); - } - - private void verifyNoProfileChanges(ProfileChangeServiceRequest serviceRequest) { - if (serviceRequest.getProfileImageFile().isEmpty() && serviceRequest.getRequest().isEmpty()) { - throw new BadRequestException(MemberErrorCode.NO_PROFILE_CHANGES); + public ProfileChangeResponse changeProfile(ProfileChangeServiceRequest request) { + Member member = findMemberById(request.getMemberId()); + MultipartFile profileImageFile = request.getProfileImageFile(); + String profileUrl = null; + + // 변경할 정보가 없는 경우 + if (profileImageFile == null && request.getNickname().isBlank()) { + throw new FineAntsException(MemberErrorCode.NO_PROFILE_CHANGES); } - } - private Member findMemberById(Optional optionalMemberId) { - return optionalMemberId - .flatMap(memberRepository::findById) - .orElseThrow(() -> new BadRequestException(MemberErrorCode.NOT_FOUND_MEMBER)); - } + // 기존 프로필 파일 유지 + if (profileImageFile == null) { + profileUrl = member.getProfileUrl(); + } + // 기본 프로필 파일로 변경인 경우 + else if (profileImageFile.isEmpty()) { + // 회원의 기존 프로필 사진 제거 + // 기존 프로필 파일 삭제 + if (member.getProfileUrl() != null) { + amazonS3Service.deleteFile(member.getProfileUrl()); + } + } + // 새로운 프로필 파일로 변경인 경우 + else if (!profileImageFile.isEmpty()) { + // 기존 프로필 파일 삭제 + if (member.getProfileUrl() != null) { + amazonS3Service.deleteFile(member.getProfileUrl()); + } + + // 새로운 프로필 파일 저장 + profileUrl = amazonS3Service.upload(profileImageFile); + } + member.updateProfileUrl(profileUrl); - private Optional extractMemberProfileImage(ProfileChangeServiceRequest serviceRequest) { - return serviceRequest.getProfileImageFile(); + if (!request.getNickname().isBlank()) { + String nickname = request.getNickname(); + verifyNickname(nickname, member.getId()); + member.updateNickname(nickname); + } + return ProfileChangeResponse.from(member); } - private Optional extractNickname(ProfileChangeServiceRequest serviceRequest) { - return serviceRequest.getRequest() - .map(ProfileChangeRequest::getNickname) - .map(nickname -> { - verifyNickname(nickname); - return nickname; - }); + private Member findMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new FineAntsException(MemberErrorCode.NOT_FOUND_MEMBER)); } @Transactional diff --git a/src/main/java/codesquad/fineants/spring/api/member/service/request/ProfileChangeServiceRequest.java b/src/main/java/codesquad/fineants/spring/api/member/service/request/ProfileChangeServiceRequest.java index e425c0149..1e196b5b1 100644 --- a/src/main/java/codesquad/fineants/spring/api/member/service/request/ProfileChangeServiceRequest.java +++ b/src/main/java/codesquad/fineants/spring/api/member/service/request/ProfileChangeServiceRequest.java @@ -1,23 +1,27 @@ package codesquad.fineants.spring.api.member.service.request; -import java.util.Optional; - +import org.apache.logging.log4j.util.Strings; import org.springframework.web.multipart.MultipartFile; -import codesquad.fineants.domain.oauth.support.AuthMember; import codesquad.fineants.spring.api.member.request.ProfileChangeRequest; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; @Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString public class ProfileChangeServiceRequest { - private final Optional profileImageFile; - private final Optional request; - private final Optional memberId; + private MultipartFile profileImageFile; + private String nickname; + private Long memberId; - public ProfileChangeServiceRequest(MultipartFile profileImageFile, ProfileChangeRequest request, - AuthMember authMember) { - this.profileImageFile = Optional.ofNullable(profileImageFile); - this.request = Optional.ofNullable(request); - this.memberId = Optional.of(authMember.getMemberId()); + public static ProfileChangeServiceRequest of(MultipartFile profileImageFile, ProfileChangeRequest request, + Long memberId) { + String nickname = request != null ? request.getNickname() : Strings.EMPTY; + return new ProfileChangeServiceRequest(profileImageFile, nickname, memberId); } } diff --git a/src/main/resources/stocks.tsv b/src/main/resources/stocks.tsv index 873f8ca4a..d4f255cbe 100755 --- a/src/main/resources/stocks.tsv +++ b/src/main/resources/stocks.tsv @@ -60,7 +60,6 @@ KR7353810005 353810 이지바이오 EASY BIO, Inc. KOSDAQ KR7029960002 029960 코엔텍 Korea Environment Technology Co., LTD. KOSDAQ KR7060590007 060590 씨티씨바이오 CTC BIO, INC. KOSDAQ KR7403490006 403490 농업회사법인 우듬지팜 WooDeumGee Farm Co., Ltd KOSDAQ -KR7204210009 204210 모두투어리츠보통주 MODETOURREIT KOSPI KR7291810000 291810 핀텔 Pintel Co., Ltd. KOSDAQ KR7008250003 008250 이건산업보통주 EagonIndustrial KOSPI KR7378850002 378850 화승알앤에이보통주 HWASEUNG R&A CO., LTD. KOSPI @@ -78,6 +77,7 @@ KR7040610008 040610 에스지엔지 SG&G Corporation KOSDAQ KR7021240007 021240 코웨이보통주 COWAY KOSPI KR7039420005 039420 케이엘넷 KL-Net Corp. KOSDAQ KR7123570004 123570 이엠넷 EMNET INC. KOSDAQ +KR7360350003 360350 코셈 COXEM CO.,LTD KOSDAQ KR7454750001 454750 하나28호기업인수목적 Hana 28 Special Purpose Acquisition Company KOSDAQ KR7200880003 200880 서연이화보통주 SEOYONEHWA KOSPI KR7194480000 194480 데브시스터즈 Devsisters corporation KOSDAQ @@ -260,7 +260,6 @@ KR7339770000 339770 교촌에프앤비보통주 KYOCHON FOOD&BEVERAGE KOSPI KR7133750000 133750 메가엠디 MegaMD Co., Ltd. KOSDAQ KR7090150004 090150 아이윈 iWIN KOSDAQ KR7193250008 193250 와이제이엠게임즈 YJM Games Co., Ltd. KOSDAQ -KR7001880004 001880 DL건설보통주 DLConstruction KOSPI KR7004960001 004960 한신공영보통주 HanshinConstruction KOSPI KR7097800007 097800 윈팩 WINPAC INC. KOSDAQ KR7002030005 002030 아세아보통주 ASIA HOLDINGS KOSPI @@ -639,6 +638,7 @@ KR7257990002 257990 나우코스 NOWCOS KONEX KR7194370003 194370 제이에스코퍼레이션보통주 JS Corporation KOSPI KR7277880001 277880 티에스아이 TSI Co., Ltd. KOSDAQ KR7120110002 120110 코오롱인더스트리보통주 KOLON INDUSTRIES KOSPI +KR7418620001 418620 이에이트 E8IGHT Co., Ltd KOSDAQ KR7086060001 086060 진바이오텍 Gene Bio Tech Co., Ltd. KOSDAQ KR7377030002 377030 맥스트 MAXST CO., LTD KOSDAQ KR7078160009 078160 메디포스트 MEDIPOST CO., LTD. KOSDAQ @@ -838,7 +838,6 @@ KR7021880000 021880 메이슨캐피탈 MASON CAPITAL CORPORATION KOSDAQ KR7372320002 372320 큐로셀 Curocell Inc. KOSDAQ KR7215570003 215570 크로넥스 CRONEX KONEX KR7058730003 058730 다스코보통주 Development Advance Solution KOSPI -KR7039980008 039980 리노스 LEENOS CORP. KOSDAQ KR7020150009 020150 롯데에너지머티리얼즈보통주 LOTTE ENERGY MATERIALS CORPORATION KOSPI KR7103590006 103590 일진전기 보통주 ILJIN ELECTRIC KOSPI KR7154030001 154030 농업회사법인 아시아종묘 ASIA SEED Co.,Ltd. KOSDAQ @@ -1382,6 +1381,7 @@ KR7236810008 236810 엔비티 NBT INC. KOSDAQ KR7347700007 347700 라이프시맨틱스 Life Semantics Corp. KOSDAQ KR7000050005 000050 경방보통주 Kyungbang KOSPI KR7007860000 007860 서연보통주 SEOYON KOSPI +KR7204210009 204210 스타자기관리부동산투자회사보통주 STAR Real Estate Investment Trust Incorporated KOSPI KR7262260003 262260 에이프로 A PRO Co., LTD KOSDAQ KR7303530000 303530 이노뎁 INNODEP INC. KOSDAQ KR7180400004 180400 디엑스앤브이엑스 Dx & Vx KOSDAQ @@ -1880,6 +1880,7 @@ KR7006401004 006405 삼성SDI1우선주 SAMSUNG SDI(1P) KOSPI KR7042600007 042600 새로닉스 SERONICS. CO., LTD. KOSDAQ KR7271830002 271830 팸텍 PAMTEK CO., LTD. KOSDAQ KR7068240001 068240 다원시스 DAWONSYS Co., LTD KOSDAQ +KR7473050003 473050 유안타제15호기업인수목적 Yuanta 15 SPECIAL PURPOSE ACQUISITION COMPANY KOSDAQ KR7053260006 053260 금강철강 KEUM KANG STEEL CO., LTD KOSDAQ KR7041510009 041510 에스엠엔터테인먼트 SM ENTERTAINMENT CO., Ltd. KOSDAQ KR7068790005 068790 디엠에스 DMS Co.,Ltd. KOSDAQ @@ -2211,6 +2212,7 @@ KR7446150005 446150 유안타제12호기업인수목적 Yuanta 12 SPECIAL PURPOS KR7036420008 036420 콘텐트리중앙보통주 ContentreeJoongAng corp. KOSPI KR7001530005 001530 DI동일보통주 DI DONGIL KOSPI KR7196700009 196700 웹스 WAPS Co., Ltd. KOSDAQ +KR7039980008 039980 폴라리스에이아이 Polaris AI KOSDAQ KR7181710005 181710 NHN보통주 NHN KOSPI KR7255220006 255220 에스지이 SG CO., LTD. KOSDAQ KR7023770001 023770 플레이위드코리아 PLAYWITH KOREA KOSDAQ @@ -2229,6 +2231,7 @@ KR7001800002 001800 오리온홀딩스보통주 ORION Holdings KOSPI KR7003240009 003240 태광산업보통주 TaekwangIndustrial KOSPI KR7053950002 053950 경남제약 KYUNG NAM PHARM.CO.,LTD. KOSDAQ KR7088290002 088290 이원컴포텍 EWON COMFORTECH CO., LTD KOSDAQ +KR7278470000 278470 에이피알보통주 APR KOSPI KR7103140000 103140 풍산 보통주 POONGSAN CORPORATION KOSPI KR7256840000 256840 한국비엔씨 BNC Korea Co, Ltd KOSDAQ KR7307180000 307180 아이엘사이언스 IL SCIENCE CO.,LTD. KOSDAQ @@ -2687,6 +2690,7 @@ KR7106190002 106190 하이텍팜 HIGH TECH PHARM CO., LTD. KOSDAQ KR7074610007 074610 이엔플러스보통주 ENPLUS KOSPI KR7238090005 238090 앤디포스 NDFOS Co., Ltd. KOSDAQ KR7048770002 048770 티피씨메카트로닉스 TPC Mechatronics Corporation KOSDAQ +KR7472230002 472230 에스케이증권제11호기업인수목적 SK Securities No.11 Special Purpose Acquisition Company KOSDAQ KR7003010006 003010 혜인보통주 HaeinCorporation KOSPI KR7049830003 049830 승일 SEUNG IL Corporation KOSDAQ KR7134790005 134790 시디즈보통주 Sidiz,Inc KOSPI @@ -2772,6 +2776,7 @@ KR7217910009 217910 에스제이켐 SJ-CHEM KONEX KR7352770002 352770 클리노믹스 Clinomics Inc. KOSDAQ KR7006380000 006380 카프로보통주 CAPRO KOSPI KR7251370003 251370 와이엠티 YMT CO., LTD. KOSDAQ +KR7468760004 468760 유진기업인수목적10호 EUGENE SPECIAL PURPOSE ACQUISITION 10 COMPANY KOSDAQ KR7200470003 200470 에이팩트 APACT Co., Ltd. KOSDAQ KR7203450002 203450 유니온커뮤니티 Union community Co., Ltd. KOSDAQ KR7190510008 190510 나무가 Namuga Co.,Ltd KOSDAQ diff --git a/src/test/java/codesquad/fineants/spring/api/member/controller/MemberRestControllerTest.java b/src/test/java/codesquad/fineants/spring/api/member/controller/MemberRestControllerTest.java index 35e01b592..980cf8bdd 100644 --- a/src/test/java/codesquad/fineants/spring/api/member/controller/MemberRestControllerTest.java +++ b/src/test/java/codesquad/fineants/spring/api/member/controller/MemberRestControllerTest.java @@ -174,24 +174,24 @@ void login() throws Exception { .andExpect(jsonPath("data.user.profileUrl").value(equalTo(member.getProfileUrl()))); } - @DisplayName("사용자는 회원의 프로필 및 닉네임을 변경한다") + @DisplayName("사용자는 회원의 프로필에서 새 프로필 및 닉네임을 수정한다") @Test void changeProfile() throws Exception { // given - Member member = createMember("일개미12345", "changeProfileUrl"); + Member member = createMember("일개미12345", "profileUrl"); given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) .willReturn(ProfileChangeResponse.from(member)); Map profileInformationMap = Map.of("nickname", "일개미12345"); - String json = ObjectMapperUtil.serialize(profileInformationMap); MockMultipartFile profileInformation = new MockMultipartFile( "profileInformation", "profileInformation", MediaType.APPLICATION_JSON_VALUE, - json.getBytes(StandardCharsets.UTF_8)); + ObjectMapperUtil.serialize(profileInformationMap) + .getBytes(StandardCharsets.UTF_8)); // when & then - mockMvc.perform(multipart(PUT, "/api/profile") + mockMvc.perform(multipart(POST, "/api/profile") .file((MockMultipartFile)createMockMultipartFile()) .file(profileInformation)) .andExpect(status().isOk()) @@ -201,22 +201,104 @@ void changeProfile() throws Exception { .andExpect(jsonPath("data.user.id").value(equalTo(1))) .andExpect(jsonPath("data.user.nickname").value(equalTo("일개미12345"))) .andExpect(jsonPath("data.user.email").value(equalTo("dragonbead95@naver.com"))) - .andExpect(jsonPath("data.user.profileUrl").value(equalTo("changeProfileUrl"))); + .andExpect(jsonPath("data.user.profileUrl").value(equalTo("profileUrl"))); + } + + @DisplayName("사용자는 회원의 프로필에서 새 프로필만 수정한다") + @Test + void changeProfile_whenNewProfile_thenOK() throws Exception { + // given + Member member = createMember("일개미12345", "profileUrl"); + given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) + .willReturn(ProfileChangeResponse.from(member)); + + // when & then + mockMvc.perform(multipart(POST, "/api/profile") + .file((MockMultipartFile)createMockMultipartFile())) + .andExpect(status().isOk()) + .andExpect(jsonPath("code").value(equalTo(200))) + .andExpect(jsonPath("status").value(equalTo("OK"))) + .andExpect(jsonPath("message").value(equalTo("프로필이 수정되었습니다"))) + .andExpect(jsonPath("data.user.id").value(equalTo(1))) + .andExpect(jsonPath("data.user.nickname").value(equalTo("일개미12345"))) + .andExpect(jsonPath("data.user.email").value(equalTo("dragonbead95@naver.com"))) + .andExpect(jsonPath("data.user.profileUrl").value(equalTo("profileUrl"))); } - @DisplayName("사용자는 아무 정보도 전달하지 않고 회원 프로필 변경 요청시 에러를 응답한다") + @DisplayName("사용자는 회원의 프로필에서 기본 프로필로 수정한다") @Test - void changeProfile_whenNotExistInput_thenResponse400Error() throws Exception { + void changeProfile_whenEmptyProfile_thenOK() throws Exception { // given + Member member = createMember("일개미12345", null); given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) - .willThrow(new BadRequestException(MemberErrorCode.NO_PROFILE_CHANGES)); + .willReturn(ProfileChangeResponse.from(member)); // when & then - mockMvc.perform(multipart(PUT, "/api/profile")) + mockMvc.perform(multipart(POST, "/api/profile") + .file((MockMultipartFile)createEmptyMockMultipartFile())) + .andExpect(status().isOk()) + .andExpect(jsonPath("code").value(equalTo(200))) + .andExpect(jsonPath("status").value(equalTo("OK"))) + .andExpect(jsonPath("message").value(equalTo("프로필이 수정되었습니다"))) + .andExpect(jsonPath("data.user.id").value(equalTo(1))) + .andExpect(jsonPath("data.user.nickname").value(equalTo("일개미12345"))) + .andExpect(jsonPath("data.user.email").value(equalTo("dragonbead95@naver.com"))) + .andExpect(jsonPath("data.user.profileUrl").value(equalTo(null))); + } + + @DisplayName("사용자는 회원의 프로필에서 프로필을 유지하고 닉네임만 변경한다") + @Test + void changeProfile_whenOnlyChangeNickname_thenOK() throws Exception { + // given + Member member = createMember("일개미1234", "profileUrl"); + given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) + .willReturn(ProfileChangeResponse.from(member)); + + Map profileInformationMap = Map.of("nickname", "일개미1234"); + MockMultipartFile profileInformation = new MockMultipartFile( + "profileInformation", + "profileInformation", + MediaType.APPLICATION_JSON_VALUE, + ObjectMapperUtil.serialize(profileInformationMap) + .getBytes(StandardCharsets.UTF_8)); + // when & then + mockMvc.perform(multipart(POST, "/api/profile") + .file(profileInformation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("code").value(equalTo(200))) + .andExpect(jsonPath("status").value(equalTo("OK"))) + .andExpect(jsonPath("message").value(equalTo("프로필이 수정되었습니다"))) + .andExpect(jsonPath("data.user.id").value(equalTo(1))) + .andExpect(jsonPath("data.user.nickname").value(equalTo("일개미1234"))) + .andExpect(jsonPath("data.user.email").value(equalTo("dragonbead95@naver.com"))) + .andExpect(jsonPath("data.user.profileUrl").value(equalTo("profileUrl"))); + } + + @DisplayName("사용자는 회원의 프로필에서 닉네임 입력 형식이 유효하지 않아 실패한다") + @Test + void changeProfile_whenInvalidNickname_thenResponse400() throws Exception { + // given + Member member = createMember("일개미1234", "profileUrl"); + given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) + .willReturn(ProfileChangeResponse.from(member)); + + Map profileInformationMap = Map.of("nickname", ""); + MockMultipartFile profileInformation = new MockMultipartFile( + "profileInformation", + "profileInformation", + MediaType.APPLICATION_JSON_VALUE, + ObjectMapperUtil.serialize(profileInformationMap) + .getBytes(StandardCharsets.UTF_8)); + // when & then + mockMvc.perform(multipart(POST, "/api/profile") + .file((MockMultipartFile)createMockMultipartFile()) + .file(profileInformation)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("code").value(equalTo(400))) .andExpect(jsonPath("status").value(equalTo("Bad Request"))) - .andExpect(jsonPath("message").value(equalTo("변경할 회원 정보가 없습니다"))); + .andExpect(jsonPath("message").value(equalTo("잘못된 입력형식입니다"))) + .andExpect(jsonPath("data[0].field").value(equalTo("nickname"))) + .andExpect(jsonPath("data[0].defaultMessage").value(equalTo("잘못된 입력형식입니다."))); } @DisplayName("사용자는 일반 회원가입을 한다") @@ -248,7 +330,7 @@ void signup() throws Exception { .andExpect(jsonPath("message").value(equalTo("회원가입이 완료되었습니다"))); } - @DisplayName("사용자는 프로필을 건너뛰고 회원가입 할 수 있다") + @DisplayName("사용자는 기본 프로필 사진으로 회원가입 할 수 있다") @Test void signup_whenSkipProfileImageFile_then200OK() throws Exception { // given @@ -266,7 +348,6 @@ void signup_whenSkipProfileImageFile_then200OK() throws Exception { "signupData", MediaType.APPLICATION_JSON_VALUE, json.getBytes(StandardCharsets.UTF_8)); - // when & then mockMvc.perform(multipart(POST, "/api/auth/signup") .file(signupData)) @@ -602,6 +683,10 @@ public MultipartFile createMockMultipartFile() { } } + public MultipartFile createEmptyMockMultipartFile() { + return new MockMultipartFile("profileImageFile", new byte[] {}); + } + public static Stream invalidSignupData() { return Stream.of( Arguments.of("", "", "", ""), diff --git a/src/test/java/codesquad/fineants/spring/api/member/service/MemberServiceTest.java b/src/test/java/codesquad/fineants/spring/api/member/service/MemberServiceTest.java index d88c77537..ba87d013c 100644 --- a/src/test/java/codesquad/fineants/spring/api/member/service/MemberServiceTest.java +++ b/src/test/java/codesquad/fineants/spring/api/member/service/MemberServiceTest.java @@ -300,60 +300,158 @@ void loginWithNaver(String provider, String email) { ); } - @DisplayName("사용자는 회원의 프로필을 변경한다") - @MethodSource(value = "changeProfileMethodSource") - @ParameterizedTest - void changeProfile(MultipartFile profileImageFile, ProfileChangeRequest request) { + @DisplayName("사용자는 회원의 프로필에서 새 프로필 사진과 닉네임을 변경한다") + @Test + void changeProfile() { // given Member member = memberRepository.save(createMember()); - ProfileChangeServiceRequest serviceRequest = new ProfileChangeServiceRequest(profileImageFile, request, - AuthMember.from(member)); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + createProfileFile(), + new ProfileChangeRequest("nemo12345"), + member.getId() + ); + given(amazonS3Service.upload(any(MultipartFile.class))) + .willReturn("profileUrl"); // when ProfileChangeResponse response = memberService.changeProfile(serviceRequest); // then - Member findMember = memberRepository.findById(member.getId()).orElseThrow(); - ProfileChangeResponse expected = ProfileChangeResponse.from(findMember); - assertThat(response).isEqualTo(expected); + assertThat(response) + .extracting("user") + .extracting("id", "nickname", "email", "profileUrl") + .containsExactlyInAnyOrder(member.getId(), "nemo12345", "dragonbead95@naver.com", "profileUrl"); } - @DisplayName("사용자가 변경할 정보 없이 프로필을 변경하는 경우 예외가 발생한다") + @DisplayName("사용자는 회원 프로필에서 새 프로필만 변경한다") @Test - void changeProfile_whenNoChangeInformation_thenResponse400Error() { + void changeProfile_whenNewProfile_thenOK() { // given Member member = memberRepository.save(createMember()); - MultipartFile profileImageFile = null; - ProfileChangeRequest request = null; - ProfileChangeServiceRequest serviceRequest = new ProfileChangeServiceRequest(profileImageFile, request, - AuthMember.from(member)); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + createProfileFile(), + null, + member.getId() + ); + + given(amazonS3Service.upload(any(MultipartFile.class))) + .willReturn("profileUrl"); + // when + ProfileChangeResponse response = memberService.changeProfile(serviceRequest); + + // then + assertThat(response) + .extracting("user") + .extracting("id", "nickname", "email", "profileUrl") + .containsExactlyInAnyOrder(member.getId(), "nemo1234", "dragonbead95@naver.com", "profileUrl"); + } + + @DisplayName("사용자는 회원 프로필에서 기본 프로필로만 변경한다") + @Test + void changeProfile_whenEmptyProfile_thenOK() { + // given + Member member = memberRepository.save(createMember()); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + createEmptyProfileImageFile(), + null, + member.getId() + ); + + given(amazonS3Service.upload(any(MultipartFile.class))) + .willReturn("profileUrl"); + // when + ProfileChangeResponse response = memberService.changeProfile(serviceRequest); + + // then + assertThat(response) + .extracting("user") + .extracting("id", "nickname", "email", "profileUrl") + .containsExactlyInAnyOrder(member.getId(), "nemo1234", "dragonbead95@naver.com", null); + } + + @DisplayName("사용자는 회원 프로필에서 프로필은 유지하고 닉네임만 변경한다") + @Test + void changeProfile_whenChangeNickname_thenOK() { + // given + Member member = memberRepository.save(createMember()); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + null, + new ProfileChangeRequest("nemo12345"), + member.getId() + ); + + // when + ProfileChangeResponse response = memberService.changeProfile(serviceRequest); + + // then + assertThat(response) + .extracting("user") + .extracting("id", "nickname", "email", "profileUrl") + .containsExactlyInAnyOrder(member.getId(), "nemo12345", "dragonbead95@naver.com", "profileUrl"); + } + + @DisplayName("사용자는 회원 프로필에서 자기 닉네임을 그대로 수정한다") + @Test + void changeProfile_whenNoChangeNickname_thenOK() { + // given + Member member = memberRepository.save(createMember()); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + createProfileFile(), + new ProfileChangeRequest("nemo1234"), + member.getId() + ); + + given(amazonS3Service.upload(any(MultipartFile.class))) + .willReturn("profileUrl"); + + // when + ProfileChangeResponse response = memberService.changeProfile(serviceRequest); + + // then + assertThat(response) + .extracting("user") + .extracting("id", "nickname", "email", "profileUrl") + .containsExactlyInAnyOrder(member.getId(), "nemo1234", "dragonbead95@naver.com", "profileUrl"); + } + + @DisplayName("사용자는 회원 프로필에서 닉네임 변경시 중복되어 변경하지 못한다") + @Test + void changeProfile_whenDuplicateNickname_thenThrowException() { + // given + Member member = memberRepository.save(createMember()); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + null, + new ProfileChangeRequest("nemo1234"), + member.getId() + ); // when Throwable throwable = catchThrowable(() -> memberService.changeProfile(serviceRequest)); // then assertThat(throwable) - .isInstanceOf(BadRequestException.class) - .hasMessage("변경할 회원 정보가 없습니다"); + .isInstanceOf(FineAntsException.class) + .hasMessage(MemberErrorCode.REDUNDANT_NICKNAME.getMessage()); } - @DisplayName("사용자가 중복된 닉네임으로 프로필을 변경하려고 하면 400 에러를 응답합니다") + @DisplayName("사용자는 회원 프로필에서 변경할 정보가 없어서 실패한다") @Test - void changeProfile_whenDuplicatedNickname_thenResponse400Error() { + void changeProfile_whenNoChangeProfile_thenThrowException() { // given Member member = memberRepository.save(createMember()); - ProfileChangeServiceRequest serviceRequest = new ProfileChangeServiceRequest( - createMockMultipartFile(), - createProfileChangeRequest(member.getNickname()), - AuthMember.from(member)); + ProfileChangeServiceRequest serviceRequest = ProfileChangeServiceRequest.of( + null, + null, + member.getId() + ); // when Throwable throwable = catchThrowable(() -> memberService.changeProfile(serviceRequest)); // then assertThat(throwable) - .isInstanceOf(BadRequestException.class) - .hasMessage("닉네임이 중복되었습니다"); + .isInstanceOf(FineAntsException.class) + .hasMessage(MemberErrorCode.NO_PROFILE_CHANGES.getMessage()); } @DisplayName("사용자는 일반 회원가입한다") @@ -374,6 +472,28 @@ void signup(SignUpRequest request, MultipartFile profileImageFile, String expect .containsExactlyInAnyOrder("일개미1234", "dragonbead95@naver.com", expectedProfileUrl, "local"); } + @DisplayName("사용자는 일반 회원가입 할때 프로필 사진을 기본 프로필 사진으로 가입한다") + @Test + void signup_whenDefaultProfile_thenSaveDefaultProfileUrl() { + // given + SignUpRequest request = new SignUpRequest( + "일개미1234", + "dragonbead95@naver.com", + "nemo1234@", + "nemo1234@" + ); + MultipartFile profileImageFile = null; + SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, profileImageFile); + + // when + SignUpServiceResponse response = memberService.signup(serviceRequest); + + // then + assertThat(response) + .extracting("nickname", "email", "profileUrl", "provider") + .containsExactlyInAnyOrder("일개미1234", "dragonbead95@naver.com", null, "local"); + } + @DisplayName("사용자는 닉네임이 중복되어 회원가입 할 수 없다") @Test void signup_whenDuplicatedNickname_thenResponse400Error() { @@ -386,14 +506,14 @@ void signup_whenDuplicatedNickname_thenResponse400Error() { "nemo1234@", "nemo1234@" ); - SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createMockMultipartFile()); + SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createProfileFile()); // when Throwable throwable = catchThrowable(() -> memberService.signup(serviceRequest)); // then assertThat(throwable) - .isInstanceOf(BadRequestException.class) + .isInstanceOf(FineAntsException.class) .hasMessage(MemberErrorCode.REDUNDANT_NICKNAME.getMessage()); } @@ -409,7 +529,7 @@ void signup_whenDuplicatedEmail_thenResponse400Error() { "nemo1234@", "nemo1234@" ); - SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createMockMultipartFile()); + SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createProfileFile()); // when Throwable throwable = catchThrowable(() -> memberService.signup(serviceRequest)); @@ -431,7 +551,7 @@ void signup_whenNotMatchPasswordAndPasswordConfirm_thenResponse400Error() { "nemo1234@", "nemo4567@" ); - SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createMockMultipartFile()); + SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createProfileFile()); // when Throwable throwable = catchThrowable(() -> memberService.signup(serviceRequest)); @@ -455,7 +575,7 @@ void signup_whenOverProfileImageFile_thenResponse400Error() { "nemo1234@", "nemo1234@" ); - SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createMockMultipartFile()); + SignUpServiceRequest serviceRequest = SignUpServiceRequest.of(request, createProfileFile()); // when Throwable throwable = catchThrowable(() -> memberService.signup(serviceRequest)); @@ -653,25 +773,7 @@ private static Member createMember(String nickname, String profileUrl) { .build(); } - public static Stream changeProfileMethodSource() { - String nickname = "nemo12345"; - return Stream.of( - Arguments.of( - null, - createProfileChangeRequest(nickname) - ), - Arguments.of( - createMockMultipartFile(), - null - ), - Arguments.of( - createMockMultipartFile(), - createProfileChangeRequest(nickname) - ) - ); - } - - public static MultipartFile createMockMultipartFile() { + public static MultipartFile createProfileFile() { ClassPathResource classPathResource = new ClassPathResource("profile.jpeg"); Path path = null; try { @@ -684,6 +786,10 @@ public static MultipartFile createMockMultipartFile() { } } + private static MultipartFile createEmptyProfileImageFile() { + return new MockMultipartFile("profileImageFile", new byte[] {}); + } + @NotNull private static ProfileChangeRequest createProfileChangeRequest(String nickname) { return new ProfileChangeRequest(nickname); @@ -696,10 +802,9 @@ public static Stream signupMethodSource() { "nemo1234@", "nemo1234@" ); - MultipartFile profileImageFile = createMockMultipartFile(); + MultipartFile profileImageFile = createProfileFile(); return Stream.of( - Arguments.of(request, profileImageFile, "profileUrl"), - Arguments.of(request, null, null) + Arguments.of(request, profileImageFile, "profileUrl") ); } diff --git a/src/test/java/codesquad/fineants/spring/docs/RestDocsSupport.java b/src/test/java/codesquad/fineants/spring/docs/RestDocsSupport.java index f50bc414a..da11136a1 100644 --- a/src/test/java/codesquad/fineants/spring/docs/RestDocsSupport.java +++ b/src/test/java/codesquad/fineants/spring/docs/RestDocsSupport.java @@ -3,6 +3,10 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -12,12 +16,15 @@ import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.springframework.core.MethodParameter; +import org.springframework.core.io.ClassPathResource; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.multipart.MultipartFile; import codesquad.fineants.domain.member.Member; import codesquad.fineants.domain.oauth.support.AuthMember; @@ -173,5 +180,22 @@ protected List createStockDividendWith(Stock stock) { ); } + protected MultipartFile createMockMultipartFile() { + ClassPathResource classPathResource = new ClassPathResource("profile.jpeg"); + Path path = null; + try { + path = Paths.get(classPathResource.getURI()); + byte[] profile = Files.readAllBytes(path); + return new MockMultipartFile("profileImageFile", "profile.jpeg", "image/jpeg", + profile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected MultipartFile createEmptyMockMultipartFile() { + return new MockMultipartFile("profileImageFile", new byte[] {}); + } + protected abstract Object initController(); } diff --git a/src/test/java/codesquad/fineants/spring/docs/member/MemberRestControllerDocsTest.java b/src/test/java/codesquad/fineants/spring/docs/member/MemberRestControllerDocsTest.java index 07ff2ae1b..718b6073f 100644 --- a/src/test/java/codesquad/fineants/spring/docs/member/MemberRestControllerDocsTest.java +++ b/src/test/java/codesquad/fineants/spring/docs/member/MemberRestControllerDocsTest.java @@ -2,14 +2,16 @@ import static org.hamcrest.Matchers.*; import static org.mockito.BDDMockito.*; +import static org.springframework.http.HttpMethod.*; import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.nio.charset.StandardCharsets; import java.util.Map; import org.junit.jupiter.api.DisplayName; @@ -18,6 +20,7 @@ import org.mockito.Mockito; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.snippet.Attributes; @@ -30,7 +33,11 @@ import codesquad.fineants.spring.api.member.response.LoginResponse; import codesquad.fineants.spring.api.member.response.OauthMemberLoginResponse; import codesquad.fineants.spring.api.member.response.OauthMemberResponse; +import codesquad.fineants.spring.api.member.response.ProfileChangeResponse; import codesquad.fineants.spring.api.member.service.MemberService; +import codesquad.fineants.spring.api.member.service.request.ProfileChangeServiceRequest; +import codesquad.fineants.spring.api.member.service.request.SignUpServiceRequest; +import codesquad.fineants.spring.api.member.service.response.SignUpServiceResponse; import codesquad.fineants.spring.docs.RestDocsSupport; import codesquad.fineants.spring.util.ObjectMapperUtil; @@ -289,4 +296,126 @@ void deleteAccount() throws Exception { ); } + @DisplayName("사용자 일반 회원가입 API") + @Test + void signup() throws Exception { + // given + given(memberService.signup(ArgumentMatchers.any(SignUpServiceRequest.class))) + .willReturn(SignUpServiceResponse.from(createMember())); + + Map profileInformationMap = Map.of( + "nickname", "일개미1234", + "email", "dragonbead95@naver.com", + "password", "nemo1234@", + "passwordConfirm", "nemo1234@"); + String json = ObjectMapperUtil.serialize(profileInformationMap); + MockMultipartFile signupData = new MockMultipartFile( + "signupData", + "signupData", + MediaType.APPLICATION_JSON_VALUE, + json.getBytes(StandardCharsets.UTF_8)); + + // when & then + mockMvc.perform(multipart(POST, "/api/auth/signup") + .file((MockMultipartFile)createMockMultipartFile()) + .file(signupData)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("code").value(equalTo(201))) + .andExpect(jsonPath("status").value(equalTo("Created"))) + .andExpect(jsonPath("message").value(equalTo("회원가입이 완료되었습니다"))) + .andDo( + document( + "member-signup", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestParts( + partWithName("profileImageFile") + .optional() + .description("프로필 파일"), + partWithName("signupData") + .description("회원가입 정보") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER) + .description("코드"), + fieldWithPath("status").type(JsonFieldType.STRING) + .description("상태"), + fieldWithPath("message").type(JsonFieldType.STRING) + .description("메시지"), + fieldWithPath("data").type(JsonFieldType.NULL) + .description("응답 데이터") + ) + ) + ); + } + + @DisplayName("회원 프로필 수정 API") + @Test + void changeProfile() throws Exception { + // given + Member member = createMember(); + + Map profileInformationMap = Map.of("nickname", "일개미12345"); + MockMultipartFile profileInformation = new MockMultipartFile( + "profileInformation", + "profileInformation", + MediaType.APPLICATION_JSON_VALUE, + ObjectMapperUtil.serialize(profileInformationMap) + .getBytes(StandardCharsets.UTF_8)); + + member.updateNickname("일개미12345"); + given(memberService.changeProfile(ArgumentMatchers.any(ProfileChangeServiceRequest.class))) + .willReturn(ProfileChangeResponse.from(member)); + // when & then + mockMvc.perform(multipart(POST, "/api/profile") + .file((MockMultipartFile)createMockMultipartFile()) + .file(profileInformation) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")) + .andExpect(status().isOk()) + .andExpect(jsonPath("code").value(equalTo(200))) + .andExpect(jsonPath("status").value(equalTo("OK"))) + .andExpect(jsonPath("message").value(equalTo("프로필이 수정되었습니다"))) + .andExpect(jsonPath("data.user.id").value(equalTo(1))) + .andExpect(jsonPath("data.user.nickname").value(equalTo("일개미12345"))) + .andExpect(jsonPath("data.user.email").value(equalTo("kim1234@gmail.com"))) + .andExpect(jsonPath("data.user.profileUrl").value(equalTo("profileUrl"))) + .andDo( + document( + "member-update", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ), + requestParts( + partWithName("profileImageFile") + .optional() + .description("프로필 파일"), + partWithName("profileInformation") + .optional() + .description("회원 수정 정보") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER) + .description("코드"), + fieldWithPath("status").type(JsonFieldType.STRING) + .description("상태"), + fieldWithPath("message").type(JsonFieldType.STRING) + .description("메시지"), + fieldWithPath("data").type(JsonFieldType.OBJECT) + .description("응답 데이터"), + fieldWithPath("data.user").type(JsonFieldType.OBJECT) + .description("회원 정보"), + fieldWithPath("data.user.id").type(JsonFieldType.NUMBER) + .description("회원 등록번호"), + fieldWithPath("data.user.nickname").type(JsonFieldType.STRING) + .description("회원 닉네임"), + fieldWithPath("data.user.email").type(JsonFieldType.STRING) + .description("회원 이메일"), + fieldWithPath("data.user.profileUrl").type(JsonFieldType.STRING) + .description("회원 프로필 URL") + ) + ) + ); + } } diff --git a/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet new file mode 100644 index 000000000..f7baca958 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,9 @@ +==== Request Fields +|=== +|Path|Optional|Description +{{#requestParts}} + |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{#optional}}Y{{/optional}}{{^optional}}N{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/requestParts}} +|===