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

[깃터디/Todo] Todo 등록 api 구현

by J-rain 2024. 5. 11.

 

 

 

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

 회고 

@Repository
public interface StudyTodoRepository extends JpaRepository<StudyTodo, Long> , StudyTodoRepositoryCustom{
    Optional<StudyTodo> findByIdAndStudyInfoId(Long todoId, Long studyInfoId);
}

JpaRepository 인터페이스는 Spring Data JPA의 일부로, CRUD(Create, Read, Update, Delete) 작업을 위한 메서드를 제공해서 save(), findById() 등등을 쓸수있는것!!

Optional<StudyTodo> findByIdAndStudyInfoId(Long todoId, Long studyInfoId); 특정 todoId와 studyInfoId에 해당하는 StudyTodo 객체를 찾아서 반환하도록 내가 작성했다. 여기서 의문점은 왜 StudyTodo 로 받지않고 Optional<> 을 사용해서 받을까??

→ 1. NullPointerException 방지: Optional<>을 사용하면, null 값으로 인해 발생할 수 있는 NullPointerException을 피할 수 있다. (존재할때만 가져오므로)

  1. Optional<>을 사용하면 API를 사용하는 개발자가 결과값이 없을 경우의 로직을 더 명확하게 처리할 수 있도록 강제한다. 예를 들어, Optional의 orElseThrow() 메서드를 사용하면, 특정 값이 없을 때 예외를 발생시킬 수 있다!!
  2. 또한 map(), flatMap(), filter() 같은 메서드를 제공하여, 함수형 프로그래밍 접근 방식을 지원한다 ✨

즉, 조회 결과가 없는 경우를 안전하게 처리하기 위한 것~~

 

 

StudyMemberRepositoryCustom.java

 // StudyInfoId를 통해 스터디의 모든 멤버들 중에 활동중인 멤버를 조회한다.
    public List<StudyMember> findActiveMembersByStudyInfoId(Long studyInfoId);

커스텀 레포지토리(Spring Data JPA와 같은 데이터 접근 프레임워크에서 제공하지 않는 특정한 쿼리나 로직을 구현하기 위해 사용자가 직접 정의함) 추가

 

StudyMemberRepositoryImpl.java

 @Override
    public List<StudyMember> findActiveMembersByStudyInfoId(Long studyInfoId) {
        return queryFactory
                .selectFrom(studyMember)
                .where(studyMember.studyInfoId.eq(studyInfoId)
                        .and(studyMember.status.eq(StudyMemberStatus.STUDY_ACTIVE)))
                .fetch();
    }

구현체 커스텀 쿼리 로직 추가

 

StudyTodoController.java

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/study")
public class StudyTodoController {

    private final StudyTodoService studyTodoService;
    private final StudyMemberService studyMemberService;

    // Todo 등록
    @ApiResponse(responseCode = "200", description = "Todo 등록 성공")
    @PostMapping("/{studyInfoId}/todo")
    public JsonResult<?> registerStudyTodo(@AuthenticationPrincipal User user,
                                           @PathVariable("studyInfoId") Long studyInfoId,
                                           @Valid @RequestBody StudyTodoRequest studyTodoRequest) {
				
				// Todo는 리더만 설정 가능
        studyMemberService.isValidateStudyLeader(user, studyInfoId);

        studyTodoService.registerStudyTodo(studyTodoRequest, studyInfoId);

        return JsonResult.successOf("Todo register Success");
    }
}

Todo 등록 Controller

studyMemberService.isValidateStudyLeader(user, studyInfoId); 을 통해 로그인 유저가 해당 스터디의 리더인지 검증

 

 

StudyTodoRequest.java

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

    @Size(max = 20, message = "제목 20자 이내")
    private String title;                      // To do 제목

    @Size(max = 50, message = "설명 50자 이내")
    private String detail;                     // To do 내용

    private String todoLink;                   // To do 링크

    private LocalDate todoDate;                // To do 기한

}

유저가 작성할 DTO 추가

 

 

StudyMemberService.java

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class StudyMemberService {

    private final UserRepository userRepository;
    private final StudyMemberRepository studyMemberRepository;

    // 스터디장 검증 메서드
    public void isValidateStudyLeader(User userPrincipal, Long studyInfoId) {

        // platformId와 platformType을 이용하여 User 객체 조회
        User user = userRepository.findByPlatformIdAndPlatformType(userPrincipal.getPlatformId(), userPrincipal.getPlatformType()).orElseThrow(() -> {
            log.warn(">>>> {},{} : {} <<<<", userPrincipal.getPlatformId(), userPrincipal.getPlatformType(), ExceptionMessage.USER_NOT_FOUND);
            return new UserException(ExceptionMessage.USER_NOT_FOUND);
        });

        // 스터디장인지 확인
        if (!studyMemberRepository.isStudyLeaderByUserIdAndStudyInfoId(user.getId(), studyInfoId)) {
            throw new MemberException(ExceptionMessage.STUDY_MEMBER_NOT_LEADER);
        }

    }

}

스터디장 검증 메서드 추가

 

