본문 바로가기
Project/깃터디 (gitudy)

[깃터디/Member] 스터디 가입 신청 api 구현

by J-rain 2024. 5. 11.

 

💡 .java를 클릭시 관련 커밋으로 이동💡

 회고 

스터디 가입 신청은 생각보다 신청경우, 예외처리 부분이 생각할 것이 조금 많았다. 스터디의 경우 공개 스터디와 비공개 스터디로 나뉘며 비공개 스터디일 경우 ‘참여코드’가 일치하면 가입신청이 가능했기에 생각할 부분이 많았다.

 

우선 StudyMemberStatus에서 상태를 나타내는 필드가 처음에는 활동중인/탈퇴한 스터디원만 확인할 수 있었기에 브랜치를 새로파서 StudyMemberStatus.java 이렇게 추가해 주었다.

 

추가된 필드는 STUDY_WAITING, STUDY_REFUSED 승인 대기중인 유저와, 승인 거부된 유저 필드이다.

 

스터디의 가입 진행 절차를 생각해보면 유저의 가입신청 → 승인 대기중 유저로 객체 생성 → 스터디장의 승인/거부 → 가입 성공 순이다.

 

여기서 유저의 가입 신청 → 승인 대기중 유저로 객체 생성까지 구현하고 다른 api를 통해 스터디장의 승인/거부와 가입 성공을 함께 처리할 생각이다.

