diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/application/manage/member/ManageMemberApplication.java b/baebae-BE/src/main/java/com/web/baebaeBE/application/manage/member/ManageMemberApplication.java new file mode 100644 index 00000000..fceed0c2 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/application/manage/member/ManageMemberApplication.java @@ -0,0 +1,48 @@ +package com.web.baebaeBE.application.manage.member; + +import com.web.baebaeBE.domain.manage.member.service.ManageMemberService; +import com.web.baebaeBE.global.jwt.JwtTokenProvider; +import com.web.baebaeBE.infra.member.entity.Member; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberRequest; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Slf4j +@Transactional +@RequiredArgsConstructor +public class ManageMemberApplication { + + private final ManageMemberService manageMemberService; + private final JwtTokenProvider jwtTokenProvider; + + public ManageMemberResponse.MemberInformationResponse getMember(Long memberId) { + return manageMemberService.getMember(memberId); + } + + public ManageMemberResponse.ObjectUrlResponse updateProfileImage(Long memberId, MultipartFile image) { + String fileKey = manageMemberService.convertImageToObject(memberId, image); + manageMemberService.updateProfileImage(memberId, fileKey); + return ManageMemberResponse.ObjectUrlResponse.of(fileKey); + } + + public void updateFcmToken(Long memberId, ManageMemberRequest.UpdateFcmTokenDto updateFcmTokenDto) { + manageMemberService.updateFcmToken(memberId, updateFcmTokenDto.getFcmToken()); + } + + public void updateNickname(Long memberId, ManageMemberRequest.UpdateNicknameDto updateNicknameDto) { + manageMemberService.updateNickname(memberId, updateNicknameDto.getNickname()); + } + + public void deleteMember(Long memberId, HttpServletRequest httpServletRequest) { + String accessToken = jwtTokenProvider.getToken(httpServletRequest); + manageMemberService.verifyMemberWithToken(memberId, accessToken); + manageMemberService.deleteMember(memberId); + } + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/exception/ManageMemberError.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/exception/ManageMemberError.java new file mode 100644 index 00000000..6ecb448b --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/exception/ManageMemberError.java @@ -0,0 +1,20 @@ +package com.web.baebaeBE.domain.manage.member.exception; + +import com.web.baebaeBE.global.error.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + + +@Getter +@RequiredArgsConstructor +public enum ManageMemberError implements ErrorCode { + NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "MM-001", "존재하지 않는 회원입니다."), + NOT_VERIFY_MEMBET_WITH_TOKEN(HttpStatus.UNAUTHORIZED,"MM-002", "회원정보와 토큰정보가 일치하지 않습니다."); + + + private final HttpStatus httpStatus; + private final String errorCode; + private final String message; + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/service/ManageMemberService.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/service/ManageMemberService.java new file mode 100644 index 00000000..e7ff8c25 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/manage/member/service/ManageMemberService.java @@ -0,0 +1,76 @@ +package com.web.baebaeBE.domain.manage.member.service; + +import com.web.baebaeBE.domain.manage.member.exception.ManageMemberError; +import com.web.baebaeBE.domain.member.exception.MemberError; +import com.web.baebaeBE.global.error.exception.BusinessException; +import com.web.baebaeBE.global.jwt.JwtTokenProvider; +import com.web.baebaeBE.infra.member.entity.Member; +import com.web.baebaeBE.infra.member.repository.MemberRepository; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberResponse; +import com.web.baebaeBE.presentation.member.dto.MemberResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ManageMemberService { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + public ManageMemberResponse.MemberInformationResponse getMember(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberError.NOT_EXIST_MEMBER)); + + return ManageMemberResponse.MemberInformationResponse.of(member); + } + + public void updateProfileImage(Long memberId, String profileImage) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberError.NOT_EXIST_MEMBER)); + + member.updateProfileImage(profileImage); + memberRepository.save(member); + } + + // 이미지 파일을 Object Storage에 저장하고 키파일을 반환하는 메서드 + // 추후 수정할 예정 + public String convertImageToObject(Long memberId, MultipartFile image){ + return image.getName(); + } + + public void updateFcmToken(Long memberId, String fcmToken) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberError.NOT_EXIST_MEMBER)); + + member.updateFcmToken(fcmToken); + memberRepository.save(member); + } + + public void updateNickname(Long memberId, String nickname) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberError.NOT_EXIST_MEMBER)); + + member.update(nickname); + memberRepository.save(member); + } + + public void deleteMember(Long id) { + memberRepository.deleteById(id); + } + + public void verifyMemberWithToken(Long memberId,String accessToken){ + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(MemberError.NOT_EXIST_MEMBER)); + + String memberEmail = jwtTokenProvider.getUserEmail(accessToken); + + //회원 정보와 토큰안의 이메일 정보가 일치하지않으면 예외 발생 + if(!member.getEmail().equals(memberEmail)) + throw new BusinessException(ManageMemberError.NOT_VERIFY_MEMBET_WITH_TOKEN); + } +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/global/jwt/JwtTokenProvider.java b/baebae-BE/src/main/java/com/web/baebaeBE/global/jwt/JwtTokenProvider.java index eec03a56..294f09df 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/global/jwt/JwtTokenProvider.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/global/jwt/JwtTokenProvider.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.Set; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -89,6 +90,9 @@ public Authentication getAuthentication(String token) { (), "", authorities), token, authorities); } + public String getToken(HttpServletRequest httpServletRequest){ + return httpServletRequest.getHeader("Authorization").substring(7); + } //토큰에서 사용자 ID 가져오는 메서드 public String getUserEmail(String token) { diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/global/util/MultipartJackson2HttpMessageConverter.java b/baebae-BE/src/main/java/com/web/baebaeBE/global/util/MultipartJackson2HttpMessageConverter.java new file mode 100644 index 00000000..77a07172 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/global/util/MultipartJackson2HttpMessageConverter.java @@ -0,0 +1,31 @@ +package com.web.baebaeBE.global.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; + +@Component +public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType){ + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType){ + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType){ + return false; + } +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/infra/member/entity/Member.java b/baebae-BE/src/main/java/com/web/baebaeBE/infra/member/entity/Member.java index 2b9d1937..9a8811db 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/infra/member/entity/Member.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/infra/member/entity/Member.java @@ -39,6 +39,9 @@ public class Member implements UserDetails { private String nickname; + @Column(name="profile_image") + private String profileImage; + @Column(name = "member_type", nullable = false) @Enumerated(EnumType.STRING) private MemberType memberType; @@ -49,6 +52,9 @@ public class Member implements UserDetails { @Column(name = "token_expiration_time") private LocalDateTime tokenExpirationTime; + @Column(name = "fcm_token") + private String fcmToken; + public Member update(String nickname) { this.nickname = nickname; @@ -58,6 +64,13 @@ public Member update(String nickname) { public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + public void updateProfileImage(String profileImageKey){ + this.profileImage = profileImageKey; + } + public void updateFcmToken(String fcmToken){ + this.fcmToken = fcmToken; + } + public void updateTokenExpirationTime(LocalDateTime time) { this.tokenExpirationTime = time; diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/healthcheck/HealthCheckController.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/healthcheck/HealthCheckController.java index 5cff1783..03b0a0d3 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/healthcheck/HealthCheckController.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/healthcheck/HealthCheckController.java @@ -1,12 +1,20 @@ package com.web.baebaeBE.presentation.healthcheck; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@Tag(name = "Health Check", description = "헬스체크용 API") public class HealthCheckController { @GetMapping("/healthcheck") + @Operation(summary = "Health Check", description = "Load Balncer용 API입니다. (사용X)", + responses = { + @ApiResponse(responseCode = "200", description = "서버 상태 정상") + }) public String healthCheck() { return "OK"; // 서버가 정상적으로 작동 중임을 의미 } diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/ManageMemberController.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/ManageMemberController.java new file mode 100644 index 00000000..6f8b8f3b --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/ManageMemberController.java @@ -0,0 +1,72 @@ +package com.web.baebaeBE.presentation.manage.member; + + +import com.web.baebaeBE.application.manage.member.ManageMemberApplication; +import com.web.baebaeBE.presentation.manage.member.api.ManageMemberApi; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberRequest; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberResponse; +import jakarta.servlet.http.HttpServletRequest; +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; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/member") +public class ManageMemberController implements ManageMemberApi { + + + private final ManageMemberApplication manageMemberApplication; + + + @GetMapping("/{memberId}") + public ResponseEntity getMemberInformation( + @PathVariable Long memberId + ) { + ManageMemberResponse.MemberInformationResponse memberInformation + = manageMemberApplication.getMember(memberId); + + return ResponseEntity.ok(memberInformation); + } + + @PatchMapping(value = "/profile-image/{memberId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity updateProfileImage( + @PathVariable Long memberId, + @RequestPart(value = "image") MultipartFile image + ) { + ManageMemberResponse.ObjectUrlResponse objectUrlResponse + = manageMemberApplication.updateProfileImage(memberId, image); + return ResponseEntity.ok(objectUrlResponse); + } + + @PatchMapping("/fcm-token/{memberId}") + public ResponseEntity updateFcmToken( + @PathVariable Long memberId, + @RequestBody ManageMemberRequest.UpdateFcmTokenDto updateFcmTokenDto + ) { + manageMemberApplication.updateFcmToken(memberId, updateFcmTokenDto); + return ResponseEntity.ok().build(); + } + + @PatchMapping("/nickname/{memberId}") + public ResponseEntity updateNickname( + @PathVariable Long memberId, + @RequestBody ManageMemberRequest.UpdateNicknameDto updateNicknameDto) { + manageMemberApplication.updateNickname(memberId, updateNicknameDto); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{memberId}") + public ResponseEntity deleteMember( + @PathVariable Long memberId, + HttpServletRequest httpServletRequest + ) { + manageMemberApplication.deleteMember(memberId, httpServletRequest); + return ResponseEntity.ok().build(); + } +} + + diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/api/ManageMemberApi.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/api/ManageMemberApi.java new file mode 100644 index 00000000..ef06fcc3 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/api/ManageMemberApi.java @@ -0,0 +1,187 @@ +package com.web.baebaeBE.presentation.manage.member.api; + +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberRequest; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Member", description = "회원 관리 API") +public interface ManageMemberApi { + + @Operation( + summary = "회원 정보 조회", + description = "회원의 정보를 조회합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ManageMemberResponse.MemberInformationResponse.class))), + @ApiResponse(responseCode = "401", description = "토큰 인증 실패", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"T-002\",\n" + + " \"message\": \"해당 토큰은 유효한 토큰이 아닙니다.\"\n" + + "}")) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-002\",\n" + + " \"message\": \"존재하지 않는 회원입니다.\"\n" + + "}")) + ) + }) + @RequestMapping(method = RequestMethod.GET, value = "/{id}") + ResponseEntity getMemberInformation(@PathVariable Long id); + + + + @Operation( + summary = "프로필 사진 업데이트", + description = "회원의 프로필 사진을 업데이트합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "업데이트 성공"), + @ApiResponse(responseCode = "401", description = "토큰 인증 실패", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"T-002\",\n" + + " \"message\": \"해당 토큰은 유효한 토큰이 아닙니다.\"\n" + + "}")) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-002\",\n" + + " \"message\": \"존재하지 않는 회원입니다.\"\n" + + "}")) + ) + }) + @RequestMapping(method = RequestMethod.PATCH, value = "/profile-image/{id}") + ResponseEntity updateProfileImage(@PathVariable Long id, + @RequestPart("image") MultipartFile image); + + + + + @Operation( + summary = "FCM 토큰 업데이트", + description = "회원의 FCM 토큰을 업데이트합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "업데이트 성공"), + @ApiResponse(responseCode = "401", description = "토큰 인증 실패", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"T-002\",\n" + + " \"message\": \"해당 토큰은 유효한 토큰이 아닙니다.\"\n" + + "}")) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-002\",\n" + + " \"message\": \"존재하지 않는 회원입니다.\"\n" + + "}")) + ) + }) + @RequestMapping(method = RequestMethod.PATCH, value = "/fcm-token/{id}") + ResponseEntity updateFcmToken(@PathVariable Long id, + @RequestBody ManageMemberRequest.UpdateFcmTokenDto updateFcmTokenDto); + + + + + @Operation( + summary = "닉네임 수정", + description = "회원의 닉네임을 수정합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수정 성공"), + @ApiResponse(responseCode = "401", description = "토큰 인증 실패", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-003\",\n" + + " \"message\": \"해당 토큰은 유효한 토큰이 아닙니다.\"\n" + + "}")) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-002\",\n" + + " \"message\": \"존재하지 않는 회원입니다.\"\n" + + "}")) + ) + }) + @RequestMapping(method = RequestMethod.PATCH, value = "/nickname/{id}") + ResponseEntity updateNickname(@PathVariable Long id, + @RequestBody ManageMemberRequest.UpdateNicknameDto updateNicknameDto); + + @Operation( + summary = "회원탈퇴", + description = "해당 회원의 정보를 영구 삭제합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "탈퇴 성공"), + @ApiResponse(responseCode = "401", description = "토큰 인증 실패", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"MM-002\",\n" + + " \"message\": \"회원정보와 토큰정보가 일치하지 않습니다.\"\n" + + "}")) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"errorCode\": \"M-002\",\n" + + " \"message\": \"존재하지 않는 회원입니다.\"\n" + + "}")) + ) + }) + ResponseEntity deleteMember(@PathVariable Long memberId, + HttpServletRequest httpServletRequest); + +} \ No newline at end of file diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberRequest.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberRequest.java new file mode 100644 index 00000000..1a787b6c --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberRequest.java @@ -0,0 +1,31 @@ +package com.web.baebaeBE.presentation.manage.member.dto; + +import com.web.baebaeBE.infra.member.enums.MemberType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + + +public class ManageMemberRequest { + + + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class UpdateFcmTokenDto { + private String fcmToken; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class UpdateNicknameDto { + private String nickname; + } + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberResponse.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberResponse.java new file mode 100644 index 00000000..8bacfa06 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/manage/member/dto/ManageMemberResponse.java @@ -0,0 +1,54 @@ +package com.web.baebaeBE.presentation.manage.member.dto; + +import com.web.baebaeBE.infra.member.entity.Member; +import com.web.baebaeBE.infra.member.enums.MemberType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +/** + * 로그인 시 컨트롤러에서 사용할 response, request dto 작성 + */ +public class ManageMemberResponse { + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MemberInformationResponse { + private Long memberId; + private String email; + private String nickname; + private String profileImage; + private MemberType memberType; + + public static MemberInformationResponse of (Member member){ + return MemberInformationResponse.builder() + .memberId(member.getId()) + .email(member.getEmail()) + .nickname(member.getNickname()) + .profileImage(member.getProfileImage()) + .memberType(member.getMemberType()) + .build(); + } + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ObjectUrlResponse{ + private String imageUrl; + + public static ObjectUrlResponse of (String imageUrl){ + return ObjectUrlResponse.builder() + .imageUrl(imageUrl) + .build(); + } + } + + + + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/MemberController.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/MemberController.java index 67f0bc3a..3925b7c6 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/MemberController.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/MemberController.java @@ -8,12 +8,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - +import org.springframework.web.bind.annotation.*; @RestController @@ -73,7 +68,4 @@ public ResponseEntity logout(HttpServletRequest httpServletRequest) { return ResponseEntity.ok().build(); } - - - } diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/api/MemberApi.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/api/MemberApi.java index bc044e05..ee187df2 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/api/MemberApi.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/api/MemberApi.java @@ -17,11 +17,12 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.ResponseEntity; -@Tag(name = "Member", description = "유저 관리 API") +@Tag(name = "Login", description = "로그인 관련 API") public interface MemberApi { @Operation( summary = "로그인", + description = "기존 회원일 경우 로그인, 새로운 회원일 경우 회원가입을 진행합니다.", security = @SecurityRequirement(name = "bearerAuth") ) @Parameter( @@ -56,6 +57,7 @@ ResponseEntity< MemberResponse.SignUpResponse> oauthSignUp( @Operation( summary = "회원가입 유무 체크", + description = "카카오 토큰을 기반으로 회원가입 유무를 체크합니다.", security = @SecurityRequirement(name = "bearerAuth") ) @Parameter( @@ -83,7 +85,10 @@ public ResponseEntity isExistingUser( - @Operation(summary = "Access Token 재발급") + @Operation( + summary = "Access Token 재발급", + description = "Refresh Token을 기반으로, 새로운 Access Token을 발급합니다." + ) @Parameter( in = ParameterIn.HEADER, name = "Authorization", required = true, @@ -114,7 +119,10 @@ ResponseEntity refreshToken( - @Operation(summary = "로그아웃") + @Operation( + summary = "로그아웃", + description = "Refresh Token 만료시간을 현재시간으로 설정해 로그아웃 시킵니다." + ) @Parameter( in = ParameterIn.HEADER, name = "Authorization", required = true, diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/dto/MemberResponse.java b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/dto/MemberResponse.java index 7739fd93..8bec568b 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/dto/MemberResponse.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/presentation/member/dto/MemberResponse.java @@ -67,8 +67,6 @@ public static class AccessTokenResponse { private String accessToken; @Schema(example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJiZWJlLXNlcnZlciIsImlhdCI6MTcxMzQxNjgyNSwiZXhwIjoxNzE0NjI2NDI1LCJzdWIiOiJ1amozOTAwQG5hdmVyLmNvbSIsImp0aSI6IjIifQ.BYrRkhwK1SSAe3nanmRIT_oSZkWyZlNnl3wFLI_nIqY") private String refreshToken; - - public static AccessTokenResponse of(Member member, String accessToken) { return AccessTokenResponse.builder() .id(member.getId()) diff --git a/baebae-BE/src/test/java/com/web/baebaeBE/integration/manage/member/ManageMemberTest.java b/baebae-BE/src/test/java/com/web/baebaeBE/integration/manage/member/ManageMemberTest.java new file mode 100644 index 00000000..1990bef0 --- /dev/null +++ b/baebae-BE/src/test/java/com/web/baebaeBE/integration/manage/member/ManageMemberTest.java @@ -0,0 +1,175 @@ +package com.web.baebaeBE.integration.manage.member; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.web.baebaeBE.global.jwt.JwtTokenProvider; +import com.web.baebaeBE.infra.member.entity.Member; +import com.web.baebaeBE.infra.member.enums.MemberType; +import com.web.baebaeBE.infra.member.repository.MemberRepository; +import com.web.baebaeBE.presentation.manage.member.dto.ManageMemberRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Optional; + +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; + +@SpringBootTest() +@AutoConfigureMockMvc +@WithMockUser +public class ManageMemberTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private JwtTokenProvider tokenProvider; + @Autowired + private MemberRepository memberRepository; + private ObjectMapper objectMapper = new ObjectMapper(); + private String accessToken; + private String refreshToken; + private Member testMember; + + + @BeforeEach + void setup() { + testMember = memberRepository.save(Member.builder() + .email("test@gmail.com") + .nickname("김예찬") + .memberType(MemberType.KAKAO) + .refreshToken("null") + .build()); + + accessToken = tokenProvider.generateToken(testMember, Duration.ofDays(1)); // 임시 accessToken 생성 + refreshToken = tokenProvider.generateToken(testMember, Duration.ofDays(14)); // 임시 refreshToken 생성 + + testMember.updateRefreshToken(refreshToken); + memberRepository.save(testMember); + } + + //각 테스트 후마다 실행 + @AfterEach + void tearDown() { + Optional member = memberRepository.findByEmail("test@gmail.com"); + if(member.isPresent()) + memberRepository.delete(member.get()); + } + + + @Test + @DisplayName("회원정보 조회 테스트(): 해당 회원의 상세정보를 조회한다.") + public void getMemberInformation() throws Exception { + // given + + // when + mockMvc.perform(get("/api/member/{memberId}", testMember.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken)) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.memberId").exists()) + .andExpect(jsonPath("$.email").exists()) + .andExpect(jsonPath("$.nickname").exists()) + .andExpect(jsonPath("$.memberType").exists()); + } + + @Test + @DisplayName("프로필 사진 업데이트 테스트(): 회원의 프로필 사진을 업데이트한다.") + public void updateProfileTest() throws Exception { + // given + File tempFile = File.createTempFile("temp_image", ".jpg"); // 임시 파일 생성 + + try (OutputStream os = new FileOutputStream(tempFile)) { + BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + ImageIO.write(bufferedImage, "jpg", os); + } + byte[] imageBytes = Files.readAllBytes(tempFile.toPath()); // 사진 Byte 변환 + + MockMultipartFile multipartFile = new MockMultipartFile( + "image", // 파라미터 이름은 컨트롤러의 @RequestPart와 일치해야 함 + "image.jpg", // 파일 이름 + MediaType.IMAGE_JPEG_VALUE, // 컨텐트 타입 + imageBytes // 파일 바이트 + ); + + // when + mockMvc.perform(MockMvcRequestBuilders.multipart("/api/member/profile-image/{memberId}", testMember.getId()) + .file(multipartFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .header("Authorization", "Bearer " + accessToken) + .with(request -> { + request.setMethod("PATCH"); // PATCH 메소드로 설정 + return request; + })) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.imageUrl").exists()); + } + + @Test + @DisplayName("FCM 토큰 업데이트 테스트(): 해당 회원의 FCM 토큰을 업데이트한다.") + public void updateFcmTokenTest() throws Exception { + // given + ManageMemberRequest.UpdateFcmTokenDto updateFcmTokenDto + = new ManageMemberRequest.UpdateFcmTokenDto("fwef094938jweSIJDe8204gaskd390GK32G9HADF0809d8708U908ud9UHD9FH4e32982hF0ODH22E"); + + // when + mockMvc.perform(patch("/api/member/fcm-token/{memberId}", testMember.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateFcmTokenDto)) + .header("Authorization", "Bearer " + accessToken)) + // then + .andExpect(status().isOk()); + } + + + @Test + @DisplayName("닉네임 업데이트 테스트(): 해당 회원의 닉네임을 업데이트한다.") + public void updateNicknameTest() throws Exception { + // given + ManageMemberRequest.UpdateNicknameDto updateNicknameDto + = new ManageMemberRequest.UpdateNicknameDto("새로운 닉네임"); + + // when + mockMvc.perform(patch("/api/member/nickname/{memberId}", testMember.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateNicknameDto)) + .header("Authorization", "Bearer " + accessToken)) + // then + .andExpect(status().isOk()); + } + + @Test + @DisplayName("회원탈퇴 테스트(): 해당 회원의 정보를 영구적으로 삭제한다.") + public void deleteMember() throws Exception { + // given + + // when + mockMvc.perform(delete("/api/member/{memberId}", testMember.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken)) + // then + .andExpect(status().isOk()); + } + + +}