diff --git a/build.gradle b/build.gradle index ca530af6..7b187a74 100644 --- a/build.gradle +++ b/build.gradle @@ -44,9 +44,6 @@ dependencies { // swagger setting implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - // webClient setting - implementation 'org.springframework.boot:spring-boot-starter-webflux' - // s3 setting implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' @@ -58,6 +55,9 @@ dependencies { // 스프링 시큐리티를 테스트하기 위한 의존성 추가 testImplementation 'org.springframework.security:spring-security-test' + + // 다른 서버와 통신을 위한 의존성 추가 + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/com/umc/networkingService/config/SwaggerConfig.java b/src/main/java/com/umc/networkingService/config/SwaggerConfig.java index de2489d3..3cf8c2ae 100644 --- a/src/main/java/com/umc/networkingService/config/SwaggerConfig.java +++ b/src/main/java/com/umc/networkingService/config/SwaggerConfig.java @@ -1,10 +1,10 @@ package com.umc.networkingService.config; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/umc/networkingService/config/security/auth/PrincipalDetailsService.java b/src/main/java/com/umc/networkingService/config/security/auth/PrincipalDetailsService.java index 0df102c3..42449cd5 100644 --- a/src/main/java/com/umc/networkingService/config/security/auth/PrincipalDetailsService.java +++ b/src/main/java/com/umc/networkingService/config/security/auth/PrincipalDetailsService.java @@ -4,12 +4,13 @@ import com.umc.networkingService.domain.member.repository.MemberRepository; import com.umc.networkingService.global.common.exception.ErrorCode; import com.umc.networkingService.global.common.exception.RestApiException; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import java.util.UUID; + @Service @RequiredArgsConstructor public class PrincipalDetailsService implements UserDetailsService { diff --git a/src/main/java/com/umc/networkingService/config/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/umc/networkingService/config/security/jwt/JwtAuthenticationFilter.java index 1374f579..9d755123 100644 --- a/src/main/java/com/umc/networkingService/config/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/umc/networkingService/config/security/jwt/JwtAuthenticationFilter.java @@ -21,15 +21,26 @@ public class JwtAuthenticationFilter extends GenericFilterBean { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // refresh Token이 있다면 refresh Token 사용 - String refreshToken = jwtTokenProvider.resolveRefreshToken((HttpServletRequest) request); - String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); - if (refreshToken != null && ((HttpServletRequest) request).getRequestURI() - .equals("/members/refresh") && jwtTokenProvider.validateRefreshToken(refreshToken)) { - Authentication authentication = jwtTokenProvider.getRefreshAuthentication(refreshToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - } else if (token != null && jwtTokenProvider.validateToken(token)) { - Authentication authentication = jwtTokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(authentication); + String token = jwtTokenProvider.resolveRefreshToken((HttpServletRequest) request); + + if(token != null) { + if (token.startsWith("Bearer ")) token = token.substring(7); + + if (((HttpServletRequest) request).getRequestURI() + .equals("/members/token/refresh") && jwtTokenProvider.validateRefreshToken(token)) { + Authentication authentication = jwtTokenProvider.getRefreshAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } else { + token = jwtTokenProvider.resolveToken((HttpServletRequest) request); + if (token != null) { + if (token.startsWith("Bearer ")) token = token.substring(7); + + if (jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } } chain.doFilter(request, response); } diff --git a/src/main/java/com/umc/networkingService/config/security/jwt/JwtTokenProvider.java b/src/main/java/com/umc/networkingService/config/security/jwt/JwtTokenProvider.java index ccb24f89..99746edf 100644 --- a/src/main/java/com/umc/networkingService/config/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/umc/networkingService/config/security/jwt/JwtTokenProvider.java @@ -4,16 +4,9 @@ import com.umc.networkingService.config.security.auth.PrincipalDetailsService; import com.umc.networkingService.global.common.exception.ErrorCode; import com.umc.networkingService.global.common.exception.RestApiException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -23,6 +16,7 @@ import java.util.Base64; import java.util.Date; +import java.util.UUID; @Component @RequiredArgsConstructor @@ -47,10 +41,13 @@ protected void init() { refreshSecretKey = Base64.getEncoder().encodeToString(refreshSecretKey.getBytes()); } - public String generateAccessToken(Claims claims) { + public String generateAccessToken(UUID memberId) { Date now = new Date(); Date accessTokenExpirationTime = new Date(now.getTime() + TOKEN_VALID_TIME); + Claims claims = Jwts.claims(); + claims.put("memberId", memberId); + return Jwts.builder() .setClaims(claims) .setIssuedAt(now) // 토큰 발행 시간 정보 @@ -59,10 +56,13 @@ public String generateAccessToken(Claims claims) { .compact(); } - public String generateRefreshToken(Claims claims) { + public String generateRefreshToken(UUID memberId) { Date now = new Date(); Date refreshTokenExpirationTime = new Date(now.getTime() + REF_TOKEN_VALID_TIME); + Claims claims = Jwts.claims(); + claims.put("memberId", memberId); + return Jwts.builder() .setClaims(claims) .setIssuedAt(now) // 토큰 발행 시간 정보 @@ -73,11 +73,8 @@ public String generateRefreshToken(Claims claims) { public TokenInfo generateToken(UUID memberId) { - Claims claims = Jwts.claims(); - claims.put("memberId", memberId); - - String accessToken = generateAccessToken(claims); - String refreshToken = generateRefreshToken(claims); + String accessToken = generateAccessToken(memberId); + String refreshToken = generateRefreshToken(memberId); return new TokenInfo(accessToken, refreshToken); } @@ -125,7 +122,7 @@ public boolean validateToken(String token) { Jwts.parser().setSigningKey(jwtSecretKey).parseClaimsJws(token); return true; } catch (SecurityException | MalformedJwtException e) { - throw new RestApiException(ErrorCode.INVALID_JWT); + throw new RestApiException(ErrorCode.INVALID_ACCESS_TOKEN); } catch (ExpiredJwtException e) { throw new RestApiException(ErrorCode.EXPIRED_MEMBER_JWT); } catch (UnsupportedJwtException | SignatureException e) { @@ -139,7 +136,7 @@ public boolean validateRefreshToken(String token) { Jwts.parser().setSigningKey(refreshSecretKey).parseClaimsJws(token); return true; } catch (SecurityException | MalformedJwtException e) { - throw new RestApiException(ErrorCode.INVALID_JWT); + throw new RestApiException(ErrorCode.INVALID_REFRESH_TOKEN); } catch (ExpiredJwtException e) { throw new RestApiException(ErrorCode.EXPIRED_MEMBER_JWT); } catch (UnsupportedJwtException | SignatureException e) { diff --git a/src/main/java/com/umc/networkingService/domain/friend/entity/Friend.java b/src/main/java/com/umc/networkingService/domain/friend/entity/Friend.java index c10078f9..35b44e22 100644 --- a/src/main/java/com/umc/networkingService/domain/friend/entity/Friend.java +++ b/src/main/java/com/umc/networkingService/domain/friend/entity/Friend.java @@ -1,12 +1,9 @@ package com.umc.networkingService.domain.friend.entity; import com.umc.networkingService.domain.member.entity.Member; -import jakarta.persistence.*; import com.umc.networkingService.global.common.base.BaseEntity; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.UuidGenerator; @@ -14,6 +11,8 @@ @Getter @Entity +@Builder +@AllArgsConstructor @NoArgsConstructor(access= AccessLevel.PROTECTED) @SQLRestriction("deleted_at is null") public class Friend extends BaseEntity { diff --git a/src/main/java/com/umc/networkingService/domain/friend/repository/FriendRepository.java b/src/main/java/com/umc/networkingService/domain/friend/repository/FriendRepository.java new file mode 100644 index 00000000..36366064 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/friend/repository/FriendRepository.java @@ -0,0 +1,11 @@ +package com.umc.networkingService.domain.friend.repository; + +import com.umc.networkingService.domain.friend.entity.Friend; +import com.umc.networkingService.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface FriendRepository extends JpaRepository { + boolean existsBySenderAndReceiver(Member sender, Member receiver); +} diff --git a/src/main/java/com/umc/networkingService/domain/friend/service/FriendService.java b/src/main/java/com/umc/networkingService/domain/friend/service/FriendService.java new file mode 100644 index 00000000..37634c6e --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/friend/service/FriendService.java @@ -0,0 +1,7 @@ +package com.umc.networkingService.domain.friend.service; + +import com.umc.networkingService.domain.member.entity.Member; + +public interface FriendService { + boolean checkFriend(Member sender, Member receiver); +} diff --git a/src/main/java/com/umc/networkingService/domain/friend/service/FriendServiceImpl.java b/src/main/java/com/umc/networkingService/domain/friend/service/FriendServiceImpl.java new file mode 100644 index 00000000..a880bb87 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/friend/service/FriendServiceImpl.java @@ -0,0 +1,18 @@ +package com.umc.networkingService.domain.friend.service; + +import com.umc.networkingService.domain.friend.repository.FriendRepository; +import com.umc.networkingService.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FriendServiceImpl implements FriendService { + + private final FriendRepository friendRepository; + + @Override + public boolean checkFriend(Member sender, Member receiver) { + return friendRepository.existsBySenderAndReceiver(sender, receiver); + } +} diff --git a/src/main/java/com/umc/networkingService/domain/member/client/GithubMemberClient.java b/src/main/java/com/umc/networkingService/domain/member/client/GithubMemberClient.java new file mode 100644 index 00000000..0b1beff0 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/client/GithubMemberClient.java @@ -0,0 +1,82 @@ +package com.umc.networkingService.domain.member.client; + +import com.umc.networkingService.domain.member.dto.client.github.GithubAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.client.github.GithubInfoResponse; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +public class GithubMemberClient { + + @Value("${github.client-id}") + private String clientId; + + @Value("${github.client-secret}") + private String clientSecret; + + @Value("${github.redirect-uri}") + private String redirectUri; + + private final WebClient webClient; + + public GithubMemberClient() { + this.webClient = generateWebClient(); + } + + private WebClient generateWebClient() { + return WebClient.builder() + .baseUrl("https://github.com/login/oauth/access_token") + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + } + + public String getAccessToken(final String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", clientId); + formData.add("client_secret", clientSecret); + formData.add("code", code); + formData.add("redirect_uri", redirectUri); + + GithubAccessTokenResponse response = webClient + .post() + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .bodyToMono(GithubAccessTokenResponse.class) + .blockOptional() + .orElseThrow(() -> new RestApiException(ErrorCode.FAILED_GITHUB_AUTHENTICATION)); + + if (response == null) + throw new RestApiException(ErrorCode.FAILED_GITHUB_AUTHENTICATION); + + return response.getAccessToken(); + } + + public String getGithubNickname(final String code) { + String accessToken = getAccessToken(code); + + WebClient userWebClient = WebClient.builder() + .baseUrl("https://api.github.com/user") + .build(); + + GithubInfoResponse response = userWebClient + .get() + .headers(headers -> headers.setBearerAuth(accessToken)) + .retrieve() + .bodyToMono(GithubInfoResponse.class) + .block(); + + if (response == null) + throw new RestApiException(ErrorCode.FAILED_SOCIAL_LOGIN); + + return response.getLogin(); + } +} \ No newline at end of file diff --git a/src/main/java/com/umc/networkingService/domain/member/client/GoogleMemberClient.java b/src/main/java/com/umc/networkingService/domain/member/client/GoogleMemberClient.java index b200277d..ba897056 100644 --- a/src/main/java/com/umc/networkingService/domain/member/client/GoogleMemberClient.java +++ b/src/main/java/com/umc/networkingService/domain/member/client/GoogleMemberClient.java @@ -24,7 +24,7 @@ public String getgoogleClientID(final String accessToken) { .block(); if(response == null) - throw new RestApiException(ErrorCode._INTERNAL_SERVER_ERROR); + throw new RestApiException(ErrorCode.FAILED_SOCIAL_LOGIN); return response.getSub(); } diff --git a/src/main/java/com/umc/networkingService/domain/member/client/KakaoMemberClient.java b/src/main/java/com/umc/networkingService/domain/member/client/KakaoMemberClient.java index 2ab8ec26..33b8b099 100644 --- a/src/main/java/com/umc/networkingService/domain/member/client/KakaoMemberClient.java +++ b/src/main/java/com/umc/networkingService/domain/member/client/KakaoMemberClient.java @@ -26,7 +26,7 @@ public String getkakaoClientID(final String accessToken) { .block(); if(response == null) - throw new RestApiException(ErrorCode._INTERNAL_SERVER_ERROR); + throw new RestApiException(ErrorCode.FAILED_SOCIAL_LOGIN); return response.getId(); } diff --git a/src/main/java/com/umc/networkingService/domain/member/client/NaverMemberClient.java b/src/main/java/com/umc/networkingService/domain/member/client/NaverMemberClient.java index 7af2759b..8cb3bdaf 100644 --- a/src/main/java/com/umc/networkingService/domain/member/client/NaverMemberClient.java +++ b/src/main/java/com/umc/networkingService/domain/member/client/NaverMemberClient.java @@ -25,7 +25,7 @@ public String getnaverClientID(final String accessToken){ .block(); if(response == null) - throw new RestApiException(ErrorCode._INTERNAL_SERVER_ERROR); + throw new RestApiException(ErrorCode.FAILED_SOCIAL_LOGIN); return response.getResponse().getId(); } diff --git a/src/main/java/com/umc/networkingService/domain/member/controller/AuthController.java b/src/main/java/com/umc/networkingService/domain/member/controller/AuthController.java new file mode 100644 index 00000000..a8f2b10e --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/controller/AuthController.java @@ -0,0 +1,82 @@ +package com.umc.networkingService.domain.member.controller; + +import com.umc.networkingService.config.security.auth.CurrentMember; +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.response.MemberGenerateNewAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.response.MemberIdResponse; +import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.service.AuthService; +import com.umc.networkingService.global.common.base.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "인증 API", description = "멤버 인증 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class AuthController { + private final AuthService authService; + + @Operation(summary = "소셜 로그인", description = "네이버, 카카오, 구글, 애플 로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공"), + @ApiResponse(responseCode = "AUTH007", description = "외부 소셜 서버와의 통신 에러" , content = + @Content(schema = @Schema(implementation = BaseResponse.class))) + }) + @PostMapping("/login") + public BaseResponse socialLogin(@RequestParam(value = "accessToken") String accessToken, + @RequestParam(value = "socialType") SocialType socialType) { + return BaseResponse.onSuccess(authService.socialLogin(accessToken, socialType)); + + } + + @Operation(summary = "회원가입 API", description = "최초 멤버 정보를 등록하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "UNIVERSITY001", description = "대학교명을 잘못 입력하였을 경우 발생"), + @ApiResponse(responseCode = "BRANCH001", description = "대학교가 지부랑 연결되어 있지 않을 경우 발생") + }) + @PostMapping + public BaseResponse signUp(@CurrentMember Member member, + @Valid @RequestBody MemberSignUpRequest request) { + return BaseResponse.onSuccess(authService.signUp(member, request)); + } + + @Operation(summary = "accessToken 재발급 API", description = "refreshToken가 유효하다면 새로운 accessToken을 발급하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "AUTH006", description = "유효하지 않는 RefreshToken일 경우 발생") + }) + @GetMapping("/token/refresh") + public BaseResponse regenerateToken(@CurrentMember Member member, + @RequestHeader(value = "refreshToken") String refreshToken) { + return BaseResponse.onSuccess(authService.generateNewAccessToken(refreshToken, member)); + } + + @Operation(summary = "로그아웃 API", description = "해당 유저의 refreshToken을 삭제하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공") + }) + @DeleteMapping("/logout") + public BaseResponse logout(@CurrentMember Member member) { + return BaseResponse.onSuccess(authService.logout(member)); + } + + @Operation(summary = "회원 탈퇴 API", description = "해당 유저 정보를 삭제하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공") + }) + @DeleteMapping + public BaseResponse withdrawal(@CurrentMember Member member) { + return BaseResponse.onSuccess(authService.withdrawal(member)); + } +} diff --git a/src/main/java/com/umc/networkingService/domain/member/controller/MemberController.java b/src/main/java/com/umc/networkingService/domain/member/controller/MemberController.java index 5e691ec1..b27e0275 100644 --- a/src/main/java/com/umc/networkingService/domain/member/controller/MemberController.java +++ b/src/main/java/com/umc/networkingService/domain/member/controller/MemberController.java @@ -2,22 +2,22 @@ import com.umc.networkingService.config.security.auth.CurrentMember; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; -import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.domain.member.dto.response.MemberSignUpResponse; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; +import com.umc.networkingService.domain.member.dto.response.*; import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.SocialType; import com.umc.networkingService.domain.member.service.MemberService; import com.umc.networkingService.global.common.base.BaseResponse; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; @Tag(name = "멤버 API", description = "멤버 관련 API") @RestController @@ -27,28 +27,68 @@ public class MemberController { private final MemberService memberService; - @Operation(summary = "소셜 로그인", description = "네이버, 카카오, 구글, 애플 로그인") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "로그인 성공"), - @ApiResponse(responseCode = "COMMON500", description = "소셜 서버와의 통신 에러" , content = - @Content(schema = @Schema(implementation = BaseResponse.class))) + @Operation(summary = "나의 프로필 수정 API", description = "본인 프로필 사진, 닉네임, 이름, 상태 메시지를 수정하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "IMAGE001", description = "이미지 S3 업로드 실패할 경우 발생") + }) + @PostMapping("/update") + public BaseResponse updateMyProfile(@CurrentMember Member member, + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, + @RequestPart MemberUpdateMyProfileRequest request) { + return BaseResponse.onSuccess(memberService.updateMyProfile(member, profileImage, request)); + } + + @Operation(summary = "유저 프로필 조회 API", description = "본인 또는 타인 프로필 조회하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "MEMBER004", description = "조회 대상이 소속 대학교가 없는 경우 발생") + }) + @Parameter(name = "memberId", in = ParameterIn.PATH, description = "조회하고자 하는 멤버의 UUID. 이 파라미터가 없으면 본인의 프로필을 조회하고, 있으면 해당 멤버의 프로필을 조회합니다.") + @GetMapping(value = {"", "/{memberId}"}) + public BaseResponse inquiryProfile(@CurrentMember Member member, + @PathVariable(required = false) UUID memberId) { + return BaseResponse.onSuccess(memberService.inquiryProfile(member, memberId)); + } + + @Operation(summary = "포인트 관련 유저 정보 조회 API", description = "닉네임, 프로필 사진, 기여도, 랭킹을 조회하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "MEMBER004", description = "조회 대상이 소속 대학교가 없는 경우 발생") }) - @PostMapping("/login") - public BaseResponse socialLogin(@RequestParam(value = "accessToken") String accessToken, - @RequestParam(value = "socialType") SocialType socialType) { - return BaseResponse.onSuccess(memberService.socialLogin(accessToken, socialType)); + @GetMapping("/rank") + public BaseResponse inquiryHomeInfo(@CurrentMember Member member) { + return BaseResponse.onSuccess(memberService.inquiryInfoWithPoint(member)); + } + @Operation(summary = "깃허브 연동 API", description = "깃허브 로그인을 통해서 발급받은 accessToken으로 깃허브 닉네임을 저장하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + @ApiResponse(responseCode = "AUTH008", description = "깃허브 서버와 통신 실패할 경우 발생") + }) + @PostMapping("/github") + public BaseResponse authenticationGithub(@CurrentMember Member member, + @RequestParam String code) { + return BaseResponse.onSuccess(memberService.authenticationGithub(member, code)); } - @Operation(summary = "회원가입 API", description = "최초 멤버 정보를 등록하는 API입니다.") + @Operation(summary = "깃허브 데이터 조회 API", description = "깃허브 잔디 이미지를 조회하는 API입니다.") @ApiResponses( value = { @ApiResponse(responseCode = "COMMON200", description = "성공"), - @ApiResponse(responseCode = "UNIVERSITY001", description = "대학교명을 잘못 입력하였을 경우 발생"), - @ApiResponse(responseCode = "BRANCH001", description = "대학교가 지부랑 연결되어 있지 않을 경우 발생") + @ApiResponse(responseCode = "AUTH008", description = "깃허브 인증이 안 된 사용자일 경우 발생") }) - @PostMapping - public BaseResponse signUp(@CurrentMember Member member, - @Valid @RequestBody MemberSignUpRequest request) { - return BaseResponse.onSuccess(memberService.signUp(member, request)); + @GetMapping("/github") + public BaseResponse inquiryGithubImage(@CurrentMember Member member) { + return BaseResponse.onSuccess(memberService.inquiryGithubImage(member)); + } + + @Operation(summary = "남은 포인트 및 사용 내역 조회 API", description = "본인의 남은 포인트 및 최근 2개의 사용 내역를 조회하는 API입니다.") + @ApiResponses( value = { + @ApiResponse(responseCode = "COMMON200", description = "성공") + }) + @GetMapping("/points") + public BaseResponse inquiryMemberPoints(@CurrentMember Member member) { + return BaseResponse.onSuccess(memberService.inquiryMemberPoints(member)); + } } diff --git a/src/main/java/com/umc/networkingService/domain/member/controller/StaffMemberController.java b/src/main/java/com/umc/networkingService/domain/member/controller/StaffMemberController.java new file mode 100644 index 00000000..8f526a4e --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/controller/StaffMemberController.java @@ -0,0 +1,22 @@ +package com.umc.networkingService.domain.member.controller; + +// TODO: security 문제 해결 후 개발 +//@Tag(name = "멤버 API", description = "운영진용 멤버 관련 API") +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/staff/members") +//public class StaffMemberController { +// private final MemberService memberService; +// @Operation(summary = "유저 정보 수정 API", description = "운영진이 특정 유저의 직책, 기수, 파트를 수정하는 API입니다.") +// @ApiResponses( value = { +// @ApiResponse(responseCode = "COMMON200", description = "성공"), +// @ApiResponse(responseCode = "MEMBER002", description = "상위 운영진의 직책을 수정할 경우 발생"), +// @ApiResponse(responseCode = "MEMBER003", description = "지부, 학교 운영진이 중앙 직책을 수정할 경우 발생") +// }) +// @PostMapping("/{memberId}/update") +// public BaseResponse updateProfile(@CurrentMember Member member, +// @PathVariable UUID memberId, +// @RequestBody MemberUpdateProfileRequest request) { +// return BaseResponse.onSuccess(memberService.updateProfile(member, memberId, request)); +// } +//} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubAccessTokenResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubAccessTokenResponse.java new file mode 100644 index 00000000..6598c35e --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubAccessTokenResponse.java @@ -0,0 +1,10 @@ +package com.umc.networkingService.domain.member.dto.client.github; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GithubAccessTokenResponse { + private String accessToken; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubInfoResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubInfoResponse.java new file mode 100644 index 00000000..2ec50d66 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/client/github/GithubInfoResponse.java @@ -0,0 +1,10 @@ +package com.umc.networkingService.domain.member.dto.client.github; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GithubInfoResponse { + private String login; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberSignUpRequest.java b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberSignUpRequest.java index 3061fbdd..52f8b115 100644 --- a/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberSignUpRequest.java +++ b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberSignUpRequest.java @@ -1,7 +1,5 @@ package com.umc.networkingService.domain.member.dto.request; -import com.umc.networkingService.global.common.enums.Part; -import com.umc.networkingService.global.common.enums.Semester; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -20,10 +18,8 @@ public class MemberSignUpRequest { private String name; @NotBlank(message = "닉네임은 필수 입력값입니다.") private String nickname; - @Size(min = 1, message = "최소 1개의 파트를 선택해야 합니다.") - private List parts; - @Size(min = 1, message = "최소 1개의 기수를 선택해야 합니다.") - private List semesters; + @Size(min = 1, message = "최소 하나의 기수별 파트가 필요합니다.") + private List semesterParts; private String universityName; private List campusPositions; private List centerPositions; diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateMyProfileRequest.java b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateMyProfileRequest.java new file mode 100644 index 00000000..ab0e0662 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateMyProfileRequest.java @@ -0,0 +1,15 @@ +package com.umc.networkingService.domain.member.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MemberUpdateMyProfileRequest { + @NotBlank(message = "이름은 필수 입력값입니다.") + private String name; + @NotBlank(message = "닉네임은 필수 입력값입니다.") + private String nickname; + private String statusMessage; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateProfileRequest.java b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateProfileRequest.java new file mode 100644 index 00000000..1d78e6b7 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/request/MemberUpdateProfileRequest.java @@ -0,0 +1,14 @@ +package com.umc.networkingService.domain.member.dto.request; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class MemberUpdateProfileRequest { + private List campusPositions; + private List centerPositions; + private List semesterParts; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/request/SemesterPartInfo.java b/src/main/java/com/umc/networkingService/domain/member/dto/request/SemesterPartInfo.java new file mode 100644 index 00000000..778bc366 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/request/SemesterPartInfo.java @@ -0,0 +1,13 @@ +package com.umc.networkingService.domain.member.dto.request; + +import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.global.common.enums.Semester; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SemesterPartInfo { + private Part part; + private Semester semester; +} \ No newline at end of file diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberAuthenticationGithubResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberAuthenticationGithubResponse.java new file mode 100644 index 00000000..087684ab --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberAuthenticationGithubResponse.java @@ -0,0 +1,10 @@ +package com.umc.networkingService.domain.member.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MemberAuthenticationGithubResponse { + private String githubNickname; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberGenerateNewAccessTokenResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberGenerateNewAccessTokenResponse.java new file mode 100644 index 00000000..105d5d37 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberGenerateNewAccessTokenResponse.java @@ -0,0 +1,10 @@ +package com.umc.networkingService.domain.member.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MemberGenerateNewAccessTokenResponse { + private String accessToken; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberSignUpResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberIdResponse.java similarity index 84% rename from src/main/java/com/umc/networkingService/domain/member/dto/response/MemberSignUpResponse.java rename to src/main/java/com/umc/networkingService/domain/member/dto/response/MemberIdResponse.java index 95146b5b..43d19ada 100644 --- a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberSignUpResponse.java +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberIdResponse.java @@ -7,6 +7,6 @@ @Getter @AllArgsConstructor -public class MemberSignUpResponse { +public class MemberIdResponse { private UUID memberId; } diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryGithubResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryGithubResponse.java new file mode 100644 index 00000000..65462c7a --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryGithubResponse.java @@ -0,0 +1,10 @@ +package com.umc.networkingService.domain.member.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MemberInquiryGithubResponse { + private String githubImage; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryInfoWithPointResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryInfoWithPointResponse.java new file mode 100644 index 00000000..2c1fc0d2 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryInfoWithPointResponse.java @@ -0,0 +1,13 @@ +package com.umc.networkingService.domain.member.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MemberInquiryInfoWithPointResponse { + private String profileImage; + private String nickname; + private Long contributionPoint; + private int contributionRank; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryPointsResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryPointsResponse.java new file mode 100644 index 00000000..7f0dabb5 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryPointsResponse.java @@ -0,0 +1,21 @@ +package com.umc.networkingService.domain.member.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class MemberInquiryPointsResponse { + private Long remainPoint; + private List usedHistories; + + @Getter + @Builder + public static class UsedHistory { + private String pointImage; + private Long point; + private String description; + } +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryProfileResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryProfileResponse.java new file mode 100644 index 00000000..cfd3092d --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberInquiryProfileResponse.java @@ -0,0 +1,22 @@ +package com.umc.networkingService.domain.member.dto.response; + +import com.umc.networkingService.domain.member.dto.request.SemesterPartInfo; +import com.umc.networkingService.domain.member.entity.MemberRelation; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +@Getter +@Builder +public class MemberInquiryProfileResponse { + private UUID memberId; + private String profileImage; + private String universityName; + private String name; + private String nickname; + private List semesterParts; + private String statusMessage; + private MemberRelation owner; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberLoginResponse.java b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberLoginResponse.java index 2ec72b38..42735ec4 100644 --- a/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberLoginResponse.java +++ b/src/main/java/com/umc/networkingService/domain/member/dto/response/MemberLoginResponse.java @@ -1,6 +1,5 @@ package com.umc.networkingService.domain.member.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,4 +11,5 @@ public class MemberLoginResponse { private UUID memberId; private String accessToken; private String refreshToken; + private boolean isServiceMember; } diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/Member.java b/src/main/java/com/umc/networkingService/domain/member/entity/Member.java index fbbf3e47..8aa1faf4 100644 --- a/src/main/java/com/umc/networkingService/domain/member/entity/Member.java +++ b/src/main/java/com/umc/networkingService/domain/member/entity/Member.java @@ -1,12 +1,10 @@ package com.umc.networkingService.domain.member.entity; import com.umc.networkingService.domain.branch.entity.Branch; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; import com.umc.networkingService.domain.university.entity.University; import com.umc.networkingService.global.common.base.BaseEntity; -import com.umc.networkingService.global.common.enums.Part; import com.umc.networkingService.global.common.enums.Role; -import com.umc.networkingService.global.common.enums.Semester; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.ColumnDefault; @@ -47,6 +45,9 @@ public class Member extends BaseEntity { @ColumnDefault("0") private Long remainPoint; + @ColumnDefault("0") + private Long contributionPoint; + private String nickname; private String name; @@ -57,17 +58,14 @@ public class Member extends BaseEntity { @Column(nullable = false) private SocialType socialType; - @Enumerated(EnumType.STRING) + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default - @CollectionTable(name = "member_part", joinColumns = @JoinColumn(name = "member_id")) - @ElementCollection(fetch = FetchType.LAZY) - private List part = new ArrayList<>(); + private List semesterParts = new ArrayList<>(); - @Enumerated(EnumType.STRING) + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default - @CollectionTable(name = "member_semester", joinColumns = @JoinColumn(name = "member_id")) - @ElementCollection(fetch = FetchType.LAZY) - private List semester = new ArrayList<>(); + private List positions = new ArrayList<>(); + @Enumerated(EnumType.STRING) private Role role; @@ -76,13 +74,39 @@ public class Member extends BaseEntity { private String notionLink; - public void setMemberInfo(MemberSignUpRequest request, Role role, University university, Branch branch) { + public void setMemberInfo(String name, String nickname, University university, Branch branch) { + this.name = name; + this.nickname = nickname; + this.university = university; + this.branch = branch; + } + + public void updateMemberInfo(MemberUpdateMyProfileRequest request, String profileImage) { this.name = request.getName(); this.nickname = request.getNickname(); + this.statusMessage = request.getStatusMessage(); + this.profileImage = profileImage; + } + + public void updatePositions(List memberPositions) { + this.positions = memberPositions; + } + + public void updateSemesterParts(List semesterParts) { + this.semesterParts = semesterParts; + } + + public void authenticationGithub(String gitNickname) { + this.gitNickname = gitNickname; + } + + public void updateContributionPoint(Long usedPoint) { + if (this.contributionPoint == null) this.contributionPoint = usedPoint; + else this.contributionPoint += usedPoint; + } + + // 테스트 코드용 + public void updateRole(Role role) { this.role = role; - this.university = university; - this.branch = branch; - this.part.addAll(request.getParts()); - this.semester.addAll(request.getSemesters()); } } diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/MemberPoint.java b/src/main/java/com/umc/networkingService/domain/member/entity/MemberPoint.java index 1b3e3f75..d88ffa2c 100644 --- a/src/main/java/com/umc/networkingService/domain/member/entity/MemberPoint.java +++ b/src/main/java/com/umc/networkingService/domain/member/entity/MemberPoint.java @@ -3,9 +3,7 @@ import com.umc.networkingService.domain.university.entity.University; import com.umc.networkingService.global.common.base.BaseEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.UuidGenerator; @@ -13,6 +11,8 @@ @Getter @Entity +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @SQLRestriction("deleted_at is null") public class MemberPoint extends BaseEntity { diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/MemberRelation.java b/src/main/java/com/umc/networkingService/domain/member/entity/MemberRelation.java new file mode 100644 index 00000000..4d1554a2 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/entity/MemberRelation.java @@ -0,0 +1,14 @@ +package com.umc.networkingService.domain.member.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberRelation { + MINE("본인"), + FRIEND("친구"), + OTHERS("그 외"); + + private final String description; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/PointType.java b/src/main/java/com/umc/networkingService/domain/member/entity/PointType.java index d3e8ae46..94e8b168 100644 --- a/src/main/java/com/umc/networkingService/domain/member/entity/PointType.java +++ b/src/main/java/com/umc/networkingService/domain/member/entity/PointType.java @@ -9,7 +9,7 @@ public enum PointType { PUDDING("은하수를 살짝 얹은 커스터드 푸딩", 5L, null), DOUGHNUT("은하스프링클 도넛", 10L, null); - private final String title; + private final String description; private final Long point; diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/RefreshToken.java b/src/main/java/com/umc/networkingService/domain/member/entity/RefreshToken.java index 9658522b..1b258c73 100644 --- a/src/main/java/com/umc/networkingService/domain/member/entity/RefreshToken.java +++ b/src/main/java/com/umc/networkingService/domain/member/entity/RefreshToken.java @@ -1,11 +1,8 @@ package com.umc.networkingService.domain.member.entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import org.hibernate.annotations.UuidGenerator; import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; diff --git a/src/main/java/com/umc/networkingService/domain/member/entity/SemesterPart.java b/src/main/java/com/umc/networkingService/domain/member/entity/SemesterPart.java new file mode 100644 index 00000000..dae39662 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/entity/SemesterPart.java @@ -0,0 +1,33 @@ +package com.umc.networkingService.domain.member.entity; + +import com.umc.networkingService.global.common.base.BaseEntity; +import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.global.common.enums.Semester; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; +import org.hibernate.annotations.UuidGenerator; + +import java.util.UUID; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("deleted_at is null") +public class SemesterPart extends BaseEntity { + @Id + @UuidGenerator + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn + private Member member; + + @Enumerated(EnumType.STRING) + private Semester semester; + + @Enumerated(EnumType.STRING) + private Part part; +} diff --git a/src/main/java/com/umc/networkingService/domain/member/mapper/MemberMapper.java b/src/main/java/com/umc/networkingService/domain/member/mapper/MemberMapper.java index 65c9008e..4e8682f3 100644 --- a/src/main/java/com/umc/networkingService/domain/member/mapper/MemberMapper.java +++ b/src/main/java/com/umc/networkingService/domain/member/mapper/MemberMapper.java @@ -1,13 +1,22 @@ package com.umc.networkingService.domain.member.mapper; import com.umc.networkingService.config.security.jwt.TokenInfo; +import com.umc.networkingService.domain.member.dto.request.SemesterPartInfo; +import com.umc.networkingService.domain.member.dto.response.MemberInquiryInfoWithPointResponse; +import com.umc.networkingService.domain.member.dto.response.MemberInquiryPointsResponse; +import com.umc.networkingService.domain.member.dto.response.MemberInquiryProfileResponse; import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.entity.*; +import com.umc.networkingService.domain.university.entity.University; import com.umc.networkingService.global.common.enums.Role; -import org.springframework.stereotype.Component;; -import com.umc.networkingService.domain.member.entity.MemberPosition; -import com.umc.networkingService.domain.member.entity.PositionType; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +; @Component public class MemberMapper { @@ -19,11 +28,12 @@ public Member toMember(final String clientId, SocialType socialType){ .build(); } - public MemberLoginResponse toLoginMember(final Member member, TokenInfo tokenInfo) { + public MemberLoginResponse toLoginMember(final Member member, TokenInfo tokenInfo, boolean isServiceMember) { return MemberLoginResponse.builder() .memberId(member.getId()) .accessToken(tokenInfo.getAccessToken()) .refreshToken(tokenInfo.getRefreshToken()) + .isServiceMember(isServiceMember) .build(); } public MemberPosition toMemberPosition(Member member, PositionType type, String position) { @@ -33,4 +43,65 @@ public MemberPosition toMemberPosition(Member member, PositionType type, String .type(type) .build(); } + + public MemberInquiryProfileResponse toInquiryProfileResponse(Member member, MemberRelation owner) { + + String universityName = Optional.ofNullable(member.getUniversity()) + .map(University::getName) + .orElseThrow(() -> new RestApiException(ErrorCode.EMPTY_MEMBER_UNIVERSITY)); + + return MemberInquiryProfileResponse.builder() + .memberId(member.getId()) + .profileImage(member.getProfileImage()) + .universityName(universityName) + .name(member.getName()) + .nickname(member.getNickname()) + .semesterParts(toSemesterPartInfos(member.getSemesterParts())) + .statusMessage(member.getStatusMessage()) + .owner(owner) + .build(); + } + + public MemberInquiryInfoWithPointResponse toInquiryHomeInfoResponse(Member member, int rank) { + return MemberInquiryInfoWithPointResponse.builder() + .profileImage(member.getProfileImage()) + .nickname(member.getNickname()) + .contributionPoint(member.getContributionPoint()) + .contributionRank(rank) + .build(); + } + + public MemberInquiryPointsResponse.UsedHistory toUsedHistory(PointType pointType) { + return MemberInquiryPointsResponse.UsedHistory.builder() + .pointImage(pointType.getImage()) + .point(pointType.getPoint()) + .description(pointType.getDescription()) + .build(); + } + + public MemberInquiryPointsResponse toInquiryPointsResponse(Long point, + List usedHistories) { + return MemberInquiryPointsResponse.builder() + .remainPoint(point) + .usedHistories(usedHistories) + .build(); + } + + public SemesterPart toSemesterPart(Member member, SemesterPartInfo semesterPartInfo) { + return SemesterPart.builder() + .member(member) + .part(semesterPartInfo.getPart()) + .semester(semesterPartInfo.getSemester()) + .build(); + } + + public List toSemesterPartInfos(List semesterParts) { + return semesterParts.stream() + .map(this::toSemesterPartInfo) + .toList(); + } + + private SemesterPartInfo toSemesterPartInfo(SemesterPart semesterPart) { + return new SemesterPartInfo(semesterPart.getPart(), semesterPart.getSemester()); + } } diff --git a/src/main/java/com/umc/networkingService/domain/member/repository/MemberPointRepository.java b/src/main/java/com/umc/networkingService/domain/member/repository/MemberPointRepository.java new file mode 100644 index 00000000..8a2a824d --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/repository/MemberPointRepository.java @@ -0,0 +1,13 @@ +package com.umc.networkingService.domain.member.repository; + +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.MemberPoint; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface MemberPointRepository extends JpaRepository { + Page findAllByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); +} diff --git a/src/main/java/com/umc/networkingService/domain/member/repository/MemberPositionRepository.java b/src/main/java/com/umc/networkingService/domain/member/repository/MemberPositionRepository.java index 28715b13..06c81d7b 100644 --- a/src/main/java/com/umc/networkingService/domain/member/repository/MemberPositionRepository.java +++ b/src/main/java/com/umc/networkingService/domain/member/repository/MemberPositionRepository.java @@ -3,7 +3,9 @@ import com.umc.networkingService.domain.member.entity.MemberPosition; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface MemberPositionRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(UUID id); } diff --git a/src/main/java/com/umc/networkingService/domain/member/repository/MemberRepository.java b/src/main/java/com/umc/networkingService/domain/member/repository/MemberRepository.java index a7963479..12bceb06 100644 --- a/src/main/java/com/umc/networkingService/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/umc/networkingService/domain/member/repository/MemberRepository.java @@ -2,20 +2,18 @@ import com.umc.networkingService.domain.member.entity.Member; import com.umc.networkingService.domain.member.entity.SocialType; -import java.util.Optional; -import java.util.UUID; - +import com.umc.networkingService.domain.university.entity.University; import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; +import java.util.Optional; +import java.util.UUID; public interface MemberRepository extends JpaRepository { - Optional findByClientIdAndSocialType(String clientId, SocialType socialType); - @Query(value = "select m from Member m where m.id = :memberId and m.deletedAt is null") Optional findById(@Param("memberId") UUID memberId); - Boolean existsByNickname(String nickname); - - Member findByNickname(String nickname); + Optional findByClientIdAndSocialType(String clientId, SocialType socialType); + List findAllByUniversityOrderByContributionPointDesc(University university); } diff --git a/src/main/java/com/umc/networkingService/domain/member/repository/RefreshTokenRepository.java b/src/main/java/com/umc/networkingService/domain/member/repository/RefreshTokenRepository.java index 18fe22a5..ec0f7467 100644 --- a/src/main/java/com/umc/networkingService/domain/member/repository/RefreshTokenRepository.java +++ b/src/main/java/com/umc/networkingService/domain/member/repository/RefreshTokenRepository.java @@ -1,7 +1,6 @@ package com.umc.networkingService.domain.member.repository; import com.umc.networkingService.domain.member.entity.RefreshToken; -import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/umc/networkingService/domain/member/repository/SemesterPartRepository.java b/src/main/java/com/umc/networkingService/domain/member/repository/SemesterPartRepository.java new file mode 100644 index 00000000..33efcf71 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/repository/SemesterPartRepository.java @@ -0,0 +1,9 @@ +package com.umc.networkingService.domain.member.repository; + +import com.umc.networkingService.domain.member.entity.SemesterPart; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface SemesterPartRepository extends JpaRepository { +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/AuthService.java b/src/main/java/com/umc/networkingService/domain/member/service/AuthService.java new file mode 100644 index 00000000..3fa3b94a --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/AuthService.java @@ -0,0 +1,19 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.response.MemberGenerateNewAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.response.MemberIdResponse; +import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.global.common.base.EntityLoader; + +import java.util.UUID; + +public interface AuthService extends EntityLoader { + MemberLoginResponse socialLogin(final String accessToken, SocialType socialType); + MemberIdResponse signUp(Member member, MemberSignUpRequest request); + MemberGenerateNewAccessTokenResponse generateNewAccessToken(String refreshToken, Member member); + MemberIdResponse logout(Member member); + MemberIdResponse withdrawal(Member member); +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/AuthServiceImpl.java b/src/main/java/com/umc/networkingService/domain/member/service/AuthServiceImpl.java new file mode 100644 index 00000000..52a06146 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/AuthServiceImpl.java @@ -0,0 +1,218 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.config.security.jwt.JwtTokenProvider; +import com.umc.networkingService.config.security.jwt.TokenInfo; +import com.umc.networkingService.domain.branch.service.BranchUniversityService; +import com.umc.networkingService.domain.member.client.AppleMemberClient; +import com.umc.networkingService.domain.member.client.GoogleMemberClient; +import com.umc.networkingService.domain.member.client.KakaoMemberClient; +import com.umc.networkingService.domain.member.client.NaverMemberClient; +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.response.MemberGenerateNewAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.response.MemberIdResponse; +import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.RefreshToken; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.MemberRepository; +import com.umc.networkingService.domain.university.entity.University; +import com.umc.networkingService.domain.university.service.UniversityService; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final MemberMapper memberMapper; + private final MemberRepository memberRepository; + + private final SemesterPartService semesterPartService; + private final MemberPositionService memberPositionService; + private final RefreshTokenService refreshTokenService; + private final UniversityService universityService; + private final BranchUniversityService branchUniversityService; + + private final JwtTokenProvider jwtTokenProvider; + private final KakaoMemberClient kakaoMemberClient; + private final GoogleMemberClient googleMemberClient; + private final NaverMemberClient naverMemberClient; + private final AppleMemberClient appleMemberClient; + + // 소셜 로그인을 수행하는 함수 + @Override + public MemberLoginResponse socialLogin(String accessToken, SocialType socialType){ + // 로그인 구분 + if(socialType.equals(SocialType.KAKAO)) + return loginByKakao(accessToken); + + if(socialType.equals(SocialType.GOOGLE)) + return loginByGoogle(accessToken); + + if(socialType.equals(SocialType.NAVER)) + return loginByNaver(accessToken); + + if(socialType.equals(SocialType.APPLE)) + return loginByApple(accessToken); + + return null; + } + + // 회원가입을 수행하는 함수 + @Override + @Transactional + public MemberIdResponse signUp(Member loginMember, MemberSignUpRequest request) { + + Member member = loadEntity(loginMember.getId()); + + // 소속 대학교 탐색 + University university = universityService.findUniversityByName(request.getUniversityName()); + + // 멤버 직책 저장 + memberPositionService.saveMemberPositionInfos(member, request.getCampusPositions(), request.getCenterPositions()); + + // 기수별 파트 저장 + semesterPartService.saveSemesterPartInfos(member, request.getSemesterParts()); + + // 이외의 기본 정보 저장 + member.setMemberInfo(request.getName(), request.getNickname(), + university, branchUniversityService.findBranchByUniversity(university)); + + return new MemberIdResponse(memberRepository.save(member).getId()); + } + + // 새로운 액세스 토큰 발급 함수 + @Override + @Transactional + public MemberGenerateNewAccessTokenResponse generateNewAccessToken(String refreshToken, Member loginMember) { + + Member member = loadEntity(loginMember.getId()); + + RefreshToken savedRefreshToken = refreshTokenService.findByMemberId(member.getId()) + .orElseThrow(() -> new RestApiException(ErrorCode.EXPIRED_MEMBER_JWT)); + + // 디비에 저장된 refreshToken과 동일하지 않다면 유효하지 않음 + if (!refreshToken.equals(savedRefreshToken.getRefreshToken())) + throw new RestApiException(ErrorCode.INVALID_REFRESH_TOKEN); + + return new MemberGenerateNewAccessTokenResponse(jwtTokenProvider.generateAccessToken(member.getId())); + } + + // 로그아웃 함수 + @Override + @Transactional + public MemberIdResponse logout(Member loginMember) { + Member member = loadEntity(loginMember.getId()); + + deleteRefreshToken(member); + return new MemberIdResponse(member.getId()); + } + + // 회원 탈퇴 함수 + @Override + @Transactional + public MemberIdResponse withdrawal(Member loginMember) { + Member member = loadEntity(loginMember.getId()); + + // refreshToken 삭제 + deleteRefreshToken(member); + + // 멤버 soft delete + member.delete(); + + return new MemberIdResponse(member.getId()); + } + + private MemberLoginResponse loginByApple(final String accessToken){ + // apple 서버와 통신해서 유저 고유값(clientId) 받기 + String clientId = appleMemberClient.getappleClientID(accessToken); + //존재 여부 파악 + Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.APPLE); + + //1. 없으면 : Member 객체 생성하고 DB 저장 + if(getMember.isEmpty()){ + return saveNewMember(clientId, SocialType.APPLE); + } + // 2. 있으면 : 새로운 토큰 반환 + return getNewToken(getMember.get(), true); + } + + private MemberLoginResponse loginByKakao(final String accessToken){ + // kakao 서버와 통신해서 유저 고유값(clientId) 받기 + String clientId = kakaoMemberClient.getkakaoClientID(accessToken); + // 존재 여부 파악 + Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.KAKAO); + + // 1. 없으면 : Member 객체 생성하고 DB 저장 + if(getMember.isEmpty()) { + return saveNewMember(clientId, SocialType.KAKAO); + } + // 2. 있으면 : 새로운 토큰 반환 + return getNewToken(getMember.get(), true); + } + + private MemberLoginResponse loginByNaver(final String accessToken){ + // naver 서버와 통신해서 유저 고유값(clientId) 받기 + String clientId = naverMemberClient.getnaverClientID(accessToken); + // 존재 여부 파악 + Optional getMember = memberRepository.findByClientIdAndSocialType(clientId,SocialType.NAVER); + + // 1. 없으면 (처음 로그인 하는 경우) + if(getMember.isEmpty()) { + return saveNewMember(clientId,SocialType.NAVER); + } + // 2. 있으면 (이미 로그인 했던 적이 있는 경우) + return getNewToken(getMember.get(), true); + } + + private MemberLoginResponse loginByGoogle(final String accessToken){ + // google 서버와 통신해서 유저 고유값(clientId) 받기 + String clientId = googleMemberClient.getgoogleClientID(accessToken); + // 존재 여부 파악 + Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.GOOGLE); + + // 1. 없으면 : Member 객체 생성하고 DB 저장 + if(getMember.isEmpty()){ + return saveNewMember(clientId, SocialType.GOOGLE); + } + // 2. 있으면 : 새로운 토큰 반환 + return getNewToken(getMember.get(), true); + } + + private MemberLoginResponse saveNewMember(String clientId, SocialType socialType) { + Member member = memberMapper.toMember(clientId, socialType); + Member newMember = memberRepository.save(member); + + return getNewToken(newMember, false); + } + + private MemberLoginResponse getNewToken(Member member, boolean isServiceMember) { + // jwt 토큰 생성 + TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getId()); + // refreshToken 디비에 저장 + refreshTokenService.saveTokenInfo(tokenInfo.getRefreshToken(), member.getId()); + + return memberMapper.toLoginMember(member, tokenInfo, isServiceMember); + } + + // member 객체를 이용한 refreshToken 삭제 함수 + private void deleteRefreshToken(Member member) { + Optional refreshToken = refreshTokenService.findByMemberId(member.getId()); + + refreshToken.ifPresent(refreshTokenService::delete); + } + + // 멤버 로드 함수 + @Override + public Member loadEntity(UUID id) { + return memberRepository.findById(id) + .orElseThrow(() -> new RestApiException(ErrorCode.EMPTY_MEMBER)); + } +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionService.java b/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionService.java new file mode 100644 index 00000000..668949da --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionService.java @@ -0,0 +1,9 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.entity.Member; + +import java.util.List; + +public interface MemberPositionService { + void saveMemberPositionInfos(Member member, List campusPositions, List centerPositions); +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionServiceImpl.java b/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionServiceImpl.java new file mode 100644 index 00000000..fa48c691 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/MemberPositionServiceImpl.java @@ -0,0 +1,43 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.MemberPosition; +import com.umc.networkingService.domain.member.entity.PositionType; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.MemberPositionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MemberPositionServiceImpl implements MemberPositionService { + private final MemberPositionRepository memberPositionRepository; + private final MemberMapper memberMapper; + + // 멤버 직책 정보 저장 함수 + @Override + public void saveMemberPositionInfos(Member member, List campusPositions, List centerPositions) { + // 기존 직책 삭제 + member.getPositions().forEach(MemberPosition::delete); + + List memberPositions = new ArrayList<>(); + memberPositions.addAll(saveMemberPositions(member, campusPositions, PositionType.CAMPUS)); + memberPositions.addAll(saveMemberPositions(member, centerPositions, PositionType.CENTER)); + + member.updatePositions(memberPositions); + } + + // 멤버 직책을 디비에 저장하는 함수 + private List saveMemberPositions(Member member, List positions, PositionType positionType) { + List memberPositions = positions.stream() + .map(position -> memberMapper.toMemberPosition(member, positionType, position)) + .toList(); + + memberPositionRepository.saveAll(memberPositions); + + return memberPositions; + } +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/MemberService.java b/src/main/java/com/umc/networkingService/domain/member/service/MemberService.java index db09720d..f9e6154c 100644 --- a/src/main/java/com/umc/networkingService/domain/member/service/MemberService.java +++ b/src/main/java/com/umc/networkingService/domain/member/service/MemberService.java @@ -1,12 +1,20 @@ package com.umc.networkingService.domain.member.service; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; -import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.domain.member.dto.response.MemberSignUpResponse; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateProfileRequest; +import com.umc.networkingService.domain.member.dto.response.*; import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.global.common.base.EntityLoader; +import org.springframework.web.multipart.MultipartFile; -public interface MemberService { - public MemberLoginResponse socialLogin(final String accessToken, SocialType socialType); - MemberSignUpResponse signUp(Member member, MemberSignUpRequest request); +import java.util.UUID; + +public interface MemberService extends EntityLoader { + MemberIdResponse updateMyProfile(Member member, MultipartFile profileImage, MemberUpdateMyProfileRequest request); + MemberIdResponse updateProfile(Member member, UUID memberId, MemberUpdateProfileRequest request); + MemberInquiryProfileResponse inquiryProfile(Member member, UUID memberId); + MemberInquiryInfoWithPointResponse inquiryInfoWithPoint(Member member); + MemberAuthenticationGithubResponse authenticationGithub(Member member, String code); + MemberInquiryGithubResponse inquiryGithubImage(Member member); + MemberInquiryPointsResponse inquiryMemberPoints(Member member); } diff --git a/src/main/java/com/umc/networkingService/domain/member/service/MemberServiceImpl.java b/src/main/java/com/umc/networkingService/domain/member/service/MemberServiceImpl.java index 398dc746..de6bb1a2 100644 --- a/src/main/java/com/umc/networkingService/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/umc/networkingService/domain/member/service/MemberServiceImpl.java @@ -1,213 +1,240 @@ package com.umc.networkingService.domain.member.service; -import com.umc.networkingService.config.security.jwt.JwtTokenProvider; -import com.umc.networkingService.domain.member.client.AppleMemberClient; -import com.umc.networkingService.domain.member.client.KakaoMemberClient; -import com.umc.networkingService.domain.member.client.NaverMemberClient; -import com.umc.networkingService.domain.member.client.GoogleMemberClient; -import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.config.security.jwt.TokenInfo; -import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.SocialType; + +import com.umc.networkingService.domain.friend.service.FriendService; +import com.umc.networkingService.domain.member.client.GithubMemberClient; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateProfileRequest; +import com.umc.networkingService.domain.member.dto.response.*; +import com.umc.networkingService.domain.member.entity.*; import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.MemberPointRepository; import com.umc.networkingService.domain.member.repository.MemberRepository; +import com.umc.networkingService.domain.university.entity.University; import com.umc.networkingService.global.common.enums.Role; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import com.umc.networkingService.global.utils.S3FileComponent; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import com.umc.networkingService.domain.branch.service.BranchUniversityService; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; -import com.umc.networkingService.domain.member.dto.response.MemberSignUpResponse; -import com.umc.networkingService.domain.member.entity.MemberPosition; -import com.umc.networkingService.domain.member.entity.PositionType; -import com.umc.networkingService.domain.member.repository.MemberPositionRepository; -import com.umc.networkingService.domain.university.entity.University; -import com.umc.networkingService.domain.university.service.UniversityService; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.List; - import java.util.Optional; +import java.util.UUID; @Service @RequiredArgsConstructor public class MemberServiceImpl implements MemberService{ - private final KakaoMemberClient kakaoMemberClient; - private final GoogleMemberClient googleMemberClient; - private final NaverMemberClient naverMemberClient; - private final AppleMemberClient appleMemberClient; + private final MemberMapper memberMapper; private final MemberRepository memberRepository; - private final JwtTokenProvider jwtTokenProvider; - private final RefreshTokenService refreshTokenService; + private final MemberPointRepository memberPointRepository; + + private final SemesterPartService semesterPartService; + private final MemberPositionService memberPositionService; + private final FriendService friendService; + + private final S3FileComponent s3FileComponent; + private final GithubMemberClient githubMemberClient; + // 나의 프로필 업데이트 함수 @Override - public MemberLoginResponse socialLogin(String accessToken, SocialType socialType){ - // 로그인 구분 - if(socialType.equals(SocialType.KAKAO)) - return loginByKakao(accessToken); + @Transactional + public MemberIdResponse updateMyProfile(Member loginMember, MultipartFile profileImage, MemberUpdateMyProfileRequest request) { + + Member member = loadEntity(loginMember.getId()); - if(socialType.equals(SocialType.GOOGLE)) - return loginByGoogle(accessToken); + String profileUrl = null; - if(socialType.equals(SocialType.NAVER)) - return loginByNaver(accessToken); + // 프로필 이미지 s3 저장 + if (profileImage != null) + profileUrl = s3FileComponent.uploadFile("member", profileImage); - if(socialType.equals(SocialType.APPLE)) - return loginByApple(accessToken); + // 수정된 정보 저장 + member.updateMemberInfo(request, profileUrl); - return null; + return new MemberIdResponse(memberRepository.save(member).getId()); } - private MemberLoginResponse loginByApple(final String accessToken){ - // apple 서버와 통신해서 유저 고유값(clientId) 받기 - String clientId = appleMemberClient.getappleClientID(accessToken); - //존재 여부 파악 - Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.APPLE); + // 프로필 수정 함수(운영진용) + @Override + @Transactional + public MemberIdResponse updateProfile(Member member, UUID memberId, MemberUpdateProfileRequest request) { - //1. 없으면 : Member 객체 생성하고 DB 저장 - if(getMember.isEmpty()){ - return saveNewMember(clientId, SocialType.APPLE); - } - // 2. 있으면 : 새로운 토큰 반환 - return getNewToken(getMember.get()); - } + // 수정할 유저 탐색 + Member updateMember = loadEntity(memberId); - private MemberLoginResponse loginByKakao(final String accessToken){ - // kakao 서버와 통신해서 유저 고유값(clientId) 받기 - String clientId = kakaoMemberClient.getkakaoClientID(accessToken); - // 존재 여부 파악 - Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.KAKAO); + // 직책 수정 + if (updateMember.getRole().getPriority() <= member.getRole().getPriority()) { + // 본인보다 높거나 같은 직책의 운영진의 정보를 수정하려 할 경우 + throw new RestApiException(ErrorCode.UNAUTHORIZED_UPDATE_MEMBER); + } - // 1. 없으면 : Member 객체 생성하고 DB 저장 - if(getMember.isEmpty()) { - return saveNewMember(clientId, SocialType.KAKAO); + if (member.getRole().getPriority() > 2 && !request.getCenterPositions().isEmpty()) { + // 학교, 지부 운영진이 중앙 직책을 수정하려는 경우 + throw new RestApiException(ErrorCode.UNAUTHORIZED_UPDATE_CENTER_POSITION); } - // 2. 있으면 : 새로운 토큰 반환 - return getNewToken(getMember.get()); - } + memberPositionService.saveMemberPositionInfos(updateMember, request.getCampusPositions(), request.getCenterPositions()); - private MemberLoginResponse loginByNaver(final String accessToken){ - // naver 서버와 통신해서 유저 고유값(clientId) 받기 - String clientId = naverMemberClient.getnaverClientID(accessToken); - // 존재 여부 파악 - Optional getMember = memberRepository.findByClientIdAndSocialType(clientId,SocialType.NAVER); + // 직책에 따른 Role 수정 + Role newRole = findMemberRole(updateMember.getPositions()); + updateMember.updateRole(newRole); - // 1. 없으면 (처음 로그인 하는 경우) - if(getMember.isEmpty()) { - return saveNewMember(clientId,SocialType.NAVER); - } - // 2. 있으면 (이미 로그인 했던 적이 있는 경우) - return getNewToken(getMember.get()); + // 특정 기수의 파트 변경 + semesterPartService.saveSemesterPartInfos(updateMember, request.getSemesterParts()); + + return new MemberIdResponse(memberRepository.save(updateMember).getId()); } - private MemberLoginResponse loginByGoogle(final String accessToken){ - // google 서버와 통신해서 유저 고유값(clientId) 받기 - String clientId = googleMemberClient.getgoogleClientID(accessToken); - // 존재 여부 파악 - Optional getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.GOOGLE); + // 프로필 조회 함수 + @Override + public MemberInquiryProfileResponse inquiryProfile(Member loginMember, UUID memberId) { - // 1. 없으면 : Member 객체 생성하고 DB 저장 - if(getMember.isEmpty()){ - return saveNewMember(clientId, SocialType.GOOGLE); + Member member = loadEntity(loginMember.getId()); + // 본인 프로필 조회인 경우 + if (memberId == null) { + return memberMapper.toInquiryProfileResponse(member, MemberRelation.MINE); } - // 2. 있으면 : 새로운 토큰 반환 - return getNewToken(getMember.get()); - } - private MemberLoginResponse saveNewMember(String clientId, SocialType socialType) { - Member member = memberMapper.toMember(clientId, socialType); - Member newMember = memberRepository.save(member); + Member inquiryMember = loadEntity(memberId); - return getNewToken(newMember); + // 친구 프로필 조회인 경우 + if (friendService.checkFriend(member, inquiryMember)) { + return memberMapper.toInquiryProfileResponse(inquiryMember, MemberRelation.FRIEND); + } + + // 이외의 프로필 조회인 경우 + return memberMapper.toInquiryProfileResponse(inquiryMember, MemberRelation.OTHERS); } - private MemberLoginResponse getNewToken(Member member) { - // jwt 토큰 생성 - TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getId()); - // refreshToken 디비에 저장 - refreshTokenService.saveTokenInfo(tokenInfo.getRefreshToken(), member.getId()); + // 포인트 관련 정보 조회 + @Override + public MemberInquiryInfoWithPointResponse inquiryInfoWithPoint(Member loginMember) { - return memberMapper.toLoginMember(member, tokenInfo); - } + Member member = loadEntity(loginMember.getId()); - private final MemberPositionRepository memberPositionRepository; + // 소속 대학교 찾기 + University university = Optional.ofNullable(member.getUniversity()) + .orElseThrow(() -> new RestApiException(ErrorCode.EMPTY_MEMBER_UNIVERSITY)); - private final UniversityService universityService; - private final BranchUniversityService branchUniversityService; + // 본인 랭킹 구하기 + int rank = calculateMyRank(member, university); + + return memberMapper.toInquiryHomeInfoResponse(member, rank); + } @Override @Transactional - public MemberSignUpResponse signUp(Member member, MemberSignUpRequest request) { + public MemberAuthenticationGithubResponse authenticationGithub(Member loginMember, String code) { + Member member = loadEntity(loginMember.getId()); - // 소속 대학교 탐색 - University university = universityService.findUniversityByName(request.getUniversityName()); + String gitNickname = githubMemberClient.getGithubNickname(code); - // 멤버 기본 정보 저장 - setMemberInfo(member, request, university); + if (gitNickname == null || gitNickname.isBlank()) + throw new RestApiException(ErrorCode.FAILED_GITHUB_AUTHENTICATION); - // 멤버 직책 저장 - saveMemberPositions(member, request); + member.authenticationGithub(gitNickname); - return new MemberSignUpResponse(memberRepository.save(member).getId()); - } + Member savedMember = memberRepository.save(member); - // 멤버 기본 정보 저장 함수 - private void setMemberInfo(Member member, MemberSignUpRequest request, University university) { - member.setMemberInfo( - request, - findMemberRole(request), - university, - branchUniversityService.findBranchByUniversity(university) - ); + return new MemberAuthenticationGithubResponse(savedMember.getGitNickname()); } - // 멤버 직책 정보 저장 함수 - private void saveMemberPositions(Member member, MemberSignUpRequest request) { - saveMemberPosition(member, request.getCenterPositions(), PositionType.CENTER); - saveMemberPosition(member, request.getCampusPositions(), PositionType.CAMPUS); + @Override + public MemberInquiryGithubResponse inquiryGithubImage(Member loginMember) { + Member member = loadEntity(loginMember.getId()); + + String gitNickName = member.getGitNickname(); + if (gitNickName == null) + throw new RestApiException(ErrorCode.UNAUTHENTICATED_GITHUB); + return new MemberInquiryGithubResponse("https://ghchart.rshah.org/2965FF/" + gitNickName); } - private void saveMemberPosition(Member member, List positions, PositionType positionType) { - List memberPositions = positions.stream() - .map(position -> memberMapper.toMemberPosition(member, positionType, position)) + @Override + public MemberInquiryPointsResponse inquiryMemberPoints(Member loginMember) { + Member member = loadEntity(loginMember.getId()); + + Page usedPointsPage = memberPointRepository. + findAllByMemberOrderByCreatedAtDesc(member, PageRequest.of(0, 2)); + List usedHistories = usedPointsPage.stream() + .map(MemberPoint::getPointType) + .map(memberMapper::toUsedHistory) .toList(); - memberPositionRepository.saveAll(memberPositions); + return memberMapper.toInquiryPointsResponse(member.getRemainPoint(), usedHistories); } - // 멤버 Role 생성 함수 - private Role findMemberRole(MemberSignUpRequest request) { - if (request.getCenterPositions().isEmpty()) { - return findCampusRole(request.getCampusPositions()); - } - return findCenterRole(request.getCenterPositions()); - } - - private Role findCampusRole(List campusPositions) { - if (campusPositions.isEmpty()) { + // 멤버의 새로운 Role 찾기 함수 + private Role findMemberRole(List memberPositions) { + if (memberPositions.isEmpty()) { return Role.MEMBER; } - if (isExecutive(campusPositions)) { - return Role.BRANCH_STAFF; + List centerPositions = findPositionsByType(memberPositions, PositionType.CENTER); + + if (!centerPositions.isEmpty()) { + if (isExecutive(centerPositions)) + return Role.TOTAL_STAFF; + return Role.CENTER_STAFF; } + List campusPositions = findPositionsByType(memberPositions, PositionType.CAMPUS); + + if (isExecutive(campusPositions)) + return Role.BRANCH_STAFF; return Role.CAMPUS_STAFF; } - private Role findCenterRole(List centerPositions) { - if (isExecutive(centerPositions)) { - return Role.TOTAL_STAFF; - } - - return Role.CENTER_STAFF; + // 특정 타입의 직책을 반환하는 함수 + private List findPositionsByType(List memberPositions, PositionType type) { + return memberPositions.stream() + .filter(memberPosition -> memberPosition.getType() == type) + .toList(); } // 회장, 부회장 판별 함수 - private boolean isExecutive(List positions) { + private boolean isExecutive(List positions) { return positions.stream() - .anyMatch(position -> position.equals("회장") || position.equals("부회장")); + .anyMatch(position -> position.getName().equals("회장") || position.getName().equals("부회장")); + } + + + + // 교내 랭킹을 계산하는 함수 + private int calculateMyRank(Member member, University university) { + List universityMembers = memberRepository.findAllByUniversityOrderByContributionPointDesc(university); + + int myRank = 1; + Long prevPoint = -1L; + int count = 0; + + for (Member universityMember : universityMembers) { + if (!universityMember.getContributionPoint().equals(prevPoint)) { + prevPoint = universityMember.getContributionPoint(); + myRank = myRank + count; + count = 1; + } else { + count++; + } + + if (universityMember.getId().equals(member.getId())) { + return myRank; + } + } + + // 학교 구성원 중 멤버를 찾지 못하였을 경우 + throw new RestApiException(ErrorCode.EMPTY_MEMBER_UNIVERSITY); + } + + @Override + public Member loadEntity(UUID id) { + return memberRepository.findById(id) + .orElseThrow(() -> new RestApiException(ErrorCode.EMPTY_MEMBER)); } } diff --git a/src/main/java/com/umc/networkingService/domain/member/service/RefreshTokenService.java b/src/main/java/com/umc/networkingService/domain/member/service/RefreshTokenService.java index ea7f8683..e3f96cdd 100644 --- a/src/main/java/com/umc/networkingService/domain/member/service/RefreshTokenService.java +++ b/src/main/java/com/umc/networkingService/domain/member/service/RefreshTokenService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; import java.util.UUID; @Service @@ -25,8 +26,8 @@ public RefreshToken saveTokenInfo(String refreshToken, UUID memberId) { // Redis } @Transactional - public RefreshToken findByMemberId(UUID memberId) { // 만료된 accessToken으로 refreshToken을 찾아옴 - return refreshTokenRepository.findByMemberId(memberId).orElseThrow(() -> new IllegalArgumentException("Refresh Token이 존재하지 않습니다.")); + public Optional findByMemberId(UUID memberId) { // 만료된 accessToken으로 refreshToken을 찾아옴 + return refreshTokenRepository.findByMemberId(memberId); } @Transactional diff --git a/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartService.java b/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartService.java new file mode 100644 index 00000000..c8381dea --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartService.java @@ -0,0 +1,17 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.dto.request.SemesterPartInfo; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.global.common.base.EntityLoader; + +import java.util.List; +import java.util.UUID; + +public interface SemesterPartService extends EntityLoader { + + void saveSemesterPartInfos(Member member, List semesterPartInfos); + + SemesterPart saveEntity(SemesterPart semesterPart); + +} diff --git a/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartServiceImpl.java b/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartServiceImpl.java new file mode 100644 index 00000000..059a1397 --- /dev/null +++ b/src/main/java/com/umc/networkingService/domain/member/service/SemesterPartServiceImpl.java @@ -0,0 +1,57 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.dto.request.SemesterPartInfo; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.SemesterPartRepository; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class SemesterPartServiceImpl implements SemesterPartService { + + private final SemesterPartRepository semesterPartRepository; + + private final MemberMapper memberMapper; + + // 기수별 파트 정보 저장 함수 + @Override + public void saveSemesterPartInfos(Member member, List semesterPartInfos) { + // 기존 기수별 파트가 있다면 삭제 + member.getSemesterParts().forEach(SemesterPart::delete); + + List semesterParts = saveSemesterParts(member, semesterPartInfos); + + member.updateSemesterParts(semesterParts); + } + + private List saveSemesterParts(Member member, List semesterPartInfos) { + List semesterParts = semesterPartInfos.stream() + .map(semesterPart -> memberMapper.toSemesterPart(member, semesterPart)) + .toList(); + + // 디비에 저장 + return semesterPartRepository.saveAll(semesterParts); + + } + + // 엔티티 저장 함수 + @Override + public SemesterPart saveEntity(SemesterPart semesterPart) { + return semesterPartRepository.save(semesterPart); + } + + // 엔티티 로드 함수 + @Override + public SemesterPart loadEntity(UUID id) { + return semesterPartRepository.findById(id) + .orElseThrow(() -> new RestApiException(ErrorCode.EMPTY_SEMESTER_PART)); + } +} diff --git a/src/main/java/com/umc/networkingService/domain/test/controller/TestController.java b/src/main/java/com/umc/networkingService/domain/test/controller/TestController.java index 45dc81ad..8eb7042c 100644 --- a/src/main/java/com/umc/networkingService/domain/test/controller/TestController.java +++ b/src/main/java/com/umc/networkingService/domain/test/controller/TestController.java @@ -1,10 +1,17 @@ package com.umc.networkingService.domain.test.controller; +import com.umc.networkingService.domain.branch.entity.Branch; +import com.umc.networkingService.domain.branch.entity.BranchUniversity; +import com.umc.networkingService.domain.branch.repository.BranchRepository; +import com.umc.networkingService.domain.branch.repository.BranchUniversityRepository; import com.umc.networkingService.domain.test.dto.TestRequest; import com.umc.networkingService.domain.test.dto.TestResponse; import com.umc.networkingService.domain.test.service.TestService; +import com.umc.networkingService.domain.university.entity.University; +import com.umc.networkingService.domain.university.repository.UniversityRepository; import com.umc.networkingService.global.common.base.BaseResponse; -import io.swagger.v3.oas.annotations.Hidden; +import com.umc.networkingService.global.common.enums.Role; +import com.umc.networkingService.global.common.enums.Semester; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -20,7 +27,9 @@ @RequestMapping("/test") public class TestController { private final TestService testService; - + private final UniversityRepository universityRepository; + private final BranchRepository branchRepository; + private final BranchUniversityRepository branchUniversityRepository; @Operation(summary = "성공적인 응답 반환 API", description = "테스트 문자열을 반환하는 API입니다.") @ApiResponse(responseCode = "200", description = "테스트 문자열을 성공적으로 반환") @@ -57,4 +66,27 @@ public BaseResponse testValidationAPI( .testString(request.getTestString()) .build()); } + + @Operation(summary = "테스트 데이터 생성 API") + @PostMapping("/etc") + public void createMember(@RequestParam Role role) { + Branch branch = branchRepository.save( + Branch.builder() + .name("가지") + .description("가치 지부입니다.") + .semester(Semester.FIFTH) + .build()); + + University university = universityRepository.save( + University.builder() + .name("인하대학교") + .build()); + + BranchUniversity branchUniversity = branchUniversityRepository.save( + BranchUniversity.builder() + .branch(branch) + .university(university) + .isActive(Boolean.TRUE) + .build()); + } } \ No newline at end of file diff --git a/src/main/java/com/umc/networkingService/domain/test/service/TestService.java b/src/main/java/com/umc/networkingService/domain/test/service/TestService.java index d6d972dd..15a24006 100644 --- a/src/main/java/com/umc/networkingService/domain/test/service/TestService.java +++ b/src/main/java/com/umc/networkingService/domain/test/service/TestService.java @@ -1,7 +1,7 @@ package com.umc.networkingService.domain.test.service; -import com.umc.networkingService.global.common.exception.RestApiException; import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/umc/networkingService/domain/university/entity/University.java b/src/main/java/com/umc/networkingService/domain/university/entity/University.java index 9cea8a76..dd8dbe81 100644 --- a/src/main/java/com/umc/networkingService/domain/university/entity/University.java +++ b/src/main/java/com/umc/networkingService/domain/university/entity/University.java @@ -33,7 +33,7 @@ public class University extends BaseEntity { private String semesterLogo; - @Column(nullable = false) + @Column(nullable = false, unique = true) private String name; @ColumnDefault("0") diff --git a/src/main/java/com/umc/networkingService/global/common/base/BaseEntity.java b/src/main/java/com/umc/networkingService/global/common/base/BaseEntity.java index 5ea28b58..6064cf90 100644 --- a/src/main/java/com/umc/networkingService/global/common/base/BaseEntity.java +++ b/src/main/java/com/umc/networkingService/global/common/base/BaseEntity.java @@ -28,4 +28,9 @@ public abstract class BaseEntity { public boolean isDeleted() { return deletedAt != null; } + + // 삭제 처리 메서드 + public void delete() { + deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/umc/networkingService/global/common/exception/ErrorCode.java b/src/main/java/com/umc/networkingService/global/common/exception/ErrorCode.java index 30387c26..0a0e42b6 100644 --- a/src/main/java/com/umc/networkingService/global/common/exception/ErrorCode.java +++ b/src/main/java/com/umc/networkingService/global/common/exception/ErrorCode.java @@ -24,21 +24,32 @@ public enum ErrorCode { // Member EMPTY_MEMBER(HttpStatus.CONFLICT, "MEMBER001", "존재하지 않는 사용자입니다."), + UNAUTHORIZED_UPDATE_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER002", "상위 운영진의 직책을 수정할 수 없습니다."), + UNAUTHORIZED_UPDATE_CENTER_POSITION(HttpStatus.BAD_REQUEST, "MEMBER003", "지부, 학교 운영진은 중앙 직책을 부여할 수 없습니다."), + EMPTY_MEMBER_UNIVERSITY(HttpStatus.CONFLICT, "MEMBER004", "소속 대학교가 존재하지 않는 사용자입니다."), + UNAUTHENTICATED_GITHUB(HttpStatus.BAD_REQUEST, "MEMBER005", "깃허브 연동이 완료되지 않은 사용자입니다."), + + // SemesterPart + EMPTY_SEMESTER_PART(HttpStatus.BAD_REQUEST, "PART006", "존재하지 않는 기수의 파트입니다."), // Auth EMPTY_JWT(HttpStatus.UNAUTHORIZED, "AUTH001", "JWT가 없습니다."), - INVALID_JWT(HttpStatus.UNAUTHORIZED, "AUTH002", "유효하지 않은 JWT입니다."), - EXPIRED_MEMBER_JWT(HttpStatus.UNAUTHORIZED, "AUTH003", "만료된 JWT입니다."), - UNSUPPORTED_JWT(HttpStatus.UNAUTHORIZED, "AUTH004", "지원하지 않는 JWT입니다."), - INVALID_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH005", "유효하지 않은 ID TOKEN입니다."), - INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "AUTH006", "유효하지 않은 ACCESS TOKEN입니다."), + EXPIRED_MEMBER_JWT(HttpStatus.UNAUTHORIZED, "AUTH002", "만료된 JWT입니다."), + UNSUPPORTED_JWT(HttpStatus.UNAUTHORIZED, "AUTH003", "지원하지 않는 JWT입니다."), + INVALID_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH004", "유효하지 않은 ID TOKEN입니다."), + INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "AUTH005", "유효하지 않은 ACCESS TOKEN입니다."), + INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH006", "유효하지 않은 REFRESH TOKEN입니다."), FAILED_SOCIAL_LOGIN(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH007", "소셜 로그인에 실패하였습니다."), + FAILED_GITHUB_AUTHENTICATION(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH008", "깃허브 서버와 통신이 실패하였습니다."), // University EMPTY_UNIVERSITY(HttpStatus.BAD_REQUEST, "UNIVERSITY001", "존재하지 않는 대학교입니다."), // Branch - EMPTY_BRANCH(HttpStatus.BAD_REQUEST, "BRANCH001", "존재하지 않는 지부입니다."); + EMPTY_BRANCH(HttpStatus.BAD_REQUEST, "BRANCH001", "존재하지 않는 지부입니다."), + + // Image + FAILED_UPLOAD_S3_IMAGE(HttpStatus.INTERNAL_SERVER_ERROR, "IMAGE001", "이미지 저장에 실패하였습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/umc/networkingService/global/common/exception/RestApiException.java b/src/main/java/com/umc/networkingService/global/common/exception/RestApiException.java index 94ffea6d..36acc5ac 100644 --- a/src/main/java/com/umc/networkingService/global/common/exception/RestApiException.java +++ b/src/main/java/com/umc/networkingService/global/common/exception/RestApiException.java @@ -2,7 +2,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import org.springframework.http.HttpStatus; @Getter diff --git a/src/main/java/com/umc/networkingService/global/utils/S3FileComponent.java b/src/main/java/com/umc/networkingService/global/utils/S3FileComponent.java index 331d3d7e..000fc8e3 100644 --- a/src/main/java/com/umc/networkingService/global/utils/S3FileComponent.java +++ b/src/main/java/com/umc/networkingService/global/utils/S3FileComponent.java @@ -5,6 +5,8 @@ import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -32,7 +34,8 @@ public String uploadFile(String category, MultipartFile multipartFile) { try { amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), objectMetadata) .withCannedAcl(CannedAccessControlList.PublicRead)); - } catch (IOException ignored) { + } catch (IOException e) { + throw new RestApiException(ErrorCode.FAILED_UPLOAD_S3_IMAGE); } return amazonS3Client.getUrl(bucket, fileName).toString(); diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 5dcdf205..9ed46296 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -13,7 +13,7 @@ spring: jpa: show-sql: true hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: format_sql: true \ No newline at end of file diff --git a/src/test/java/com/umc/networkingService/domain/member/controller/AuthControllerTest.java b/src/test/java/com/umc/networkingService/domain/member/controller/AuthControllerTest.java new file mode 100644 index 00000000..a6cdc267 --- /dev/null +++ b/src/test/java/com/umc/networkingService/domain/member/controller/AuthControllerTest.java @@ -0,0 +1,164 @@ +package com.umc.networkingService.domain.member.controller; + +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.response.MemberGenerateNewAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.response.MemberIdResponse; +import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.service.AuthService; +import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.global.common.enums.Semester; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("Auth 컨트롤러의") +@SpringBootTest +@AutoConfigureMockMvc +public class AuthControllerTest extends MemberControllerTestConfig { + + @MockBean private AuthService authService; + + @Autowired private MemberMapper memberMapper; + + @Test + @DisplayName("소셜 로그인 테스트") + public void loginTest() throws Exception { + // given + String accessToken = "ya29.a0AfB_byD6"; + + MemberLoginResponse response = MemberLoginResponse.builder() + .memberId(UUID.randomUUID()) + .accessToken("서버에서 발급받은 accessToken") + .refreshToken("서버에서 발급받은 refreshToken") + .build(); + + // when + given(authService.socialLogin(accessToken, SocialType.KAKAO)).willReturn(response); + + // then + this.mockMvc.perform(post("/members/login") + .contentType(MediaType.APPLICATION_JSON) + .param("accessToken", accessToken) + .param("socialType", "KAKAO")) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(response.getMemberId().toString())) + .andExpect(jsonPath("$.result.accessToken").value(response.getAccessToken())); + } + + @DisplayName("회원가입 API 테스트") + @Test + public void signUpTest() throws Exception { + // given + + List semesterParts = List.of( + SemesterPart.builder().semester(Semester.THIRD).part(Part.ANDROID).build(), + SemesterPart.builder().semester(Semester.FOURTH).part(Part.SPRING).build(), + SemesterPart.builder().semester(Semester.FIFTH).part(Part.SPRING).build() + ); + + MemberSignUpRequest request = MemberSignUpRequest.builder() + .name("김준석") + .nickname("벡스") + .universityName("인하대학교") + .semesterParts(memberMapper.toSemesterPartInfos(semesterParts)) + .campusPositions(List.of("회장")) + .centerPositions(List.of("Server 파트장")) + .build(); + + MemberIdResponse response = new MemberIdResponse(member.getId()); + + // when + given(authService.signUp(eq(member), any(MemberSignUpRequest.class))).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // then + mockMvc.perform(post("/members") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", accessToken) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); + } + + @DisplayName("Access 토큰 재발급 API 테스트") + @Test + public void generateNewAccessTokenTest() throws Exception { + // given + MemberGenerateNewAccessTokenResponse response = new MemberGenerateNewAccessTokenResponse("newAccessToken"); + + given(authService.generateNewAccessToken(any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(get("/members/token/refresh") + .header("refreshToken", refreshToken)) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.accessToken").value(response.getAccessToken())); + } + + @DisplayName("로그아웃 API 테스트") + @Test + public void logoutTest() throws Exception { + // given + MemberIdResponse response = new MemberIdResponse(member.getId()); + + given(authService.logout(any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(delete("/members/logout") + .header("Authorization", accessToken)) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); + } + + @DisplayName("회원 탈퇴 API 테스트") + @Test + public void withdrawalTest() throws Exception { + // given + MemberIdResponse response = new MemberIdResponse(member.getId()); + + given(authService.withdrawal(any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(delete("/members") + .header("Authorization", accessToken)) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); + } +} diff --git a/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTest.java b/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTest.java index 246047b6..451d3745 100644 --- a/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTest.java @@ -1,130 +1,260 @@ package com.umc.networkingService.domain.member.controller; -import com.umc.networkingService.config.security.jwt.JwtTokenProvider; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; -import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.domain.member.dto.response.MemberSignUpResponse; -import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.SocialType; -import com.umc.networkingService.domain.member.repository.MemberRepository; + +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; +import com.umc.networkingService.domain.member.dto.response.*; +import com.umc.networkingService.domain.member.entity.MemberRelation; +import com.umc.networkingService.domain.member.entity.PointType; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.domain.member.mapper.MemberMapper; import com.umc.networkingService.domain.member.service.MemberService; import com.umc.networkingService.global.common.enums.Part; -import com.umc.networkingService.global.common.enums.Role; import com.umc.networkingService.global.common.enums.Semester; - -import com.fasterxml.jackson.databind.ObjectMapper; -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.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DisplayName("Member 컨트롤러의") -@SpringBootTest -@AutoConfigureMockMvc -public class MemberControllerTest { +public class MemberControllerTest extends MemberControllerTestConfig { - @Autowired private MockMvc mockMvc; + @Autowired private MemberMapper memberMapper; - @Autowired private ObjectMapper objectMapper; + @MockBean private MemberService memberService; - @Autowired private JwtTokenProvider jwtTokenProvider; + @DisplayName("나의 프로필 수정 API 테스트 - 프로필 이미지 없음") + @Test + public void updateMyProfileWithoutImageTest() throws Exception { + // given + MemberUpdateMyProfileRequest request = MemberUpdateMyProfileRequest.builder() + .name("김준석") + .nickname("준써크") + .statusMessage("이번 기수 화이팅~") + .build(); - @MockBean private MemberService memberService; + String requestJson = objectMapper.writeValueAsString(request); + MockMultipartFile requestPart = new MockMultipartFile("request", "", "application/json", requestJson.getBytes(StandardCharsets.UTF_8)); - @MockBean private MemberRepository memberRepository; + MemberIdResponse response = new MemberIdResponse(member.getId()); - private Member member; - private String accessToken; + given(memberService.updateMyProfile(any(), any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); - @BeforeEach - public void setUp() { - member = createMember(); - accessToken = jwtTokenProvider.generateToken(member.getId()).getAccessToken(); + // when & then + mockMvc.perform(MockMvcRequestBuilders.multipart("/members/update") + .file(requestPart) + .header("Authorization", accessToken)) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); } - private Member createMember() { - return Member.builder() - .id(UUID.randomUUID()) - .clientId("123456") - .socialType(SocialType.KAKAO) - .role(Role.MEMBER) + @DisplayName("나의 프로필 수정 API 테스트 - 프로필 이미지 있음") + @Test + public void updateMyProfileWithImageTest() throws Exception { + // given + MemberUpdateMyProfileRequest request = MemberUpdateMyProfileRequest.builder() + .name("김준석") + .nickname("준써크") + .statusMessage("이번 기수 화이팅~") .build(); + + String requestJson = objectMapper.writeValueAsString(request); + MockMultipartFile requestPart = new MockMultipartFile("request", "", "application/json", requestJson.getBytes(StandardCharsets.UTF_8)); + + MockMultipartFile profileImage = new MockMultipartFile("profileImage", "profileImage.jpg", "image/jpeg", "profile image".getBytes()); + + MemberIdResponse response = new MemberIdResponse(member.getId()); + + given(memberService.updateMyProfile(any(), any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.multipart("/members/update") + .file(profileImage) + .file(requestPart) + .header("Authorization", accessToken)) + .andDo(print()) // 응답 출력 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); } + @DisplayName("유저 프로필 조회 API 테스트 - 본인") @Test - @DisplayName("회원가입 API 테스트") - public void signUpTest() throws Exception { + public void inquiryMyProfileTest() throws Exception { // given - MemberSignUpRequest request = MemberSignUpRequest.builder() + List semesterParts = List.of( + SemesterPart.builder().semester(Semester.THIRD).part(Part.ANDROID).build(), + SemesterPart.builder().semester(Semester.FOURTH).part(Part.SPRING).build() + ); + + MemberInquiryProfileResponse response = MemberInquiryProfileResponse.builder() + .memberId(member.getId()) + .profileImage("profileImage") + .universityName("인하대학교") .name("김준석") .nickname("벡스") + .semesterParts(memberMapper.toSemesterPartInfos(semesterParts)) + .statusMessage("아자아자 화이팅") + .owner(MemberRelation.MINE) + .build(); + + given(memberService.inquiryProfile(any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(get("/members") + .header("Authorization", accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); + } + + @DisplayName("유저 프로필 조회 API 테스트 - 타인") + @Test + public void inquiryOthersProfileTest() throws Exception { + // given + List semesterParts = List.of( + SemesterPart.builder().semester(Semester.THIRD).part(Part.ANDROID).build(), + SemesterPart.builder().semester(Semester.FOURTH).part(Part.SPRING).build() + ); + + MemberInquiryProfileResponse response = MemberInquiryProfileResponse.builder() + .memberId(member.getId()) + .profileImage("profileImage") .universityName("인하대학교") - .parts(List.of(Part.SPRING)) - .semesters(List.of(Semester.THIRD, Semester.FOURTH, Semester.FIFTH)) - .campusPositions(List.of("회장")) - .centerPositions(List.of("Server 파트장")) + .name("김준석") + .nickname("벡스") + .semesterParts(memberMapper.toSemesterPartInfos(semesterParts)) + .statusMessage("아자아자 화이팅") + .owner(MemberRelation.OTHERS) .build(); - MemberSignUpResponse response = new MemberSignUpResponse(member.getId()); + given(memberService.inquiryProfile(any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(get("/members/" + member.getId()) + .header("Authorization", accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.memberId").value(member.getId().toString())); + } + + @DisplayName("포인트 관련 유저 정보 조회 API 테스트") + @Test + public void inquiryHomeInfoTest() throws Exception { + // given + MemberInquiryInfoWithPointResponse response = MemberInquiryInfoWithPointResponse.builder() + .profileImage("프로필 이미지") + .nickname("벡스") + .contributionPoint(1000L) + .contributionRank(2) + .build(); + + given(memberService.inquiryInfoWithPoint(any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(get("/members/rank") + .header("Authorization", accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.contributionPoint").value("1000")); + } + + @DisplayName("깃허브 연동 API 테스트") + @Test + public void authenticationGithub() throws Exception { + // given + MemberAuthenticationGithubResponse response = new MemberAuthenticationGithubResponse("junseokkim"); - // when - when(memberService.signUp(eq(member), any(MemberSignUpRequest.class))).thenReturn(response); - when(memberRepository.findById(any(UUID.class))).thenReturn(Optional.of(member)); + given(memberService.authenticationGithub(any(), any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(post("/members/github") + .param("code", "깃허브 인가 코드") + .header("Authorization", accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.githubNickname").value(response.getGithubNickname().toString())); + } - // then - this.mockMvc.perform(post("/members") - .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", accessToken) // accessToken 설정 - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) // 응답 출력 - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value("COMMON200")) - .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) - .andExpect(jsonPath("$.result").exists()); // result 필드가 존재하는지 검사 + @DisplayName("깃허브 데이터 조회 API 테스트") + @Test + public void inquiryGithubImage() throws Exception { + // given + MemberInquiryGithubResponse response = new MemberInquiryGithubResponse( + "https://ghchart.rshah.org/2965FF/junseokkim"); + given(memberService.inquiryGithubImage(any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + // when & then + mockMvc.perform(get("/members/github") + .header("Authorization", accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.githubImage").value(response.getGithubImage().toString())); } + @DisplayName("나의 남은 포인트 및 사용 내역 조회 API 테스트") @Test - @DisplayName("소셜 로그인 테스트") - public void loginTest() throws Exception { + public void inquiryMemberPoints() throws Exception { // given - String accessToken = "ya29.a0AfB_byD6"; + List usedHistories = Stream.of(PointType.PUDDING, PointType.DOUGHNUT) + .map(memberMapper::toUsedHistory) + .toList(); - MemberLoginResponse response = MemberLoginResponse.builder() - .memberId(UUID.randomUUID()) - .accessToken("서버에서 발급받은 accessToken") - .refreshToken("서버에서 발급받은 refreshToken") + MemberInquiryPointsResponse response = MemberInquiryPointsResponse.builder() + .remainPoint(1000L) + .usedHistories(usedHistories) .build(); - // when - when(memberService.socialLogin(accessToken, SocialType.KAKAO)).thenReturn(response); + given(memberService.inquiryMemberPoints(any())).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); - // then - this.mockMvc.perform(post("/members/login") - .contentType(MediaType.APPLICATION_JSON) - .param("accessToken", accessToken) - .param("socialType", "KAKAO")) - .andDo(print()) // 응답 출력 + // when & then + mockMvc.perform(get("/members/points") + .header("Authorization", accessToken)) + .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("COMMON200")) .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) - .andExpect(jsonPath("$.result.memberId").value(response.getMemberId().toString())) - .andExpect(jsonPath("$.result.accessToken").value(response.getAccessToken())); + .andExpect(jsonPath("$.result.remainPoint").value(response.getRemainPoint())) + .andExpect(jsonPath("$.result.usedHistories", hasSize(response.getUsedHistories().size()))); } } \ No newline at end of file diff --git a/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTestConfig.java b/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTestConfig.java new file mode 100644 index 00000000..9db59bec --- /dev/null +++ b/src/test/java/com/umc/networkingService/domain/member/controller/MemberControllerTestConfig.java @@ -0,0 +1,49 @@ +package com.umc.networkingService.domain.member.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.networkingService.config.security.jwt.JwtTokenProvider; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.repository.MemberRepository; +import com.umc.networkingService.global.common.enums.Role; +import org.junit.jupiter.api.BeforeEach; +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.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +@SpringBootTest +@AutoConfigureMockMvc +public abstract class MemberControllerTestConfig { + @Autowired protected MockMvc mockMvc; + @Autowired protected ObjectMapper objectMapper; + @Autowired protected JwtTokenProvider jwtTokenProvider; + @MockBean protected MemberRepository memberRepository; + + protected Member member; + protected String accessToken; + protected String refreshToken; + + @BeforeEach + public void setUp() { + member = createMember(); + setToken(member); + } + + private Member createMember() { + return Member.builder() + .id(UUID.randomUUID()) + .clientId("123456") + .socialType(SocialType.KAKAO) + .role(Role.MEMBER) + .build(); + } + + private void setToken(Member member) { + accessToken = jwtTokenProvider.generateAccessToken(member.getId()); + refreshToken = jwtTokenProvider.generateRefreshToken(member.getId()); + } +} diff --git a/src/test/java/com/umc/networkingService/domain/member/controller/StaffMemberControllerTest.java b/src/test/java/com/umc/networkingService/domain/member/controller/StaffMemberControllerTest.java new file mode 100644 index 00000000..cb054b33 --- /dev/null +++ b/src/test/java/com/umc/networkingService/domain/member/controller/StaffMemberControllerTest.java @@ -0,0 +1,100 @@ +package com.umc.networkingService.domain.member.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.networkingService.config.security.jwt.JwtTokenProvider; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateProfileRequest; +import com.umc.networkingService.domain.member.dto.response.MemberIdResponse; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.MemberRepository; +import com.umc.networkingService.domain.member.service.MemberService; +import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.global.common.enums.Role; +import com.umc.networkingService.global.common.enums.Semester; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@DisplayName("StaffMember 컨트롤러의") +@SpringBootTest +@AutoConfigureMockMvc +public class StaffMemberControllerTest { + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + @Autowired private JwtTokenProvider jwtTokenProvider; + @Autowired private MemberMapper memberMapper; + @MockBean private MemberService memberService; + @MockBean private MemberRepository memberRepository; + + private Member member; + private String accessToken; + + @BeforeEach + public void setUp() { + member = createMember(); + setToken(member); + } + + private Member createMember() { + return Member.builder() + .id(UUID.randomUUID()) + .clientId("111111") + .socialType(SocialType.KAKAO) + .role(Role.MEMBER) + .build(); + } + + private void setToken(Member member) { + accessToken = jwtTokenProvider.generateAccessToken(member.getId()); + } + + // 에러 해결해야함 + @DisplayName("유저 정보 수정 API 테스트") + @Test + public void updateProfileTest() throws Exception { + // given + List semesterParts = List.of( + SemesterPart.builder().semester(Semester.THIRD).part(Part.ANDROID).build(), + SemesterPart.builder().semester(Semester.FOURTH).part(Part.SPRING).build() + ); + + UUID memberId = UUID.randomUUID(); + MemberUpdateProfileRequest request = MemberUpdateProfileRequest.builder() + .campusPositions(List.of()) + .centerPositions(List.of("회장")) + .semesterParts(memberMapper.toSemesterPartInfos(semesterParts)) + .build(); + + MemberIdResponse response = new MemberIdResponse(member.getId()); + given(memberService.updateProfile(any(Member.class), any(UUID.class), any(MemberUpdateProfileRequest.class))).willReturn(response); + given(memberRepository.findById(any(UUID.class))).willReturn(Optional.of(member)); + + // when & then + mockMvc.perform(post("/staff/members/" + memberId + "/update") + .with(user(member.getClientId())) // RequestPostProcessor를 사용하여 요청에 인증 정보 추가 + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", accessToken) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/umc/networkingService/domain/member/service/AuthServiceIntegrationTest.java b/src/test/java/com/umc/networkingService/domain/member/service/AuthServiceIntegrationTest.java new file mode 100644 index 00000000..71ebe247 --- /dev/null +++ b/src/test/java/com/umc/networkingService/domain/member/service/AuthServiceIntegrationTest.java @@ -0,0 +1,187 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.domain.member.client.GoogleMemberClient; +import com.umc.networkingService.domain.member.client.KakaoMemberClient; +import com.umc.networkingService.domain.member.client.NaverMemberClient; +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.dto.response.MemberGenerateNewAccessTokenResponse; +import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.RefreshToken; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.global.common.enums.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@DisplayName("Auth 서비스의 ") +@SpringBootTest +public class AuthServiceIntegrationTest extends MemberServiceTestConfig { + + @Autowired AuthService authService; + + @MockBean private KakaoMemberClient kakaoMemberClient; + @MockBean private GoogleMemberClient googleMemberClient; + @MockBean private NaverMemberClient naverMemberClient; + + @Test + @DisplayName("카카오 로그인 테스트") + @Transactional + public void kakaoLoginTest() { + // given + String accessToken = "ya29.a0AfB_byD6"; + String clientId = "abcdefg"; + + given(kakaoMemberClient.getkakaoClientID(any())).willReturn(clientId); + + //when + MemberLoginResponse response = authService.socialLogin(accessToken, SocialType.KAKAO); + + //then + // 멤버 저장 상태 테스트 + Optional optionalMember = memberRepository.findById(response.getMemberId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); + + assertEquals(clientId, savedMember.getClientId()); + assertEquals(Role.MEMBER, savedMember.getRole()); + assertEquals(SocialType.KAKAO, savedMember.getSocialType()); + + // 리프레시 토큰 저장 상태 테스트 + RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()) + .orElseThrow(); + assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + } + + @Test + @DisplayName("구글 로그인 테스트") + @Transactional + public void googleLoginTest() { + // given + String accessToken = "ya29.a0AfB_byD6"; + String sub = "abcdefg"; + + given(googleMemberClient.getgoogleClientID(any())).willReturn(sub); + + //when + MemberLoginResponse response = authService.socialLogin(accessToken, SocialType.GOOGLE); + + //then + // 멤버 저장 상태 테스트 + Optional optionalMember = memberRepository.findById(response.getMemberId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); + + assertEquals(sub, savedMember.getClientId()); + assertEquals(Role.MEMBER, savedMember.getRole()); + assertEquals(SocialType.GOOGLE, savedMember.getSocialType()); + + // 리프레시 토큰 저장 상태 테스트 + RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()) + .orElseThrow(); + assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + } + + @Test + @DisplayName("네이버 로그인 테스트") + @Transactional + public void naverLoginTest() { + // given + String accessToken = "ya29.a0AfB_byD6"; + String clientId = "abcdefg"; + + given(naverMemberClient.getnaverClientID(any())).willReturn(clientId); + + //when + MemberLoginResponse response = authService.socialLogin(accessToken, SocialType.NAVER); + + //then + // 멤버 저장 상태 테스트 + Optional optionalMember = memberRepository.findById(response.getMemberId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); + + assertEquals(clientId, savedMember.getClientId()); + assertEquals(Role.MEMBER, savedMember.getRole()); + assertEquals(SocialType.NAVER, savedMember.getSocialType()); + + // 리프레시 토큰 저장 상태 테스트 + RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()) + .orElseThrow(); + assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + } + + + @Test + @DisplayName("회원 가입 테스트") + @Transactional + public void signUpTest() { + // given + MemberSignUpRequest request = MemberSignUpRequest.builder() + .name("김준석") + .nickname("벡스") + .universityName("인하대학교") + .semesterParts(memberMapper.toSemesterPartInfos(createSemesterPart(member))) + .campusPositions(List.of("Android 파트장")) + .centerPositions(List.of()) + .build(); + + // when + authService.signUp(member, request); + + // then + Optional optionalMember = memberRepository.findById(member.getId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); + + assertEquals("김준석", savedMember.getName()); + assertEquals("벡스", savedMember.getNickname()); + assertEquals("GACI", savedMember.getBranch().getName()); + assertEquals("인하대학교", savedMember.getUniversity().getName()); + assertEquals(2, savedMember.getSemesterParts().size()); + assertEquals(1, savedMember.getPositions().size()); + } + + @Test + @DisplayName("access 토큰 재발급 테스트") + public void generateNewAccessTokenTest() { + // when + MemberGenerateNewAccessTokenResponse response = authService.generateNewAccessToken(refreshToken, member); + + // then + assertNotNull(response); + assertNotNull(response.getAccessToken()); + } + + @Test + @DisplayName("로그아웃 테스트") + public void logoutTest() { + // when + authService.logout(member); + + // then + assertFalse(refreshTokenService.findByMemberId(member.getId()).isPresent()); + } + + @Test + @DisplayName("회원탈퇴 테스트") + @Transactional + public void withdrawalTest() { + // when + authService.withdrawal(member); + + // then + assertFalse(memberRepository.findById(member.getId()).isPresent()); + assertFalse(refreshTokenService.findByMemberId(member.getId()).isPresent()); + } +} diff --git a/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceIntegrationTest.java b/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceIntegrationTest.java index 24fb0360..8ee596ca 100644 --- a/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceIntegrationTest.java +++ b/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceIntegrationTest.java @@ -1,126 +1,109 @@ package com.umc.networkingService.domain.member.service; -import com.umc.networkingService.config.security.jwt.JwtTokenProvider; -import com.umc.networkingService.domain.branch.entity.Branch; -import com.umc.networkingService.domain.branch.entity.BranchUniversity; -import com.umc.networkingService.domain.branch.repository.BranchRepository; -import com.umc.networkingService.domain.branch.repository.BranchUniversityRepository; -import com.umc.networkingService.domain.member.client.GoogleMemberClient; -import com.umc.networkingService.domain.member.client.KakaoMemberClient; -import com.umc.networkingService.domain.member.client.NaverMemberClient; -import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; -import com.umc.networkingService.domain.member.dto.response.MemberLoginResponse; -import com.umc.networkingService.domain.member.entity.Member; -import com.umc.networkingService.domain.member.entity.RefreshToken; -import com.umc.networkingService.domain.member.entity.SocialType; -import com.umc.networkingService.domain.member.repository.MemberRepository; -import com.umc.networkingService.domain.university.entity.University; -import com.umc.networkingService.domain.university.repository.UniversityRepository; -import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.domain.friend.entity.Friend; +import com.umc.networkingService.domain.friend.repository.FriendRepository; +import com.umc.networkingService.domain.member.client.GithubMemberClient; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateMyProfileRequest; +import com.umc.networkingService.domain.member.dto.request.MemberUpdateProfileRequest; +import com.umc.networkingService.domain.member.dto.response.*; +import com.umc.networkingService.domain.member.entity.*; +import com.umc.networkingService.domain.member.repository.MemberPointRepository; +import com.umc.networkingService.domain.member.repository.MemberPositionRepository; import com.umc.networkingService.global.common.enums.Role; -import com.umc.networkingService.global.common.enums.Semester; -import org.junit.jupiter.api.BeforeEach; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; +import com.umc.networkingService.global.utils.S3FileComponent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; @DisplayName("Member 서비스의 ") @SpringBootTest -public class MemberServiceIntegrationTest { +public class MemberServiceIntegrationTest extends MemberServiceTestConfig { - @Autowired private MemberService memberService; + @Autowired + private MemberService memberService; + @Autowired + private AuthService authService; - @Autowired private MemberRepository memberRepository; - @Autowired private UniversityRepository universityRepository; - @Autowired private BranchRepository branchRepository; - @Autowired private BranchUniversityRepository branchUniversityRepository; + @Autowired + private MemberPositionRepository memberPositionRepository; + @Autowired + private MemberPointRepository memberPointRepository; + @Autowired + private FriendRepository friendRepository; - @Autowired private JwtTokenProvider jwtTokenProvider; - @Autowired private RefreshTokenService refreshTokenService; + @MockBean private S3FileComponent s3FileComponent; + @MockBean private GithubMemberClient githubMemberClient; - @MockBean private KakaoMemberClient kakaoMemberClient; - @MockBean private GoogleMemberClient googleMemberClient; - @MockBean private NaverMemberClient naverMemberClient; - private Member member; - private University university; - private Branch branch; - private BranchUniversity branchUniversity; - - @BeforeEach - public void setUp() { - member = createMember(); - university = createUniversity(); - branch = createBranch(); - branchUniversity = createBranchUniversity(); + private MemberPoint createMemberPoint(PointType pointType) { + return MemberPoint.builder() + .member(member) + .pointType(pointType) + .build(); } - private Member createMember() { - return memberRepository.save( - Member.builder() - .clientId("123456") - .socialType(SocialType.KAKAO) - .role(Role.MEMBER) - .build() - ); - } + @Test + @DisplayName("나의 프로필 수정 테스트(이미지가 없는 경우)") + @Transactional + public void updateMyProfileWithoutImage() { + // given + MemberUpdateMyProfileRequest request = MemberUpdateMyProfileRequest.builder() + .name("김준석") + .nickname("준써크") + .statusMessage("이번 기수 화이팅~") + .build(); - private University createUniversity() { - return universityRepository.save( - University.builder() - .name("인하대학교") - .build() - ); - } + // when + memberService.updateMyProfile(member, null, request); - private Branch createBranch() { - return branchRepository.save( - Branch.builder() - .name("GACI") - .description("가치 지부입니다.") - .semester(Semester.FIFTH) - .build() - ); - } + // then + Optional optionalMember = memberRepository.findById(member.getId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); - private BranchUniversity createBranchUniversity() { - return branchUniversityRepository.save( - BranchUniversity.builder() - .branch(branch) - .university(university) - .isActive(Boolean.TRUE) - .build() - ); + assertEquals("김준석", savedMember.getName()); + assertEquals("이번 기수 화이팅~", savedMember.getStatusMessage()); } @Test - @DisplayName("회원 가입 테스트") + @DisplayName("나의 프로필 수정 테스트(이미지가 있는 경우)") @Transactional - public void signUpTest() { + public void updateMyProfileWithImage() { // given - MemberSignUpRequest request = MemberSignUpRequest.builder() + MemberUpdateMyProfileRequest request = MemberUpdateMyProfileRequest.builder() .name("김준석") - .nickname("벡스") - .universityName("인하대학교") - .parts(List.of(Part.SPRING)) - .semesters(List.of(Semester.THIRD, Semester.FOURTH, Semester.FIFTH)) - .campusPositions(List.of("회장")) - .centerPositions(List.of("Server 파트장")) + .nickname("준써크") + .statusMessage("이번 기수 화이팅~") .build(); + MockMultipartFile profileImage = new MockMultipartFile( + "profileImage", + "profile.png", + "image/png", + "이미지 데이터".getBytes() + ); + + // 테스트에서는 s3 파일 생성 X + given(s3FileComponent.uploadFile(any(), any())).willReturn("s3 url"); + // when - memberService.signUp(member, request); + memberService.updateMyProfile(member, profileImage, request); // then Optional optionalMember = memberRepository.findById(member.getId()); @@ -128,94 +111,283 @@ public void signUpTest() { Member savedMember = optionalMember.get(); assertEquals("김준석", savedMember.getName()); - assertEquals("벡스", savedMember.getNickname()); - assertEquals("GACI", savedMember.getBranch().getName()); - assertEquals("인하대학교", savedMember.getUniversity().getName()); - assertEquals(1, savedMember.getPart().size()); - assertEquals(3, savedMember.getSemester().size()); + assertEquals("이번 기수 화이팅~", savedMember.getStatusMessage()); + assertEquals("s3 url", savedMember.getProfileImage()); } @Test - @DisplayName("카카오 로그인 테스트") + @DisplayName("프로필 수정 테스트") @Transactional - public void kakaoLoginTest() { + public void updateProfile() { // given - String accessToken = "ya29.a0AfB_byD6"; - String clientId = "abcdefg"; - given(kakaoMemberClient.getkakaoClientID(any())).willReturn(clientId); + Member staff = createMember("222222", Role.TOTAL_STAFF); + + member.updatePositions(List.of( + memberPositionRepository.save(MemberPosition.builder() + .member(member) + .type(PositionType.CAMPUS) + .name("Android 파트장") + .build()) + )); - //when - MemberLoginResponse response = memberService.socialLogin(accessToken, SocialType.KAKAO); + MemberUpdateProfileRequest request = MemberUpdateProfileRequest.builder() + .campusPositions(List.of("회장")) + .centerPositions(List.of()) + .semesterParts(memberMapper.toSemesterPartInfos(createSemesterPart(member))) + .build(); + + Optional optionalMemberPosition = member.getPositions().stream().findFirst(); + assertTrue(optionalMemberPosition.isPresent()); + UUID memberPositionId = optionalMemberPosition.get().getId(); + + // when + memberService.updateProfile(staff, member.getId(), request); - //then - // 멤버 저장 상태 테스트 - Optional optionalMember = memberRepository.findById(response.getMemberId()); + // then + Optional optionalMember = memberRepository.findById(member.getId()); assertTrue(optionalMember.isPresent()); Member savedMember = optionalMember.get(); - assertEquals(clientId, savedMember.getClientId()); - assertEquals(Role.MEMBER, savedMember.getRole()); - assertEquals(SocialType.KAKAO, savedMember.getSocialType()); + assertEquals(1, savedMember.getPositions().size()); + assertEquals(2, savedMember.getSemesterParts().size()); + assertEquals(Role.BRANCH_STAFF, savedMember.getRole()); + assertFalse(memberPositionRepository.findByIdAndDeletedAtIsNull(memberPositionId).isPresent()); + } - // 리프레시 토큰 저장 상태 테스트 - RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()); - assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + @Test + @DisplayName("프로필 수정 권한 예외 테스트 - 상위 운영진 정보 수정 시") + @Transactional + public void updateProfileWithUpdateMember() { + // given + Member staff = createMember("222222", Role.CAMPUS_STAFF); + member.updateRole(Role.TOTAL_STAFF); + + MemberUpdateProfileRequest request = MemberUpdateProfileRequest.builder() + .campusPositions(List.of("회장")) + .centerPositions(List.of()) + .semesterParts(memberMapper.toSemesterPartInfos(createSemesterPart(member))) + .build(); + + + // when + RestApiException exception = assertThrows(RestApiException.class, + () -> memberService.updateProfile(staff, member.getId(), request)); + + // then + assertEquals(ErrorCode.UNAUTHORIZED_UPDATE_MEMBER, exception.getErrorCode()); + + Optional optionalMember = memberRepository.findById(member.getId()); + assertTrue(optionalMember.isPresent()); + Member savedMember = optionalMember.get(); + + assertEquals(0, savedMember.getPositions().size()); + assertEquals(0, savedMember.getSemesterParts().size()); } @Test - @DisplayName("구글 로그인 테스트") + @DisplayName("프로필 수정 권한 예외 테스트 - 학교, 지부 운영진이 중앙 직책 부여 시") @Transactional - public void googleLoginTest() { + public void updateProfileWithUpdateCenterPosition() { // given - String accessToken = "ya29.a0AfB_byD6"; - String sub = "abcdefg"; + Member staff = createMember("222222", Role.CAMPUS_STAFF); - given(googleMemberClient.getgoogleClientID(any())).willReturn(sub); + MemberUpdateProfileRequest request = MemberUpdateProfileRequest.builder() + .campusPositions(List.of()) + .centerPositions(List.of("회장")) + .semesterParts(memberMapper.toSemesterPartInfos(createSemesterPart(member))) + .build(); + + + // when + RestApiException exception = assertThrows(RestApiException.class, + () -> memberService.updateProfile(staff, member.getId(), request)); - //when - MemberLoginResponse response = memberService.socialLogin(accessToken, SocialType.GOOGLE); + // then + assertEquals(ErrorCode.UNAUTHORIZED_UPDATE_CENTER_POSITION, exception.getErrorCode()); - //then - // 멤버 저장 상태 테스트 - Optional optionalMember = memberRepository.findById(response.getMemberId()); + Optional optionalMember = memberRepository.findById(member.getId()); assertTrue(optionalMember.isPresent()); Member savedMember = optionalMember.get(); - assertEquals(sub, savedMember.getClientId()); - assertEquals(Role.MEMBER, savedMember.getRole()); - assertEquals(SocialType.GOOGLE, savedMember.getSocialType()); + assertEquals(0, savedMember.getPositions().size()); + assertEquals(0, savedMember.getSemesterParts().size()); + } + + @Test + @DisplayName("유저 프로필 조회 테스트 - 본인") + @Transactional + public void inquiryMyProfile() { + // given + authService.signUp(member, getInfoRequest(member)); + + // when + MemberInquiryProfileResponse response = memberService.inquiryProfile(member, null); + + // then + assertEquals("김준석", response.getName()); + assertEquals("인하대학교", response.getUniversityName()); + assertEquals(MemberRelation.MINE, response.getOwner()); + } + + @Test + @DisplayName("유저 프로필 조회 테스트 - 친구") + @Transactional + public void inquiryFriendProfile() { + // given + Member loginMember = createMember("222222", Role.CAMPUS_STAFF); + + authService.signUp(member, getInfoRequest(member)); - // 리프레시 토큰 저장 상태 테스트 - RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()); - assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + friendRepository.save(Friend.builder() + .sender(loginMember) + .receiver(member) + .build()); + + // when + MemberInquiryProfileResponse response = memberService.inquiryProfile(loginMember, member.getId()); + + // then + assertEquals("김준석", response.getName()); + assertEquals("인하대학교", response.getUniversityName()); + assertEquals(MemberRelation.FRIEND, response.getOwner()); } @Test - @DisplayName("네이버 로그인 테스트") + @DisplayName("유저 프로필 조회 테스트 - 그 외") @Transactional - public void naverLoginTest() { + public void inquiryOthersProfile() { // given - String accessToken = "ya29.a0AfB_byD6"; - String clientId = "abcdefg"; + Member loginMember = createMember("222222", Role.CAMPUS_STAFF); - given(naverMemberClient.getnaverClientID(any())).willReturn(clientId); + authService.signUp(member, getInfoRequest(member)); - //when - MemberLoginResponse response = memberService.socialLogin(accessToken, SocialType.NAVER); + // when + MemberInquiryProfileResponse response = memberService.inquiryProfile(loginMember, member.getId()); + + // then + assertEquals("김준석", response.getName()); + assertEquals("인하대학교", response.getUniversityName()); + assertEquals(MemberRelation.OTHERS, response.getOwner()); + } + + @Test + @DisplayName("포인트 관련 유저 정보 조회 테스트") + @Transactional + public void inquiryHomeInfo() { + // given + authService.signUp(member, getInfoRequest(member)); + member.updateContributionPoint(1000L); + + Member universityMember1 = createMember("222222", Role.MEMBER); + authService.signUp(universityMember1, getInfoRequest(universityMember1)); + universityMember1.updateContributionPoint(2000L); + Member universityMember2 = createMember("333333", Role.MEMBER); + authService.signUp(universityMember2, getInfoRequest(universityMember2)); + universityMember2.updateContributionPoint(2000L); + Member universityMember3 = createMember("444444", Role.MEMBER); + authService.signUp(universityMember3, getInfoRequest(universityMember3)); + universityMember3.updateContributionPoint(3000L); + Member universityMember4 = createMember("555555", Role.MEMBER); + authService.signUp(universityMember4, getInfoRequest(universityMember4)); + universityMember4.updateContributionPoint(1000L); - //then - // 멤버 저장 상태 테스트 - Optional optionalMember = memberRepository.findById(response.getMemberId()); + // when + MemberInquiryInfoWithPointResponse response = memberService.inquiryInfoWithPoint(member); + + // then + assertEquals("벡스", response.getNickname()); + assertEquals(1000L, response.getContributionPoint()); + assertEquals(4, response.getContributionRank()); + } + + @Test + @DisplayName("깃허브 연동 테스트") + @Transactional + public void authenticationGithub() { + // given + + // 실제 깃허브 서버와 통신 x + given(githubMemberClient.getGithubNickname(any())).willReturn("junseokkim"); + + // when + MemberAuthenticationGithubResponse response = memberService.authenticationGithub(member, "깃허브 인가 코드"); + + // then + assertEquals("junseokkim", response.getGithubNickname()); + } + + @Test + @DisplayName("깃허브 데이터 조회 테스트") + @Transactional + public void inquiryGithubImage() { + // given + member.authenticationGithub("junseokkim"); + + // when + MemberInquiryGithubResponse response = memberService.inquiryGithubImage(member); + + // then + assertEquals("https://ghchart.rshah.org/2965FF/junseokkim", response.getGithubImage()); + } + + @Test + @DisplayName("깃허브 데이터 조회 테스트 - 연동 안된 경우") + @Transactional + public void inquiryGithubImageWithException() { + // when + RestApiException exception = assertThrows(RestApiException.class, + () -> memberService.inquiryGithubImage(member)); + + // then + assertEquals(ErrorCode.UNAUTHENTICATED_GITHUB, exception.getErrorCode()); + + Optional optionalMember = memberRepository.findById(member.getId()); assertTrue(optionalMember.isPresent()); Member savedMember = optionalMember.get(); - assertEquals(clientId, savedMember.getClientId()); - assertEquals(Role.MEMBER, savedMember.getRole()); - assertEquals(SocialType.NAVER, savedMember.getSocialType()); + assertNull(savedMember.getGitNickname()); + } + + @Test + @DisplayName("나의 남은 포인트 및 사용 내역 조회 테스트 - 1개") + @Transactional + public void inquiryMemberPoints() { + // given + memberPointRepository.save(MemberPoint.builder() + .member(member) + .pointType(PointType.PUDDING) + .build()); + + // when + MemberInquiryPointsResponse response = memberService.inquiryMemberPoints(member); + + // then + assertNull(response.getRemainPoint()); + assertEquals(1, response.getUsedHistories().size()); + } + + @Test + @DisplayName("나의 남은 포인트 및 사용 내역 조회 테스트 - 2개 이상") + @Transactional + public void inquiryMemberPointsWithMultiple() { + // given + try { + memberPointRepository.save(createMemberPoint(PointType.PUDDING)); + memberPointRepository.save(createMemberPoint(PointType.PUDDING)); + Thread.sleep(1000); // 1초 대기 + memberPointRepository.save(createMemberPoint(PointType.DOUGHNUT)); + } catch (InterruptedException ignored) { + } + + // when + MemberInquiryPointsResponse response = memberService.inquiryMemberPoints(member); - // 리프레시 토큰 저장 상태 테스트 - RefreshToken refreshToken = refreshTokenService.findByMemberId(savedMember.getId()); - assertEquals(response.getRefreshToken(), refreshToken.getRefreshToken()); + // then + assertNull(response.getRemainPoint()); + List points = response.getUsedHistories().stream() + .map(MemberInquiryPointsResponse.UsedHistory::getPoint) + .toList(); + assertEquals(List.of(10L, 5L), points); } } \ No newline at end of file diff --git a/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceTestConfig.java b/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceTestConfig.java new file mode 100644 index 00000000..bbd4deea --- /dev/null +++ b/src/test/java/com/umc/networkingService/domain/member/service/MemberServiceTestConfig.java @@ -0,0 +1,117 @@ +package com.umc.networkingService.domain.member.service; + +import com.umc.networkingService.config.security.jwt.JwtTokenProvider; +import com.umc.networkingService.domain.branch.entity.Branch; +import com.umc.networkingService.domain.branch.entity.BranchUniversity; +import com.umc.networkingService.domain.branch.repository.BranchRepository; +import com.umc.networkingService.domain.branch.repository.BranchUniversityRepository; +import com.umc.networkingService.domain.member.dto.request.MemberSignUpRequest; +import com.umc.networkingService.domain.member.entity.Member; +import com.umc.networkingService.domain.member.entity.SemesterPart; +import com.umc.networkingService.domain.member.entity.SocialType; +import com.umc.networkingService.domain.member.mapper.MemberMapper; +import com.umc.networkingService.domain.member.repository.MemberRepository; +import com.umc.networkingService.domain.member.repository.SemesterPartRepository; +import com.umc.networkingService.domain.university.entity.University; +import com.umc.networkingService.domain.university.repository.UniversityRepository; +import com.umc.networkingService.global.common.enums.Part; +import com.umc.networkingService.global.common.enums.Role; +import com.umc.networkingService.global.common.enums.Semester; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; +import java.util.UUID; + +@SpringBootTest +public abstract class MemberServiceTestConfig { + + @Autowired protected MemberMapper memberMapper; + @Autowired protected MemberRepository memberRepository; + @Autowired protected SemesterPartRepository semesterPartRepository; + @Autowired protected UniversityRepository universityRepository; + @Autowired protected BranchRepository branchRepository; + @Autowired protected BranchUniversityRepository branchUniversityRepository; + + @Autowired protected JwtTokenProvider jwtTokenProvider; + @Autowired protected RefreshTokenService refreshTokenService; + + protected String refreshToken; + protected University university; + protected Branch branch; + protected BranchUniversity branchUniversity; + protected Member member; + + @BeforeEach + public void setUp() { + member = createMember("111111", Role.MEMBER); + setToken(member.getId()); + university = createUniversity(); + branch = createBranch(); + branchUniversity = createBranchUniversity(); + } + + protected Member createMember(String clientId, Role role) { + return memberRepository.save( + Member.builder() + .clientId(clientId) + .socialType(SocialType.KAKAO) + .role(role) + .build() + ); + } + + protected MemberSignUpRequest getInfoRequest(Member nowMember) { + return MemberSignUpRequest.builder() + .name("김준석") + .nickname("벡스") + .universityName("인하대학교") + .semesterParts(memberMapper.toSemesterPartInfos(createSemesterPart(member))) + .campusPositions(List.of("Android 파트장")) + .centerPositions(List.of()) + .build(); + } + + protected List createSemesterPart(Member member) { + List semesterParts = List.of( + SemesterPart.builder().member(member).part(Part.ANDROID).semester(Semester.THIRD).build(), + SemesterPart.builder().member(member).part(Part.SPRING).semester(Semester.FOURTH).build() + ); + + return semesterPartRepository.saveAll(semesterParts); + } + + protected void setToken(UUID memberId) { + refreshToken = jwtTokenProvider.generateRefreshToken(memberId); + refreshTokenService.saveTokenInfo(refreshToken, memberId); + } + + protected University createUniversity() { + return universityRepository.save( + University.builder() + .name("인하대학교") + .build() + ); + } + + protected Branch createBranch() { + return branchRepository.save( + Branch.builder() + .name("GACI") + .description("가치 지부입니다.") + .semester(Semester.FIFTH) + .build() + ); + } + + protected BranchUniversity createBranchUniversity() { + return branchUniversityRepository.save( + BranchUniversity.builder() + .branch(branch) + .university(university) + .isActive(Boolean.TRUE) + .build() + ); + } +} diff --git a/src/test/java/com/umc/networkingService/domain/member/service/RefreshTokenServiceIntegrationTest.java b/src/test/java/com/umc/networkingService/domain/member/service/RefreshTokenServiceIntegrationTest.java index 03367f01..c067bf5f 100644 --- a/src/test/java/com/umc/networkingService/domain/member/service/RefreshTokenServiceIntegrationTest.java +++ b/src/test/java/com/umc/networkingService/domain/member/service/RefreshTokenServiceIntegrationTest.java @@ -2,12 +2,13 @@ import com.umc.networkingService.config.security.jwt.JwtTokenProvider; import com.umc.networkingService.domain.member.entity.RefreshToken; -import com.umc.networkingService.domain.member.service.RefreshTokenService; +import com.umc.networkingService.global.common.exception.ErrorCode; +import com.umc.networkingService.global.common.exception.RestApiException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.junit.jupiter.api.Test; import java.util.UUID; @@ -48,7 +49,8 @@ void testFindByAccessToken() { RefreshToken savedToken = refreshTokenService.saveTokenInfo(REFRESHTOKEN, MEMBERID); // When - RefreshToken foundToken = refreshTokenService.findByMemberId(MEMBERID); + RefreshToken foundToken = refreshTokenService.findByMemberId(MEMBERID) + .orElseThrow(() -> new RestApiException(ErrorCode.EXPIRED_MEMBER_JWT)); // Then (test에서 사용되는 assertion, 조건이 참이 아니라면 테스트 실패) assertNotNull(foundToken); @@ -65,14 +67,16 @@ void testDeleteByAccessToken() { RefreshToken savedToken = refreshTokenService.saveTokenInfo(REFRESHTOKEN, MEMBERID); - RefreshToken tokenToDelete = refreshTokenService.findByMemberId(MEMBERID); + RefreshToken tokenToDelete = refreshTokenService.findByMemberId(MEMBERID) + .orElseThrow(() -> new RestApiException(ErrorCode.EXPIRED_MEMBER_JWT)); + // When refreshTokenService.delete(tokenToDelete); // Then (test에서 사용되는 assertion, 조건이 참이 아니라면 테스트 실패) - assertThrows(IllegalArgumentException.class, () -> refreshTokenService.findByMemberId(MEMBERID)); + assertFalse(refreshTokenService.findByMemberId(MEMBERID).isPresent(), "Optional 값이 비어있어야 성공으로 처리됩니다."); } @Test