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

[깃터디/Todo] 스터디원의 Todo 완료여부 조회 api 구현

by J-rain 2024. 5. 11.

 

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

 

 

스터디원의 Todo 완료여부를 조회하려면 일단 해당 스터디의 스터디원들을 조회해야하고 각각의 팀원들이 Todo에 대해 완료했는지 확인해볼 수 있어야했다.

StudyTodoMapping 테이블과 StudyMember 테이블은 직접적인 연관관계 설정이 되어있지 않았다.

그럼에도 조회가 가능한 방법이있다 🙀 ✨

엔티티 간의 직접적인 연관관계가 없다고 해도 조회가 가능한 간접적 연관관계 ORM(Object-Relational Mapping)의 개념을 통해서 구현했다.

        // 스터디 active 멤버들 찾기
        List<StudyMember> activeMembers = studyMemberRepository.findActiveMembersByStudyInfoId(studyInfoId);

        // active 멤버들의 userId 추출
        List<Long> userIds = extractUserIds(activeMembers);

        // active 멤버들에 대한 특정 Todo의 완료 상태를 조회
        List<StudyTodoMapping> todoMappings = studyTodoMappingRepository.findByTodoIdAndUserIds(todoId, userIds);

        // 조회된 정보를 바탕으로 응답 객체를 생성
        return todoMappings.stream()
                .map(mapping -> new StudyTodoStatusResponse(mapping.getUserId(), mapping.getStatus()))
                .collect(Collectors.toList());

두 테이블 간에 직접적인 ORM 매핑이 정의되어 있지 않더라도 공통 필드(userId)를 사용하여 두 도메인 객체의 관련 데이터를 연결하고, 이를 기반으로 비즈니스 로직을 수행할 수 있다!!!

 

 

StudyTodoMappingRepositoryCustom.java

public interface StudyTodoMappingRepositoryCustom {

    // 스터디 멤버들의 userId와 todoId로 해당 StudyTodoMapping 객체를 조회한다.
    List<StudyTodoMapping> findByTodoIdAndUserIds(Long todoId, List<Long> userIds);
}

 

 

StudyTodoMappingRepositoryImpl.java

 

    @Override
    public List<StudyTodoMapping> findByTodoIdAndUserIds(Long todoId, List<Long> userIds) {

        return queryFactory.selectFrom(studyTodoMapping)
                .where(studyTodoMapping.todoId.eq(todoId)
                        .and(studyTodoMapping.userId.in(userIds)))
                .fetch();
    }

스터디 멤버들의 userId와 todoId로 해당 StudyTodoMapping 객체를 조회하도록 작성한 QueryDsl 이다.

 

 

StudyTodoController.java

    // 스터디원들의 Todo 완료여부 조회
    @ApiResponse(responseCode = "200", description = "Todo 완료조회 성공", content = @Content(schema = @Schema(implementation = StudyTodoStatusResponse.class)))
    @GetMapping("/{studyInfoId}/todo/{todoId}")
    public JsonResult<?> readStudyTodoStatus(@AuthenticationPrincipal User user,
                                             @PathVariable(name = "studyInfoId") Long studyInfoId,
                                             @PathVariable(name = "todoId") Long todoId) {

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

        return JsonResult.successOf(studyTodoService.readStudyTodoStatus(studyInfoId, todoId));
    }

Todo 완료 여부 조회를 위한 studyInfoId와 해당 todoId를 @PathVariable 어노테이션을 통해 받고있다.

 

 

StudyTodoStatusResponse.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class StudyTodoStatusResponse {

    private Long userId;  // 스터디멤버 Id

    private StudyTodoStatus status;  // to do 진행 상황

}

조회를 위한 DTO response 부분이다.

 

 

StudyTodoService.java

    // 스터디원들의 Todo 완료여부 조회
    public List<StudyTodoStatusResponse> readStudyTodoStatus(Long studyInfoId, Long todoId) {

        // 스터디와 관련된 To do 예외처리
        studyTodoRepository.findByIdAndStudyInfoId(todoId, studyInfoId).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", todoId, ExceptionMessage.TODO_NOT_FOUND);
            return new TodoException(ExceptionMessage.TODO_NOT_FOUND);
        });

        // 스터디 active 멤버들 찾기
        List<StudyMember> activeMembers = studyMemberRepository.findActiveMembersByStudyInfoId(studyInfoId);

        // active 멤버들의 userId 추출
        List<Long> userIds = extractUserIds(activeMembers);

        // active 멤버들에 대한 특정 Todo의 완료 상태를 조회
        List<StudyTodoMapping> todoMappings = studyTodoMappingRepository.findByTodoIdAndUserIds(todoId, userIds);

        // 조회된 정보를 바탕으로 응답 객체를 생성
        return todoMappings.stream()
                .map(mapping -> new StudyTodoStatusResponse(mapping.getUserId(), mapping.getStatus()))
                .collect(Collectors.toList());
    }
    
    // 멤버들의 userId만 추출
    private List<Long> extractUserIds(List<StudyMember> activeMembers) {
        return activeMembers.stream()
                .map(StudyMember::getUserId)
                .toList();
    }

