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

[깃터디/Member] 스터디원 강퇴, 스터디원 탈퇴 api 구현

by J-rain 2024. 5. 11.

 

 

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

 

 회고 

        // 삭제할 StudyTodoMapping의 ID 조회
        List<Long> todoMappingIds = queryFactory
                .select(studyTodoMapping.id)
                .from(studyTodoMapping)
                .join(studyTodo).on(studyTodoMapping.todoId.eq(studyTodo.id))
                .where(studyTodo.studyInfoId.eq(studyInfoId)
                        .and(studyTodo.todoDate.after(LocalDate.now()))
                        .and(studyTodoMapping.userId.eq(userId)))
                .fetch();

구현 queryDsl의 일부인데 .join(studyTodo).on(studyTodoMapping.todoId.eq(studyTodo.id)) 에서

.join 테이블(또는 엔티티)에 대한 조인을하고 .on 절은 두 데이터 집합을 연결할 때 어떤 기준을 사용할 것인지 선택한다. StudyTodoMapping 테이블과 StudyTodo 테이블은 todo_id 필드로 연관 관계가 있기 때문에 이렇게 설정했다.

 

이전 Todo 기록들을 남겨두면서 현재 할당된 Todo를 어떻게 삭제 해야 할까 고민을 많이 해보았는데

.after() 메소드를 통해 (날짜 비교를 위한 함수) tododate(마감 시간)이 현재 시간 즉, 강퇴,탈퇴 시점보다 이후인 Todo만 선택하도록 구현하였다!

 

StudyTodoRepositoryImpl.java

    @Override
    public void deleteTodoIdsByStudyInfoIdAndUserId(Long studyInfoId, Long userId) {

        // 삭제할 StudyTodoMapping의 ID 조회
        List<Long> todoMappingIds = queryFactory
                .select(studyTodoMapping.id)
                .from(studyTodoMapping)
                .join(studyTodo).on(studyTodoMapping.todoId.eq(studyTodo.id))
                .where(studyTodo.studyInfoId.eq(studyInfoId)
                        .and(studyTodo.todoDate.after(LocalDate.now()))
                        .and(studyTodoMapping.userId.eq(userId)))
                .fetch();

        // 찾은 ID를 사용하여 StudyTodoMapping 삭제
        if (!todoMappingIds.isEmpty()) {
            queryFactory
                    .delete(studyTodoMapping)
                    .where(studyTodoMapping.id.in(todoMappingIds))
                    .execute();
        }
    }

원래는 스터디원 강퇴, 스터디원의 탈퇴를 상태만 변경해주려고 했지만 스코어점수나 투두 매핑에서 복잡할것 같아 → 강퇴나 탈퇴시 해당 멤버의 상태변경과 할당된 투두매핑까지 삭제해주는 것으로 로직을 변경 했다.

그렇다고해서 이전 Todo기록까지 지우는 것은 아님!

 

 

StudyMemberController.java

    // 스터디원 강퇴
    @ApiResponse(responseCode = "200", description = "스터디원 강퇴 성공")
    @PatchMapping("/{studyInfoId}/resign/{resignUserId}")
    public JsonResult<?> resignStudyMember(@AuthenticationPrincipal User user,
                                           @PathVariable(name = "studyInfoId") Long studyInfoId,
                                           @PathVariable(name = "resignUserId") Long resignUserId) {

        // 스터디장 검증
        studyMemberService.isValidateStudyLeader(user, studyInfoId);

        studyMemberService.resignStudyMember(studyInfoId, resignUserId);

        return JsonResult.successOf("Resign Member Success");
    }

    // 스터디 탈퇴
    @ApiResponse(responseCode = "200", description = "스터디 탈퇴 성공")
    @PatchMapping("/{studyInfoId}/withdrawal/{userId}")
    public JsonResult<?> withdrawalStudyMember(@AuthenticationPrincipal User user,
                                               @PathVariable(name = "studyInfoId") Long studyInfoId,
                                               @PathVariable(name = "userId") Long userId) {

        // 스터디멤버 검증
        studyMemberService.isValidateStudyMember(user, studyInfoId);

        studyMemberService.withdrawalStudyMember(studyInfoId, userId);

        return JsonResult.successOf("Withdrawal Member Success");
    }

