diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java index 3f9cfca2..a7c675d9 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java @@ -122,6 +122,11 @@ public ResponseEntity> refreshToken( .body(new RsData<>("200", "액세스 토큰을 재발급 했습니다.", null)); } + /** + * 확장프로그램의 액세스 토큰 발급을 위한 백그라운드 풀링에 대응하는 API + * @param state 확장프로그램 로그인 시 전달한 state 값. + */ + @GetMapping("/result") @Operation(summary = "확장프로그램 백그라운드 풀링 대응 API") public ResponseEntity> pullingResult( diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberController.java index 70b6d872..2db7f4e6 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/controller/ApiV1MemberController.java @@ -8,10 +8,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import org.tuna.zoopzoop.backend.domain.member.dto.req.ReqBodyForEditMember; import org.tuna.zoopzoop.backend.domain.member.dto.req.ReqBodyForEditMemberName; -import org.tuna.zoopzoop.backend.domain.member.dto.res.ResBodyForEditMemberName; -import org.tuna.zoopzoop.backend.domain.member.dto.res.ResBodyForGetMemberInfo; -import org.tuna.zoopzoop.backend.domain.member.dto.res.ResBodyForGetMemberInfoV2; +import org.tuna.zoopzoop.backend.domain.member.dto.req.ReqBodyForEditMemberProfileImage; +import org.tuna.zoopzoop.backend.domain.member.dto.res.*; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; import org.tuna.zoopzoop.backend.global.rsData.RsData; @@ -62,14 +62,14 @@ public ResponseEntity> getMemberInfo( * @param userDetails @AuthenticationPrincipal로 받아오는 현재 사용자 정보 * @param reqBodyForEditMemberName 수정할 닉네임을 받아오는 reqDto */ - @PutMapping("/edit") + @PutMapping("/edit/name") @Operation(summary = "사용자 닉네임 수정") public ResponseEntity> editMemberName( @AuthenticationPrincipal CustomUserDetails userDetails, @Valid @RequestBody ReqBodyForEditMemberName reqBodyForEditMemberName ) { Member member = userDetails.getMember(); - member.updateName(reqBodyForEditMemberName.newName()); + memberService.updateMemberName(member, reqBodyForEditMemberName.newName()); return ResponseEntity .status(HttpStatus.OK) .body( @@ -81,12 +81,64 @@ public ResponseEntity> editMemberName( ); } + /** + * 현재 로그인한 사용자의 프로필 이미지를 변경하는 API + * HTTP METHOD: PUT + * @param userDetails @AuthenticationPrincipal로 받아오는 현재 사용자 정보 + * @param reqBodyForEditMemberProfileImage 수정할 프로필 이미지를 받아오는 dto + */ + @PutMapping("/edit/image") + @Operation(summary = "사용자 닉네임 수정") + public ResponseEntity> editMemberProfileImage( + @AuthenticationPrincipal CustomUserDetails userDetails, + @Valid @RequestBody ReqBodyForEditMemberProfileImage reqBodyForEditMemberProfileImage + ) { + Member member = userDetails.getMember(); + memberService.updateMemberProfileUrl(member, reqBodyForEditMemberProfileImage.file()); + return ResponseEntity + .status(HttpStatus.OK) + .body( + new RsData<>( + "200", + "사용자의 프로필 이미지를 변경했습니다.", + new ResBodyForEditMemberProfileImage(member.getProfileImageUrl()) + ) + ); + } + + /** + * 현재 로그인한 사용자의 닉네임과 프로필 이미지를 변경하는 API + * HTTP METHOD: PUT + * @param userDetails @AuthenticationPrincipal로 받아오는 현재 사용자 정보 + * @param reqBodyForEditMember 수정할 프로필 정보를 받아오는 dto + */ + @PutMapping("/edit") + @Operation(summary = "사용자 프로필 수정") + public ResponseEntity> editMemberProfile( + @AuthenticationPrincipal CustomUserDetails userDetails, + @Valid @RequestBody ReqBodyForEditMember reqBodyForEditMember + ) { + Member member = userDetails.getMember(); + memberService.updateMemberProfile(member, reqBodyForEditMember.newName(), reqBodyForEditMember.file()); + return ResponseEntity + .status(HttpStatus.OK) + .body( + new RsData<>( + "200", + "사용자의 프로필을 변경했습니다.", + new ResBodyForEditMember(member.getName(), member.getProfileImageUrl()) + ) + ); + } + + /** * 현재 로그인한 사용자를 삭제하는 API * 사용할 지 모르겠음. * HTTP METHOD: DELETE * @param userDetails @AuthenticationPrincipal로 받아오는 현재 사용자 정보 */ + @DeleteMapping @Operation(summary = "사용자 삭제") public ResponseEntity> deleteMember( diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMember.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMember.java new file mode 100644 index 00000000..ee7d8f53 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMember.java @@ -0,0 +1,17 @@ +package org.tuna.zoopzoop.backend.domain.member.dto.req; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record ReqBodyForEditMember ( + @NotBlank(message = "잘못된 요청입니다.") //MethodArgumentException + String newName, + @NotNull(message = "파일을 선택해주세요.") + MultipartFile file +) { + public ReqBodyForEditMember(String newName, MultipartFile file) { + this.newName = newName; + this.file = file; + } +} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMemberProfileImage.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMemberProfileImage.java new file mode 100644 index 00000000..7075531f --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/req/ReqBodyForEditMemberProfileImage.java @@ -0,0 +1,13 @@ +package org.tuna.zoopzoop.backend.domain.member.dto.req; + +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record ReqBodyForEditMemberProfileImage ( + @NotNull(message = "파일을 선택해주세요.") + MultipartFile file +) { + public ReqBodyForEditMemberProfileImage(MultipartFile file) { + this.file = file; + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMember.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMember.java new file mode 100644 index 00000000..527acc6b --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMember.java @@ -0,0 +1,11 @@ +package org.tuna.zoopzoop.backend.domain.member.dto.res; + +public record ResBodyForEditMember( + String name, + String profileUrl +) { + public ResBodyForEditMember(String name, String profileUrl) { + this.name = name; + this.profileUrl = profileUrl; + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMemberProfileImage.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMemberProfileImage.java new file mode 100644 index 00000000..40798e71 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForEditMemberProfileImage.java @@ -0,0 +1,9 @@ +package org.tuna.zoopzoop.backend.domain.member.dto.res; + +public record ResBodyForEditMemberProfileImage( + String profileUrl +) { + public ResBodyForEditMemberProfileImage(String profileUrl) { + this.profileUrl = profileUrl; + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfoV2.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfoV2.java index 9eeb0059..4b390675 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfoV2.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/dto/res/ResBodyForGetMemberInfoV2.java @@ -2,18 +2,22 @@ import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import java.time.LocalDateTime; + public record ResBodyForGetMemberInfoV2( Integer id, String name, String profileUrl, - String provider + String provider, + LocalDateTime createAt ) { public ResBodyForGetMemberInfoV2(Member member){ this( member.getId(), member.getName(), member.getProfileImageUrl(), - member.getProvider().name() + member.getProvider().name(), + member.getCreateDate() ); } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/entity/Member.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/entity/Member.java index a438e413..b5ad97b9 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/member/entity/Member.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/entity/Member.java @@ -54,9 +54,14 @@ public Member(String name, String providerKey, Provider provider, String profile //---------- 메소드 ----------// public boolean isActive() { return this.active; } - public void updateName(String name) { //사용자 이름 수정 + public String updateName(String name) { //사용자 이름 수정 this.name = name; + return this.name; } //사용자 이름 변경 public void deactivate() { this.active = false; } //soft-delete public void activate() { this.active = true; } //restore + public String updateProfileUrl(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + return this.profileImageUrl; + } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberService.java index 0b2ec51b..d6016230 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/member/service/MemberService.java @@ -5,10 +5,14 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; +import org.tuna.zoopzoop.backend.global.aws.S3Service; +import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -17,6 +21,7 @@ @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; + private final S3Service s3Service; //회원 조회 관련 public Member findById(Integer id) { @@ -84,8 +89,40 @@ public void updateMemberName(Member member, String newName){ throw new DataIntegrityViolationException("이미 사용중인 이름입니다."); } member.updateName(generateUniqueUserNameTag(newName)); + memberRepository.save(member); } + @Transactional + public void updateMemberProfileUrl(Member member, MultipartFile file){ + String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); + String fileName = "profile/" + member.getId() + "/profile." + extension; + try { + String newUrl = s3Service.upload(file, fileName); + member.updateProfileUrl(newUrl); + memberRepository.save(member); + } catch (IOException e) { + throw new IllegalArgumentException("잘못된 파일 입력입니다."); + } + } + + @Transactional + public void updateMemberProfile(Member member, String newName, MultipartFile file){ + if(memberRepository.findByName(newName).isPresent()) { + throw new DataIntegrityViolationException("이미 사용중인 이름입니다."); + } + member.updateName(generateUniqueUserNameTag(newName)); + String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); + String fileName = "profile/" + member.getId() + "/profile." + extension; + try { + String newUrl = s3Service.upload(file, fileName); + member.updateProfileUrl(newUrl); + memberRepository.save(member); + } catch (IOException e) { + throw new IllegalArgumentException("잘못된 파일 입력입니다."); + } + } + + //회원 삭제/복구 관련 public void softDeleteMember(Member member){ member.deactivate(); } public void hardDeleteMember(Member member){ memberRepository.delete(member); } diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberControllerTest.java index 06af9ce6..16fcde3a 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/member/controller/MemberControllerTest.java @@ -16,6 +16,7 @@ import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -158,13 +159,13 @@ void getMemberInfoByNameUnauthorized() throws Exception { @DisplayName("사용자 이름 수정 - 성공(200)") void editMemberNameSuccess() throws Exception { ReqBodyForEditMemberName reqBodyForEditMemberName = new ReqBodyForEditMemberName("test3"); - mockMvc.perform(put("/api/v1/member/edit") + mockMvc.perform(put("/api/v1/member/edit/name") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(reqBodyForEditMemberName))) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value(200)) .andExpect(jsonPath("$.msg").value("사용자의 닉네임을 변경했습니다.")) - .andExpect(jsonPath("$.data.name").value("test3")); + .andExpect(jsonPath("$.data.name").value(containsString("test"))); } @Test @@ -172,7 +173,7 @@ void editMemberNameSuccess() throws Exception { @DisplayName("사용자 이름 수정 - 실패(400, Bad_Request)") void editMemberNameFailedByBadRequest() throws Exception { ReqBodyForEditMemberName reqBodyForEditMemberName = new ReqBodyForEditMemberName(""); - mockMvc.perform(put("/api/v1/member/edit") + mockMvc.perform(put("/api/v1/member/edit/name") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(reqBodyForEditMemberName))) .andExpect(status().isBadRequest()) @@ -184,7 +185,7 @@ void editMemberNameFailedByBadRequest() throws Exception { @DisplayName("사용자 이름 수정 - 실패(401, Unauthorized)") void editMemberNameFailedByUnauthorized() throws Exception { ReqBodyForEditMemberName reqBodyForEditMemberName = new ReqBodyForEditMemberName("test3"); - mockMvc.perform(put("/api/v1/member/edit") + mockMvc.perform(put("/api/v1/member/edit/name") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(reqBodyForEditMemberName))) .andExpect(status().isUnauthorized())