우선 스터디가 존재하는지 예외처리를 해주고 findActiveMembersByStudyInfoId 메서드를 통해 해당 스터디의 활동중인 멤버만 따로 찾아주고 있다.

 


테스트

 

StudyTodoFixture.java

    // 테스트용 studyTodoMapping 생성
    public static StudyTodoMapping createStudyTodoDefaultMapping(Long userId) {
        return StudyTodoMapping.builder()
                .userId(userId)
                .status(expectedStatus)
                .build();
    }

테스트를 위한 studyTodoMapping 생성 Fixture이다.

 

 

StudyTodoControllerTest.java

    @Test
    public void Todo_스터디원들의_완료여부_조회() 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);

        List<StudyTodoStatusResponse> response = Arrays.asList(
                StudyTodoStatusResponse.builder()
                        .userId(1L)
                        .status(StudyTodoStatus.TODO_COMPLETE)
                        .build(),
                StudyTodoStatusResponse.builder()
                        .userId(2L)
                        .status(StudyTodoStatus.TODO_INCOMPLETE)
                        .build()
        );

        when(studyMemberService.isValidateStudyMember(any(User.class), any(Long.class)))
                .thenReturn(UserInfoResponse.of(savedUser));
        when(studyTodoService.readStudyTodoStatus(any(Long.class), any(Long.class))).thenReturn(response);

        // when
        mockMvc.perform(get("/study/" + studyInfo.getId() + "/todo/" + 1L)
                        .contentType(MediaType.APPLICATION_JSON)
                        .header(AUTHORIZATION, createAuthorizationHeader(accessToken, refreshToken)))

                // then
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.res_code").value(200))
                .andExpect(jsonPath("$.res_msg").value("OK"))
                .andExpect(jsonPath("$.res_obj").isNotEmpty())
                .andDo(print());
    }

완료 여부 조회 컨트롤러 동작 테스트

 

 

StudyTodoServiceTest.java

    @Test
    @DisplayName("스터디원들의 특정 Todo에 대한 완료여부 조회 테스트")
    void readStudyTodo_status() {
        // given
        User leader = userRepository.save(generateAuthUser());
        User member1 = userRepository.save(generateKaKaoUser());
        User member2 = userRepository.save(generateGoogleUser());

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

        // 스터디장 To do 생성
        StudyTodo studyTodo = StudyTodoFixture.createStudyTodo(studyInfo.getId());
        studyTodoRepository.save(studyTodo);

        StudyMember koo = StudyMemberFixture.createDefaultStudyMember(member1.getId(), studyInfo.getId());
        StudyMember Lee = StudyMemberFixture.createDefaultStudyMember(member2.getId(), studyInfo.getId());
        studyMemberRepository.saveAll(List.of(koo, Lee));

        StudyTodoMapping studyTodoMapping1 = StudyTodoFixture.createStudyTodoMapping(studyTodo.getId(), koo.getUserId());
        StudyTodoMapping studyTodoMapping2 = StudyTodoFixture.createCompleteStudyTodoMapping(studyTodo.getId(), Lee.getUserId());
        studyTodoMappingRepository.saveAll(List.of(studyTodoMapping1, studyTodoMapping2));

        // when
        List<StudyTodoStatusResponse> results = studyTodoService.readStudyTodoStatus(studyInfo.getId(), studyTodo.getId());

        // then
        assertEquals(2, results.size());
        assertTrue(results.stream().anyMatch(r -> r.getUserId().equals(koo.getUserId()) && r.getStatus() == StudyTodoStatus.TODO_INCOMPLETE));
        assertTrue(results.stream().anyMatch(r -> r.getUserId().equals(Lee.getUserId()) && r.getStatus() == StudyTodoStatus.TODO_COMPLETE));

    }

활동중인 멤버의 userId가 잘 추출되는지 + 추출된 멤버들의 todo 완료 여부 상태를 조회가능한지 테스트한다.

댓글