StudyMemberController 구현 처음에는 @RequestParam 으로 resignUserId, userId 를 처리했지만 @PathVariable 로 로직 변경

 

 

StudyMemberService.java

    // 스터디원 강퇴 메서드
    @Transactional
    public void resignStudyMember(Long studyInfoId, Long resignUserId) {

        // 강퇴시킬 스터디원 조회
        StudyMember resignMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfoId, resignUserId).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", resignUserId, ExceptionMessage.USER_NOT_STUDY_MEMBER);
            return new MemberException(ExceptionMessage.USER_NOT_STUDY_MEMBER);
        });

        // 강퇴 스터디원 상태 업데이트
        resignMember.updateStudyMemberStatus(StudyMemberStatus.STUDY_RESIGNED);

        // 강퇴 스터디원에게 할당된 마감기한이 지나지 않은 To do 삭제
        studyTodoRepository.deleteTodoIdsByStudyInfoIdAndUserId(studyInfoId, resignUserId);
    }

    // 스터디원 탈퇴 메서드
    @Transactional
    public void withdrawalStudyMember(Long studyInfoId, Long userId) {

        // 탈퇴 스터디원 조회
        StudyMember withdrawalMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfoId, userId).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", userId, ExceptionMessage.USER_NOT_STUDY_MEMBER);
            return new MemberException(ExceptionMessage.USER_NOT_STUDY_MEMBER);
        });

        // 탈퇴 스터디원 상태 메서드
        withdrawalMember.updateStudyMemberStatus(StudyMemberStatus.STUDY_WITHDRAWAL);

        // 탈퇴 스터디원에게 할당된 마감기한이 지나지 않은 To do 삭제
        studyTodoRepository.deleteTodoIdsByStudyInfoIdAndUserId(studyInfoId, userId);

    }

서비스단에서는 스터디원강퇴, 스터디원이 탈퇴함에 따라 상태를 맞게 수정하고 할당된 todo를 제거한다.


테스트

 

StudyTodoFixture.java

    // 테스트용  완료된 studyTodoMapping 생성
    public static StudyTodoMapping createCompleteStudyTodoMapping(Long todoId, Long userId) {
        return StudyTodoMapping.builder()
                .todoId(todoId)
                .userId(userId)
                .status(StudyTodoStatus.TODO_COMPLETE)
                .build();
    }

    // 테스트용 날짜 to do 설정
    public static StudyTodo createDateStudyTodo(Long studyInfoId, LocalDate todoDate) {
        return StudyTodo.builder()
                .studyInfoId(studyInfoId)
                .title(expectedTitle)
                .todoDate(todoDate)
                .build();
    }

먼저 마감기한 이후, 이전 todo들이 삭제,유지 되는지 확인하기위해 TodoFixture 정의했다.

 

 

