diff --git a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java index 2ef434a..de00b68 100644 --- a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java +++ b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java @@ -42,7 +42,7 @@ public class JoinCrew extends BaseEntity { @JoinColumn(name = "crew_id") private Crew crew; - void approveJoin() { + public void approveJoin() { this.role = Role.MEMBER; this.joinedDate = LocalDate.now(); this.joinStatus = JoinStatus.APPROVED; diff --git a/src/main/java/run/backend/domain/member/controller/MemberController.java b/src/main/java/run/backend/domain/member/controller/MemberController.java new file mode 100644 index 0000000..bff7acd --- /dev/null +++ b/src/main/java/run/backend/domain/member/controller/MemberController.java @@ -0,0 +1,28 @@ +package run.backend.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.service.MemberServiceImpl; +import run.backend.global.annotation.member.Login; +import run.backend.global.common.response.CommonResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +@Tag(name = "Members", description = "Member 관련 API") +public class MemberController { + + private final MemberServiceImpl memberService; + + @Operation(summary = "유저 정보 조회", description = "마이페이지 상단 유저 정보를 조회하는 API 입니다.") + @GetMapping + public CommonResponse getMemberInfo(@Login Member member) { + + MemberInfoResponse response = memberService.getMemberInfo(member); + return new CommonResponse<>("유저 정보 조회 성공", response); + } +} diff --git a/src/main/java/run/backend/domain/member/dto/request/MemberInfoRequest.java b/src/main/java/run/backend/domain/member/dto/request/MemberInfoRequest.java index 2aa4798..854e3b2 100644 --- a/src/main/java/run/backend/domain/member/dto/request/MemberInfoRequest.java +++ b/src/main/java/run/backend/domain/member/dto/request/MemberInfoRequest.java @@ -1,4 +1,10 @@ package run.backend.domain.member.dto.request; -public record MemberInfoRequest() { +import run.backend.domain.member.enums.Gender; + +public record MemberInfoRequest( + Gender gender, + int age, + String nickname +) { } diff --git a/src/main/java/run/backend/domain/member/dto/response/MemberInfoResponse.java b/src/main/java/run/backend/domain/member/dto/response/MemberInfoResponse.java index b39a408..8b9fafb 100644 --- a/src/main/java/run/backend/domain/member/dto/response/MemberInfoResponse.java +++ b/src/main/java/run/backend/domain/member/dto/response/MemberInfoResponse.java @@ -1,4 +1,8 @@ package run.backend.domain.member.dto.response; -public record MemberInfoResponse() { +public record MemberInfoResponse( + String profileImageUrl, + String nickName, + String crewName +) { } diff --git a/src/main/java/run/backend/domain/member/entity/Member.java b/src/main/java/run/backend/domain/member/entity/Member.java index 962995b..a94ad15 100644 --- a/src/main/java/run/backend/domain/member/entity/Member.java +++ b/src/main/java/run/backend/domain/member/entity/Member.java @@ -47,6 +47,12 @@ public class Member extends BaseEntity { private boolean pushEnabled; + public void setMemberDefaultInfo(Gender gender, int age, String nickname) { + this.gender = gender; + this.age = age; + this.nickname = nickname; + } + @Builder public Member(String username, String nickname, Gender gender, int age, String oauthId, OAuthType oauthType, String profileImage) { this.username = username; diff --git a/src/main/java/run/backend/domain/member/exception/MemberErrorCode.java b/src/main/java/run/backend/domain/member/exception/MemberErrorCode.java new file mode 100644 index 0000000..87b68ee --- /dev/null +++ b/src/main/java/run/backend/domain/member/exception/MemberErrorCode.java @@ -0,0 +1,15 @@ +package run.backend.domain.member.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import run.backend.global.exception.ErrorCode; + +@Getter +@AllArgsConstructor +public enum MemberErrorCode implements ErrorCode { + + MEMBER_NOT_JOINED_CREW(5001, "가입한 크루가 없습니다."); + + private final int errorCode; + private final String errorMessage; +} diff --git a/src/main/java/run/backend/domain/member/exception/MemberException.java b/src/main/java/run/backend/domain/member/exception/MemberException.java new file mode 100644 index 0000000..fed1333 --- /dev/null +++ b/src/main/java/run/backend/domain/member/exception/MemberException.java @@ -0,0 +1,16 @@ +package run.backend.domain.member.exception; + +import run.backend.global.exception.CustomException; + +public class MemberException extends CustomException { + + public MemberException(final MemberErrorCode memberErrorCode) { + super(memberErrorCode); + } + + public static class MemberNotJoinedCrew extends MemberException { + public MemberNotJoinedCrew() { + super(MemberErrorCode.MEMBER_NOT_JOINED_CREW); + } + } +} diff --git a/src/main/java/run/backend/domain/member/repository/MemberRepository.java b/src/main/java/run/backend/domain/member/repository/MemberRepository.java index 333559d..c49c9d1 100644 --- a/src/main/java/run/backend/domain/member/repository/MemberRepository.java +++ b/src/main/java/run/backend/domain/member/repository/MemberRepository.java @@ -2,8 +2,20 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.member.entity.Member; public interface MemberRepository extends JpaRepository { Optional findByOauthId(String oauthId); + + @Query(""" + SELECT jc.crew + FROM JoinCrew jc + WHERE jc.member.id = :memberId + AND jc.joinStatus = :status + """) + Optional findCrewByMemberIdAndStatus(@Param("memberId") Long memberId, @Param("status") JoinStatus status); } diff --git a/src/main/java/run/backend/domain/member/service/MemberService.java b/src/main/java/run/backend/domain/member/service/MemberService.java index ecdd34b..ec19efd 100644 --- a/src/main/java/run/backend/domain/member/service/MemberService.java +++ b/src/main/java/run/backend/domain/member/service/MemberService.java @@ -1,17 +1,17 @@ package run.backend.domain.member.service; -import run.backend.domain.member.dto.request.MemberInfoRequest; import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.entity.Member; public interface MemberService { - void updateMember(Long memberId, MemberInfoRequest memberInfoRequest); + MemberInfoResponse getMemberInfo(Member member); - MemberInfoResponse getMemberInfo(Long memberId); +// void updateMember(); - void deleteMember(Long memberId); - - void leaveCrew(Long memberId, Long crewId); - - void joinCrew(String crewCode); +// void deleteMember(Member member); +// +// void leaveCrew(Member member, Long crewId); +// +// void joinCrew(String crewCode); } diff --git a/src/main/java/run/backend/domain/member/service/MemberServiceImpl.java b/src/main/java/run/backend/domain/member/service/MemberServiceImpl.java new file mode 100644 index 0000000..fa4a1f1 --- /dev/null +++ b/src/main/java/run/backend/domain/member/service/MemberServiceImpl.java @@ -0,0 +1,28 @@ +package run.backend.domain.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberServiceImpl implements MemberService { + + private final MemberRepository memberRepository; + + @Override + public MemberInfoResponse getMemberInfo(Member member) { + + String crewName = memberRepository.findCrewByMemberIdAndStatus(member.getId(), JoinStatus.APPROVED) + .map(Crew::getName) + .orElse("N/A"); + + return new MemberInfoResponse(member.getProfileImage(), member.getNickname(), crewName); + } +} diff --git a/src/main/java/run/backend/global/annotation/member/Login.java b/src/main/java/run/backend/global/annotation/member/Login.java new file mode 100644 index 0000000..03ce2b7 --- /dev/null +++ b/src/main/java/run/backend/global/annotation/member/Login.java @@ -0,0 +1,11 @@ +package run.backend.global.annotation.member; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Login { +} diff --git a/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java b/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java new file mode 100644 index 0000000..c381ec4 --- /dev/null +++ b/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java @@ -0,0 +1,35 @@ +package run.backend.global.annotation.member; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import run.backend.domain.member.entity.Member; +import run.backend.global.security.CustomUserDetails; + +@Component +@RequiredArgsConstructor +public class LoginArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + + boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); + boolean isMemberType = parameter.getParameterType().equals(Member.class); + return hasLoginAnnotation && isMemberType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + CustomUserDetails principal = (CustomUserDetails) authentication.getPrincipal(); + return principal.getMember(); + } +} diff --git a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java index f569aa4..aba91ea 100644 --- a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import run.backend.domain.auth.exception.AuthException; import run.backend.domain.file.exception.FileException; +import run.backend.domain.member.exception.MemberException; import run.backend.global.common.response.CommonResponse; import run.backend.global.exception.httpError.HttpErrorCode; @@ -18,7 +19,8 @@ public class GlobalExceptionHandler { @ExceptionHandler({ AuthException.RefreshTokenNotFound.class, - FileException.FileNotFound.class + FileException.FileNotFound.class, + MemberException.MemberNotJoinedCrew.class }) public ResponseEntity> handleNotFound(final CustomException e) { diff --git a/src/test/java/run/backend/domain/member/service/MemberServiceTest.java b/src/test/java/run/backend/domain/member/service/MemberServiceTest.java new file mode 100644 index 0000000..f62b6e5 --- /dev/null +++ b/src/test/java/run/backend/domain/member/service/MemberServiceTest.java @@ -0,0 +1,68 @@ +package run.backend.domain.member.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.OAuthType; +import run.backend.domain.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private MemberServiceImpl memberService; + + private Member testMember; + private Crew testCrew; + + @BeforeEach + public void setUp() { + + // member + testMember = Member.builder() + .username("test username") + .oauthId("test id") + .oauthType(OAuthType.GOOGLE) + .profileImage("test image") + .build(); + + // crew + testCrew = Crew.builder() + .name("test crew name") + .description("크루 소개 테스트") + .image("test image url") + .build(); + } + + @Test + @DisplayName("회원 정보가 올바르게 조회 되는지 확인") + public void getMemberInfoTest() { + + // given + when(memberRepository.findCrewByMemberIdAndStatus(testMember.getId(), JoinStatus.APPROVED)) + .thenReturn(Optional.of(testCrew)); + + // when + MemberInfoResponse response = memberService.getMemberInfo(testMember); + + // then + assertEquals(testMember.getNickname(), response.nickName()); + assertEquals(testCrew.getName(), response.crewName()); + } +}