💡 .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을 피할 수 있다. (존재할때만 가져오므로)
- Optional<>을 사용하면 API를 사용하는 개발자가 결과값이 없을 경우의 로직을 더 명확하게 처리할 수 있도록 강제한다. 예를 들어, Optional의 orElseThrow() 메서드를 사용하면, 특정 값이 없을 때 예외를 발생시킬 수 있다!!
- 또한 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();
}
구현체 커스텀 쿼리 로직 추가
@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); 을 통해 로그인 유저가 해당 스터디의 리더인지 검증
@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 추가
@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);
}
}
}
스터디장 검증 메서드 추가
@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 구현
테스트
public static User generateKaKaoUser() {
return User.builder()
.platformId("1")
.platformType(KAKAO)
.role(UNAUTH)
.name("이름")
.githubId("카카오아이디")
.profileImageUrl("프로필이미지")
.build();
}
유저 Fixture 설정
// 테스트용 비활동중인(탈퇴) 스터디원 생성 메서드
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 설정
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
@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 요청을 올바르게 처리하고 적절한 응답을 반환하는지 확인)
@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 확인)
'Project > 깃터디 (gitudy)' 카테고리의 다른 글
[깃터디/Todo] Todo 수정 api 구현 (0) | 2024.05.11 |
---|---|
[깃터디/Todo] Todo 조회 api 구현 (0) | 2024.05.11 |
[깃터디/Auth] 닉네임 중복검사 api 구현 (0) | 2024.05.10 |
[깃터디/Auth] 회원 정보 조회 구현 (0) | 2024.05.10 |
[깃터디/Auth] 로그아웃 요청 구현 (0) | 2024.05.10 |
댓글