StudyMemberControllerTest.java

    @Test
    public void 스터디원_강퇴_테스트() throws Exception {
        // given

        User leader = userRepository.save(generateAuthUser());
        User member = userRepository.save(generateKaKaoUser());

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

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

        StudyMember studyMember = StudyMemberFixture.createDefaultStudyMember(member.getId(), studyInfo.getId());
        studyMemberRepository.save(studyMember);

        when(studyMemberService.isValidateStudyLeader(any(User.class), any(Long.class))).thenReturn(UserInfoResponse.of(leader));
        doNothing().when(studyMemberService).resignStudyMember(any(Long.class), any(Long.class));

        //when , then
        mockMvc.perform(patch("/member/" + studyInfo.getId() + "/resign/" + studyMember.getUserId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .header(AUTHORIZATION, createAuthorizationHeader(accessToken, refreshToken))
                        .param("resignUserId", String.valueOf(studyMember.getUserId())))

                // then
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.res_code").value(200))
                .andExpect(jsonPath("$.res_obj").value("Resign Member Success"));
    }

    @Test
    public void 스터디원_탈퇴_테스트() throws Exception {
        // given

        User leader = userRepository.save(generateAuthUser());
        User member = userRepository.save(generateKaKaoUser());

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

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

        StudyMember studyMember = StudyMemberFixture.createDefaultStudyMember(member.getId(), studyInfo.getId());
        studyMemberRepository.save(studyMember);

        when(studyMemberService.isValidateStudyMember(any(User.class), any(Long.class))).thenReturn(UserInfoResponse.of(member));
        doNothing().when(studyMemberService).resignStudyMember(any(Long.class), any(Long.class));

        //when , then
        mockMvc.perform(patch("/member/" + studyInfo.getId() + "/withdrawal/" + studyMember.getUserId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .header(AUTHORIZATION, createAuthorizationHeader(accessToken, refreshToken))
                        .param("userId", String.valueOf(studyMember.getUserId())))

                // then
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.res_code").value(200))
                .andExpect(jsonPath("$.res_obj").value("Withdrawal Member Success"));
    }

스터디원 강퇴, 스터디원 탈퇴 컨트롤러 테스트이다. 동작 검증 ( HTTP 요청을 올바르게 처리하고 적절한 응답을 반환하는지 확인)

 

StudyMemberServiceTest.java

    @Test
    @DisplayName("스터디원 강퇴 테스트")
    public void resignStudyMember() {
        // given

        User leaderuser = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        User user2 = UserFixture.generateKaKaoUser();

        userRepository.saveAll(List.of(leaderuser, user1, user2));

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

        StudyMember leader = StudyMemberFixture.createStudyMemberLeader(leaderuser.getId(), studyInfo.getId());
        StudyMember activeMember1 = StudyMemberFixture.createDefaultStudyMember(user1.getId(), studyInfo.getId());
        StudyMember activeMember2 = StudyMemberFixture.createStudyMemberResigned(user2.getId(), studyInfo.getId());
        studyMemberRepository.saveAll(List.of(leader, activeMember1, activeMember2));

        // when
        studyMemberService.resignStudyMember(studyInfo.getId(), activeMember1.getUserId());
        Optional<StudyMember> studyMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), activeMember1.getUserId());

        // then
        assertEquals(StudyMemberStatus.STUDY_RESIGNED, studyMember.get().getStatus());

    }

    @Test
    @DisplayName("스터디원 탈퇴 테스트")
    public void withdrawalMember() {
        // given

        User leaderuser = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();
        User user2 = UserFixture.generateKaKaoUser();

        userRepository.saveAll(List.of(leaderuser, user1, user2));

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

        StudyMember leader = StudyMemberFixture.createStudyMemberLeader(leaderuser.getId(), studyInfo.getId());
        StudyMember activeMember1 = StudyMemberFixture.createDefaultStudyMember(user1.getId(), studyInfo.getId());
        studyMemberRepository.saveAll(List.of(leader, activeMember1));

        // when
        studyMemberService.withdrawalStudyMember(studyInfo.getId(), activeMember1.getUserId());
        Optional<StudyMember> studyMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), activeMember1.getUserId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WITHDRAWAL, studyMember.get().getStatus());
    }