public static StudyMember waitingStudyMember(Long studyInfoId, Long userId) {
        return StudyMember.builder()
                .studyInfoId(studyInfoId)
                .userId(userId)
                .role(StudyMemberRole.STUDY_MEMBER)
                .status(StudyMemberStatus.STUDY_WAITING)
                .score(0)
                .build();

이 메서드를 통해 승인 대기중인 유저 생성 을 처리한다.

예외 처리 생각할 것

  • 공개 스터디, 비공개 스터인지 판별
  • 비공개 스터디일 경우 ‘참여코드 확인’
    • 참여코드가 null인 경우
    • 참여코드가 일치/일치하지 않는 경우
    • 참여코드가 10자 이상인 경우
  • 해당 유저가 이전의 강퇴되었던 유저인지 확인 → (이전에 강퇴유저인경우 재가입 불가)
  • 해당 유저가 스터디에 이미 가입 신청한 상태인지 확인 → (중복 검증)
  • 해당 유저가 탈퇴했거나, 승인 거부된 유저인지 확인 → (가입신청은 되지만 스터디장이 판단)

등을 생각하면서 구현했다.

구현하면서 생각할 점이 많았지만 막상 구현하고 테스트가 통과되면 짜릿함이 있는 것 같다. 🌟

 

 

StudyMemberRepositoryCustom.java

    // UserId와 StudyInfoId를 통해 사용자가 해당 스터디의 강퇴자 였는지 판별한다.
    public boolean isResignedStudyMemberByUserIdAndStudyInfoId(Long userId, Long studyInfoId);

    // UserId와 StudyInfoId를 통해 사용자가 해당 스터디에 이미 가입 신청했는지 판별한다.
    public boolean isWaitingStudyMemberByUserIdAndStudyInfoId(Long userId, Long studyInfoId);

먼저 판별하기 위해 정의

 

 

StudyMemberRepositoryImpl.java

    @Override
    public boolean isResignedStudyMemberByUserIdAndStudyInfoId(Long userId, Long studyInfoId) {

        return queryFactory.from(studyMember)
                .where(studyMember.studyInfoId.eq(studyInfoId)
                        .and(studyMember.userId.eq(userId))
                        .and(studyMember.status.eq(StudyMemberStatus.STUDY_RESIGNED)))
                .fetchFirst() != null;
    }

    @Override
    public boolean isWaitingStudyMemberByUserIdAndStudyInfoId(Long userId, Long studyInfoId) {
        return queryFactory.from(studyMember)
                .where(studyMember.studyInfoId.eq(studyInfoId)
                        .and(studyMember.userId.eq(userId))
                        .and(studyMember.status.eq(StudyMemberStatus.STUDY_WAITING)))
                .fetchFirst() != null;
    }

해당 판별 로직 쿼리 작성

 

 

StudyMemberController.java

    // 스터디 가입 신청
    @ApiResponse(responseCode = "200", description = "스터디 가입 신청 성공")
    @PostMapping("/{studyInfoId}/apply")
    public JsonResult<?> applyStudyMember(@AuthenticationPrincipal User user,
                                          @PathVariable(name = "studyInfoId") Long studyInfoId,
                                          @RequestParam(name = "joinCode", required = false) String joinCode) {

        UserInfoResponse userInfo = authService.findUserInfo(user);

        studyMemberService.applyStudyMember(userInfo, studyInfoId, joinCode);

        return JsonResult.successOf("Apply StudyMember Success");
    }

joinCode (참여코드) @RequestParam 으로 받아서 처리했다. (required 추가하여 필수가 아니고 비공개 스터디일 경우에만 요청처리)

 

 

StudyMemberService.java

    // 스터디 가입 메서드
    @Transactional
    public void applyStudyMember(UserInfoResponse user, Long studyInfoId, String joinCode) {

        // 스터디 조회 예외처리
        StudyInfo studyInfo = studyInfoRepository.findById(studyInfoId).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", studyInfoId, ExceptionMessage.STUDY_INFO_NOT_FOUND);
            return new StudyInfoException(ExceptionMessage.STUDY_INFO_NOT_FOUND);
        });

        // 비공개 스터디인 경우 joinCode 검증 (null, 맞지않을때, 10자를 넘겼을때)
        if (studyInfo.getStatus() == StudyStatus.STUDY_PRIVATE) {
            if (joinCode == null || !joinCode.equals(studyInfo.getJoinCode()) || joinCode.length() > JOIN_CODE_LENGTH) {
                log.warn(">>>> {} : {} <<<<", joinCode, ExceptionMessage.STUDY_JOIN_CODE_FAIL);
                throw new MemberException(ExceptionMessage.STUDY_JOIN_CODE_FAIL);
            }
        }
        // 스터디 멤버인지확인
        if (studyMemberRepository.existsStudyMemberByUserIdAndStudyInfoId(user.getUserId(), studyInfoId)) {
            log.warn(">>>> {} : {} <<<<", user.getUserId(), ExceptionMessage.STUDY_ALREADY_MEMBER);
            throw new MemberException(ExceptionMessage.STUDY_ALREADY_MEMBER);
        }

        // 스터디 가입 신청후 이미 대기중인 멤버인지 확인
        if (studyMemberRepository.isWaitingStudyMemberByUserIdAndStudyInfoId(user.getUserId(), studyInfoId)) {
            log.warn(">>>> {} : {} <<<<", user.getUserId(), ExceptionMessage.STUDY_WAITING_MEMBER);
            throw new MemberException(ExceptionMessage.STUDY_WAITING_MEMBER);
        }

        // 강퇴되었던 멤버인지 확인
        if (studyMemberRepository.isResignedStudyMemberByUserIdAndStudyInfoId(user.getUserId(), studyInfoId)) {
            log.warn(">>>> {} : {} <<<<", user.getUserId(), ExceptionMessage.STUDY_RESIGNED_MEMBER);
            throw new MemberException(ExceptionMessage.STUDY_RESIGNED_MEMBER);
        }

        // 알림 여부
        boolean notifyLeader = false;

        // 탈퇴한 멤버인지 확인, 승인 거부된 유저인지 확인
        Optional<StudyMember> existingMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfoId, user.getUserId());
        if (existingMember.isPresent()) {
            if (existingMember.get().getStatus() == StudyMemberStatus.STUDY_WITHDRAWAL || existingMember.get().getStatus() == StudyMemberStatus.STUDY_REFUSED) {
                existingMember.get().updateStudyMemberStatus(StudyMemberStatus.STUDY_WAITING); // 상태변경 후 종료
                notifyLeader = true;  // 알림설정
            }

        } else {

            // '스터디 승인 대기중인 유저' 로 생성
            StudyMember studyMember = StudyMember.waitingStudyMember(studyInfoId, user.getUserId());
            studyMemberRepository.save(studyMember);
            notifyLeader = true;

        }

        if (notifyLeader) {

            /*
             해당 스터디장에게 알림 메서드 추가되어야함  -> 유저의 이름등등을 보여주기위해 파라미터로 user 객체 가져옴
         */
        }

    }