StudyTodoService.java

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class StudyTodoService {

    private final StudyTodoRepository studyTodoRepository;
    private final StudyTodoMappingRepository studyTodoMappingRepository;
    private final StudyMemberRepository studyMemberRepository;

    // Todo 등록
    @Transactional
    public void registerStudyTodo(StudyTodoRequest studyTodoRequest, Long studyInfoId) {

        // 스터디에 속한 활동중인 스터디원 조회
        List<StudyMember> studyActiveMembers = studyMemberRepository.findActiveMembersByStudyInfoId(studyInfoId);

        StudyTodo studyTodo = createStudyTodo(studyTodoRequest, studyInfoId);
        studyTodoRepository.save(studyTodo);

        // 활동중인 스터디원에게만 TO DO 할당
        List<StudyTodoMapping> todoMappings = studyActiveMembers.stream()
                .map(activeMember -> StudyTodoMapping.builder()
                        .userId(activeMember.getUserId())
                        .todoId(studyTodo.getId())
                        .status(StudyTodoStatus.TODO_INCOMPLETE) // 기본 상태
                        .build())
                .collect(Collectors.toList());

        // 한 번의 쿼리로 모든 매핑 저장
        studyTodoMappingRepository.saveAll(todoMappings);

    }

    // StudyTodo 생성 로직
    private StudyTodo createStudyTodo(StudyTodoRequest studyTodoRequest, Long studyInfoId) {
        return StudyTodo.builder()
                .studyInfoId(studyInfoId)
                .title(studyTodoRequest.getTitle())
                .detail(studyTodoRequest.getDetail())
                .todoLink(studyTodoRequest.getTodoLink())
                .todoDate(studyTodoRequest.getTodoDate())
                .build();
    }
}

Todo 등록 Service 구현

 


테스트

 

UserFixture.java

public static User generateKaKaoUser() {
        return User.builder()
                .platformId("1")
                .platformType(KAKAO)
                .role(UNAUTH)
                .name("이름")
                .githubId("카카오아이디")
                .profileImageUrl("프로필이미지")
                .build();
    }

유저 Fixture 설정

 

StudyMemberFixture.java

  // 테스트용 비활동중인(탈퇴) 스터디원 생성 메서드
    public static StudyMember createStudyMemberWithdrawal(Long userId, Long studyInfoId) {
        return StudyMember.builder()
                .userId(userId)
                .studyInfoId(studyInfoId)
                .role(StudyMemberRole.STUDY_MEMBER)
                .status(StudyMemberStatus.STUDY_WITHDRAWAL)
                .build();
    }

스터디 멤버 Fixture 설정

 

 

StudyTodoFixture.java

public class StudyTodoFixture {

    //테스트용 studyTodo 생성
    public static StudyTodo createStudyTodo(Long studyInfoId) {
        return StudyTodo.builder()
                .studyInfoId(studyInfoId)
                .title(expectedTitle)
                .detail(expectedDetail)
                .todoLink(expectedTodoLink)
                .todoDate(expectedTodoDate)
                .build();
    }

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

    // 테스트용 To do 등록
    public static StudyTodoRequest generateStudyTodoRequest() {
        return StudyTodoRequest.builder()
                .title(expectedTitle)
                .detail(expectedDetail)
                .todoLink(expectedTodoLink)
                .todoDate(expectedTodoDate)
                .build();
    }

}

테스트용 Todo 설정 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);

        StudyMemberFixture.createStudyMemberLeader(savedUser.getId(), studyInfo.getId());

        StudyTodoRequest studyTodoRequest = StudyTodoFixture.generateStudyTodoRequest();

        doNothing().when(studyMemberService).isValidateStudyLeader(any(User.class), any(Long.class));
        doNothing().when(studyTodoService).registerStudyTodo(any(StudyTodoRequest.class), any(Long.class));

        //when , then
        mockMvc.perform(post("/study/" + studyInfo.getId() + "/todo")
                        .contentType(MediaType.APPLICATION_JSON)
                        .header(AUTHORIZATION, createAuthorizationHeader(accessToken, refreshToken))
                        .content(objectMapper.writeValueAsString(studyTodoRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.res_code").value(200))
                .andExpect(jsonPath("$.res_msg").value("OK"))
                .andExpect(jsonPath("$.res_obj").value("Todo register Success"))
                .andDo(print());

    }

컨트롤러 동작 검증 ( HTTP 요청을 올바르게 처리하고 적절한 응답을 반환하는지 확인)

 

 

StudyTodoServiceTest.java

  @Test
    @DisplayName("Todo 등록 테스트")
    public void registerTodo() {
        //given
        User leader = userRepository.save(generateAuthUser());
        User activeMember = userRepository.save(generateGoogleUser());
        User withdrawalMember = userRepository.save(generateKaKaoUser());

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

        studyMemberRepository.saveAll(List.of(
                StudyMemberFixture.createStudyMemberLeader(leader.getId(), studyInfo.getId()),
                StudyMemberFixture.createDefaultStudyMember(activeMember.getId(), studyInfo.getId()),
                StudyMemberFixture.createStudyMemberWithdrawal(withdrawalMember.getId(), studyInfo.getId())
        ));

        StudyTodoRequest request = StudyTodoFixture.generateStudyTodoRequest();

        //when
        studyMemberService.isValidateStudyLeader(leader, studyInfo.getId());
        studyTodoService.registerStudyTodo(request, studyInfo.getId());

        //then
        // StudyTodo
        List<StudyTodo> studyTodos = studyTodoRepository.findAll();
        assertNotNull(studyTodos);
        StudyTodo savedStudyTodo = studyTodos.get(0);
        assertEquals(studyInfo.getId(), savedStudyTodo.getStudyInfoId());

        List<StudyTodoMapping> mappings = studyTodoMappingRepository.findAll();
        // 활동중인 멤버에게 할당되었는지 확인
        assertTrue(mappings.stream()
                .anyMatch(mappingMember -> mappingMember.getUserId().equals(activeMember.getId())));
        // 비활동중인 멤버에게 할당되지 않았는지 확인
        assertFalse(mappings.stream()
                .anyMatch(mappingMember -> mappingMember.getUserId().equals(withdrawalMember.getId())));

    }

Todo 등록 ServiceTest (활동중인 멤버에게 할당 확인, 비활동중인 멤버에게 할당x 확인)

댓글