서비스 테스트는 두가지 경우로 나누었다. 먼저 기본적인 강퇴, 탈퇴 테스트이다.

 

    @Test
    @DisplayName("스터디원 강퇴 테스트 - Todo mappings 함께 삭제 테스트")
    public void resignStudyMember_todo() {
        // given

        User leaderuser = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();

        userRepository.saveAll(List.of(leaderuser, user1));

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

        StudyMember leader = StudyMemberFixture.createStudyMemberLeader(leaderuser.getId(), studyInfo.getId());
        StudyMember activeMember = StudyMemberFixture.createDefaultStudyMember(user1.getId(), studyInfo.getId());
        studyMemberRepository.saveAll(List.of(leader, activeMember));

        // 현재 날짜 이후로 설정된 To do (마감기한 지나지 않은 To do)
        StudyTodo futureTodo1 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().plusDays(3));
        StudyTodo futureTodo2 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().plusDays(5));
        // 현재 날짜 이전으로 설정된 To do (마감기한 지난 To do)
        StudyTodo pastTodo1 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().minusDays(3));
        studyTodoRepository.saveAll(List.of(futureTodo1, futureTodo2, pastTodo1));

        // activeMember에게 할당된 to do: 2개는 미래, 1개는 과거
        StudyTodoMapping mappingFuture1 = StudyTodoFixture.createStudyTodoMapping(futureTodo1.getId(), activeMember.getUserId());
        StudyTodoMapping mappingFuture2 = StudyTodoFixture.createStudyTodoMapping(futureTodo2.getId(), activeMember.getUserId());
        StudyTodoMapping mappingPast1 = StudyTodoFixture.createCompleteStudyTodoMapping(pastTodo1.getId(), activeMember.getUserId());
        studyTodoMappingRepository.saveAll(List.of(mappingFuture1, mappingFuture2, mappingPast1));

        // when
        studyMemberService.resignStudyMember(studyInfo.getId(), activeMember.getUserId());
        Optional<StudyMember> resignedMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), activeMember.getUserId());
        List<StudyTodoMapping> todoMappings = studyTodoMappingRepository.findByUserId(activeMember.getUserId());

        // then
        assertEquals(StudyMemberStatus.STUDY_RESIGNED, resignedMember.get().getStatus());

        // 마감 기한이 지난 To do는 삭제x
        assertTrue(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingPast1.getId())));

        // 마감 기한이 지나지 않은 To do 삭제되어야 함
        assertFalse(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingFuture1.getId())));
        assertFalse(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingFuture2.getId())));
    }

    @Test
    @DisplayName("스터디원 탈퇴 테스트 - Todo mappings 함께 삭제 테스트")
    public void withdrawalStudyMember_todo() {
        // given

        User leaderuser = UserFixture.generateAuthUser();
        User user1 = UserFixture.generateGoogleUser();

        userRepository.saveAll(List.of(leaderuser, user1));

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

        StudyMember leader = StudyMemberFixture.createStudyMemberLeader(leaderuser.getId(), studyInfo.getId());
        StudyMember activeMember = StudyMemberFixture.createDefaultStudyMember(user1.getId(), studyInfo.getId());
        studyMemberRepository.saveAll(List.of(leader, activeMember));

        // 현재 날짜 이후로 설정된 To do (마감기한 지나지 않은 To do)
        StudyTodo futureTodo1 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().plusDays(3));
        StudyTodo futureTodo2 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().plusDays(5));
        // 현재 날짜 이전으로 설정된 To do (마감기한 지난 To do)
        StudyTodo pastTodo1 = StudyTodoFixture.createDateStudyTodo(studyInfo.getId(), LocalDate.now().minusDays(3));
        studyTodoRepository.saveAll(List.of(futureTodo1, futureTodo2, pastTodo1));

        // activeMember에게 할당된 to do: 2개는 미래, 1개는 과거
        StudyTodoMapping mappingFuture1 = StudyTodoFixture.createStudyTodoMapping(futureTodo1.getId(), activeMember.getUserId());
        StudyTodoMapping mappingFuture2 = StudyTodoFixture.createStudyTodoMapping(futureTodo2.getId(), activeMember.getUserId());
        StudyTodoMapping mappingPast1 = StudyTodoFixture.createCompleteStudyTodoMapping(pastTodo1.getId(), activeMember.getUserId());
        studyTodoMappingRepository.saveAll(List.of(mappingFuture1, mappingFuture2, mappingPast1));

        // when
        studyMemberService.withdrawalStudyMember(studyInfo.getId(), activeMember.getUserId());
        Optional<StudyMember> withdrawalMember = studyMemberRepository.findByStudyInfoIdAndUserId(studyInfo.getId(), activeMember.getUserId());
        List<StudyTodoMapping> todoMappings = studyTodoMappingRepository.findByUserId(activeMember.getUserId());

        // then
        assertEquals(StudyMemberStatus.STUDY_WITHDRAWAL, withdrawalMember.get().getStatus());

        // 마감 기한이 지난 To do는 삭제x
        assertTrue(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingPast1.getId())));

        // 마감 기한이 지나지 않은 To do 삭제되어야 함
        assertFalse(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingFuture1.getId())));
        assertFalse(todoMappings.stream()
                .anyMatch(mapping -> mapping.getId().equals(mappingFuture2.getId())));

    }

이번에는 Todo를 마감기한 지난것과 지나지 않은것을 저장한 후 유저가 강퇴,탈퇴하면서 할당된 Todo가(마감기한 지나지 않은) 삭제되는지도 테스트한다.

댓글