Service 작성 각 예외처리 추가+ 추후에 알림 메서드 구현되면 추가 예정

 


테스트

 

 

StudyInfoFixture.java

    // 비공개 스터디 생성 메서드
    public static StudyInfo createPrivateStudyInfo(Long userId, String joinCode) {
        return StudyInfo.builder()
                .userId(userId)
                .topic("토픽")
                .joinCode(joinCode)
                .status(STUDY_PRIVATE)
                .build();
    }

비공개 스터디를 생성하는 Fixture

 

 

StudyMemberFixture.java

   // 테스트용 승인 대기중인 스터디원 생성 메서드
    public static StudyMember createStudyMemberWaiting(Long userId, Long studyInfoId) {
        return StudyMember.builder()
                .userId(userId)
                .studyInfoId(studyInfoId)
                .role(StudyMemberRole.STUDY_MEMBER)
                .status(StudyMemberStatus.STUDY_WAITING)
                .build();
    }

    // 테스트용 승인 거부된 스터디원 생성 메서드
    public static StudyMember createStudyMemberRefused(Long userId, Long studyInfoId) {
        return StudyMember.builder()
                .userId(userId)
                .studyInfoId(studyInfoId)
                .role(StudyMemberRole.STUDY_MEMBER)
                .status(StudyMemberStatus.STUDY_REFUSED)
                .build();
    }

각각 테스트를 위한 멤버 생성 Fixture

 

 

StudyMemberControllerTest.java

    @Test
    public void 스터디_가입_신청_테스트() throws Exception {
        // given
        User savedUser = userRepository.save(generateAuthUser());

        Map<String, String> map = TokenUtil.createTokenMap(savedUser);
        String accessToken = jwtService.generateAccessToken(map, savedUser);
        String refreshToken = jwtService.generateRefreshToken(map, savedUser);

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(savedUser.getId());
        studyInfoRepository.save(studyInfo);

        when(authService.findUserInfo(any(User.class))).thenReturn(UserInfoResponse.of(savedUser));
        doNothing().when(studyMemberService).applyStudyMember(any(UserInfoResponse.class), any(Long.class), any(String.class));

        //when , then
        mockMvc.perform(post("/member/" + studyInfo.getId() + "/apply")
                        .contentType(MediaType.APPLICATION_JSON)
                        .header(AUTHORIZATION, createAuthorizationHeader(accessToken, refreshToken)))

                // then
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.res_code").value(200))
                .andExpect(jsonPath("$.res_obj").value("Apply StudyMember Success"));

    }

컨트롤러 테스트

 

 

StudyMemberServiceTest.java

    @Test
    @DisplayName("스터디 가입 신청 테스트")
    public void applyStudyMember() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(leader.getId());
        studyInfoRepository.save(studyInfo);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // when
        studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        Optional<StudyMember> waitMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), user1.getId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WAITING, waitMember.get().getStatus());

    }

    @Test
    @DisplayName("한번 강퇴된 스터디원 가입 신청 테스트")
    public void applyStudyMember_resigned() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(leader.getId());
        studyInfoRepository.save(studyInfo);

        StudyMember studyMember = StudyMemberFixture.createStudyMemberResigned(user1.getId(), studyInfo.getId()); // 강퇴 멤버 생성
        studyMemberRepository.save(studyMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // then
        MemberException em = assertThrows(MemberException.class, () -> {
            studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        });
        assertEquals(ExceptionMessage.STUDY_RESIGNED_MEMBER.getText(), em.getMessage());

    }

    @Test
    @DisplayName("이미 신청완료한 스터디에 가입 재신청 테스트")
    public void applyStudyMember_replay() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(leader.getId());
        studyInfoRepository.save(studyInfo);

        StudyMember studyMember = StudyMemberFixture.createStudyMemberWaiting(user1.getId(), studyInfo.getId()); // 승인 대기중 멤버 생성
        studyMemberRepository.save(studyMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // then
        MemberException em = assertThrows(MemberException.class, () -> {
            studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        });

        assertEquals(ExceptionMessage.STUDY_WAITING_MEMBER.getText(), em.getMessage());

    }

    @Test
    @DisplayName("비공개 스터디 가입 신청- 참여코드가 맞는 경우")
    public void applyStudyMember_privateStudy_joinCode_match() {
        // given
        String joinCode = "joinCode";

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createPrivateStudyInfo(leader.getId(), joinCode); // 비공개 스터디 생성
        studyInfoRepository.save(studyInfo);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // when
        studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), studyInfo.getJoinCode());
        Optional<StudyMember> waitMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), user1.getId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WAITING, waitMember.get().getStatus());
    }

    @Test
    @DisplayName("비공개 스터디 가입 신청- 참여코드가 null인 경우")
    public void applyStudyMember_privateStudy_joinCode_null() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createPrivateStudyInfo(leader.getId(), joinCode); // 비공개 스터디 생성
        studyInfoRepository.save(studyInfo);

        StudyMember studyMember = StudyMemberFixture.createStudyMemberWaiting(user1.getId(), studyInfo.getId()); // 승인 대기중 멤버 생성
        studyMemberRepository.save(studyMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // then
        MemberException em = assertThrows(MemberException.class, () -> {
            studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        });

        assertEquals(ExceptionMessage.STUDY_JOIN_CODE_FAIL.getText(), em.getMessage());
    }

    @Test
    @DisplayName("비공개 스터디 가입 신청- 참여코드가 10자 이상인 경우")
    public void applyStudyMember_privateStudy_joinCode_10() {
        // given
        String joinCode = "1234567891011";

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createPrivateStudyInfo(leader.getId(), joinCode); // 비공개 스터디 생성
        studyInfoRepository.save(studyInfo);

        StudyMember studyMember = StudyMemberFixture.createStudyMemberWaiting(user1.getId(), studyInfo.getId()); // 승인 대기중 멤버 생성
        studyMemberRepository.save(studyMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // then
        MemberException em = assertThrows(MemberException.class, () -> {
            studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        });

        assertEquals(ExceptionMessage.STUDY_JOIN_CODE_FAIL.getText(), em.getMessage());
    }

    @Test
    @DisplayName("이전에 탈퇴한 멤버가 가입 신청 테스트")
    public void applyStudyMember_withdrawal() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(leader.getId());
        studyInfoRepository.save(studyInfo);

        StudyMember withdrawalMember = StudyMemberFixture.createStudyMemberWithdrawal(user1.getId(), studyInfo.getId());
        studyMemberRepository.save(withdrawalMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // when
        studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        Optional<StudyMember> waitMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), user1.getId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WAITING, waitMember.get().getStatus());
    }

    @Test
    @DisplayName("이전에 승인 거부된 멤버가 가입 신청 테스트")
    public void applyStudyMember_refused() {
        // given
        String joinCode = null;

        User leader = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        userRepository.saveAll(List.of(leader, user1));

        StudyInfo studyInfo = StudyInfoFixture.createDefaultPublicStudyInfo(leader.getId());
        studyInfoRepository.save(studyInfo);

        StudyMember refusedMember = StudyMemberFixture.createStudyMemberRefused(user1.getId(), studyInfo.getId());
        studyMemberRepository.save(refusedMember);

        UserInfoResponse userInfo = authService.findUserInfo(user1);

        // when
        studyMemberService.applyStudyMember(userInfo, studyInfo.getId(), joinCode);
        Optional<StudyMember> waitMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), user1.getId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WAITING, waitMember.get().getStatus());
    }
  • 스터디 가입 신청 테스트
  • 이전에 강퇴된 유저 가입 신청 테스트
  • 이전에 탈퇴한 유저 가입 신청 테스트
  • 이전에 승인 거부된 유저 가입 신청 테스트
  • 이미 신청완료한 스터디에 가입 재신청 테스트
  • 비공개 스터디 가입신청 - 참여코드 일치 테스트
  • 비공개 스터디 가입신청 - 참여코드 null일 경우 테스트
  • 비공개 스터디 가입신청 - 참여코드 10자일 경우 테스트
  • 비공개 스터디 가입신청 - 참여코드 일치하지 않을때 테스트

를 나눠서 테스트 진행하였다.

댓글