Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.moru.server.domain.onboarding.controller;

import com.moru.server.domain.onboarding.dto.OnboardingRequestDTO;
import com.moru.server.domain.onboarding.dto.OnboardingResponseDTO;
import com.moru.server.domain.onboarding.service.command.OnboardingCommandService;
import com.moru.server.domain.onboarding.service.query.OnboardingQueryService;
import com.moru.server.domain.routine.dto.RoutineGroupResponseDTO;
import com.moru.server.domain.routine.entity.enums.RoutineGoalType;
Expand All @@ -9,9 +11,12 @@
import com.moru.server.global.security.auth.AuthenticatedMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -25,8 +30,22 @@
public class OnboardingController {

private final OnboardingQueryService onboardingQueryService;
private final OnboardingCommandService onboardingCommandService;
private final RoutineGroupQueryService routineGroupQueryService;

@Operation(
summary = "온보딩 완료",
description = "로그인 후 서버에 동기화된 본인 소유 루틴 그룹을 기준으로 온보딩을 완료합니다."
)
@PostMapping("/complete")
public ApiResponse<OnboardingResponseDTO.StatusResponse> completeOnboarding(
@AuthenticationPrincipal AuthenticatedMember member,
@Valid @RequestBody OnboardingRequestDTO.CompleteRequest request
) {
return ApiResponse.onSuccess(
onboardingCommandService.completeOnboarding(member.memberId(), request));
}

@Operation(summary = "온보딩 완료 여부 조회", description = "현재 로그인한 사용자의 온보딩 완료 여부를 조회합니다.")
@GetMapping("/status")
public ApiResponse<OnboardingResponseDTO.StatusResponse> getOnboardingStatus(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moru.server.domain.onboarding.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record OnboardingRequestDTO() {

@Schema(description = "온보딩 완료 요청")
public record CompleteRequest(
@NotNull(message = "routineGroupId는 필수입니다.")
@Schema(description = "동기화가 완료된 회원 소유 루틴 그룹 ID", example = "15")
Long routineGroupId
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

public record OnboardingResponseDTO() {

@Schema(description = "온보딩 완료 여부 조회 응답")
@Schema(description = "온보딩 완료 상태 응답")
public record StatusResponse(
@Schema(description = "온보딩 완료 여부", example = "false")
Boolean onboardingCompleted
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.moru.server.domain.onboarding.service.command;

import com.moru.server.domain.onboarding.dto.OnboardingRequestDTO;
import com.moru.server.domain.onboarding.dto.OnboardingResponseDTO;

public interface OnboardingCommandService {

OnboardingResponseDTO.StatusResponse completeOnboarding(
Long memberId,
OnboardingRequestDTO.CompleteRequest request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.moru.server.domain.onboarding.service.command;

import com.moru.server.domain.member.entity.Member;
import com.moru.server.domain.member.repository.MemberRepository;
import com.moru.server.domain.onboarding.dto.OnboardingRequestDTO;
import com.moru.server.domain.onboarding.dto.OnboardingResponseDTO;
import com.moru.server.domain.routine.repository.RoutineGroupRepository;
import com.moru.server.global.exception.BusinessException;
import com.moru.server.global.response.code.status.ErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class OnboardingCommandServiceImpl implements OnboardingCommandService {

private final MemberRepository memberRepository;
private final RoutineGroupRepository routineGroupRepository;

@Override
public OnboardingResponseDTO.StatusResponse completeOnboarding(
Long memberId,
OnboardingRequestDTO.CompleteRequest request
) {
Member member = memberRepository.findByIdForUpdate(memberId)
.orElseThrow(() -> new BusinessException(ErrorStatus.MEMBER_NOT_FOUND));

boolean ownsRoutineGroup = routineGroupRepository
.existsByIdAndMember_Id(request.routineGroupId(), memberId);
if (!ownsRoutineGroup) {
throw new BusinessException(ErrorStatus.ROUTINE_GROUP_NOT_FOUND);
}

if (Boolean.TRUE.equals(member.getOnboardingCompleted())) {
return new OnboardingResponseDTO.StatusResponse(true);
}

member.completeOnboarding();
return new OnboardingResponseDTO.StatusResponse(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

public interface RoutineGroupRepository extends JpaRepository<RoutineGroup, Long> {

boolean existsByIdAndMember_Id(Long routineGroupId, Long memberId);

@Query("""
select distinct rg from RoutineGroup rg
left join fetch rg.routines
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.moru.server.domain.onboarding.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.moru.server.domain.member.entity.enums.Role;
import com.moru.server.domain.onboarding.dto.OnboardingRequestDTO;
import com.moru.server.domain.onboarding.dto.OnboardingResponseDTO;
import com.moru.server.domain.onboarding.service.command.OnboardingCommandService;
import com.moru.server.domain.onboarding.service.query.OnboardingQueryService;
import com.moru.server.domain.routine.service.query.RoutineGroup.RoutineGroupQueryService;
import com.moru.server.global.response.ApiResponse;
import com.moru.server.global.security.auth.AuthenticatedMember;

@ExtendWith(MockitoExtension.class)
class OnboardingControllerTest {

@Mock
private OnboardingQueryService onboardingQueryService;

@Mock
private OnboardingCommandService onboardingCommandService;

@Mock
private RoutineGroupQueryService routineGroupQueryService;

@InjectMocks
private OnboardingController onboardingController;

@Test
void delegatesOnboardingCompletionToCommandService() {
AuthenticatedMember member = new AuthenticatedMember(1L, Role.MEMBER);
OnboardingRequestDTO.CompleteRequest request =
new OnboardingRequestDTO.CompleteRequest(15L);
OnboardingResponseDTO.StatusResponse serviceResponse =
new OnboardingResponseDTO.StatusResponse(true);
when(onboardingCommandService.completeOnboarding(1L, request))
.thenReturn(serviceResponse);

ApiResponse<OnboardingResponseDTO.StatusResponse> response =
onboardingController.completeOnboarding(member, request);

assertThat(response.getIsSuccess()).isTrue();
assertThat(response.getResult().onboardingCompleted()).isTrue();
verify(onboardingCommandService).completeOnboarding(1L, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.moru.server.domain.onboarding.service.command;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.moru.server.domain.member.entity.Member;
import com.moru.server.domain.member.entity.enums.LoginType;
import com.moru.server.domain.member.entity.enums.Role;
import com.moru.server.domain.member.repository.MemberRepository;
import com.moru.server.domain.onboarding.dto.OnboardingRequestDTO;
import com.moru.server.domain.onboarding.dto.OnboardingResponseDTO;
import com.moru.server.domain.routine.repository.RoutineGroupRepository;
import com.moru.server.global.exception.BusinessException;
import com.moru.server.global.response.code.status.ErrorStatus;

@ExtendWith(MockitoExtension.class)
class OnboardingCommandServiceImplTest {

private static final Long MEMBER_ID = 1L;
private static final Long ROUTINE_GROUP_ID = 15L;

@Mock
private MemberRepository memberRepository;

@Mock
private RoutineGroupRepository routineGroupRepository;

@InjectMocks
private OnboardingCommandServiceImpl onboardingCommandService;

@Test
void completesOnboardingWhenMemberOwnsRoutineGroup() {
Member member = createMember(false);
OnboardingRequestDTO.CompleteRequest request = createRequest();
when(memberRepository.findByIdForUpdate(MEMBER_ID)).thenReturn(Optional.of(member));
when(routineGroupRepository.existsByIdAndMember_Id(ROUTINE_GROUP_ID, MEMBER_ID))
.thenReturn(true);

OnboardingResponseDTO.StatusResponse response =
onboardingCommandService.completeOnboarding(MEMBER_ID, request);

assertThat(response.onboardingCompleted()).isTrue();
assertThat(member.getOnboardingCompleted()).isTrue();
}

@Test
void returnsSuccessWhenOnboardingIsAlreadyCompleted() {
Member member = createMember(true);
when(memberRepository.findByIdForUpdate(MEMBER_ID)).thenReturn(Optional.of(member));
when(routineGroupRepository.existsByIdAndMember_Id(ROUTINE_GROUP_ID, MEMBER_ID))
.thenReturn(true);

OnboardingResponseDTO.StatusResponse response =
onboardingCommandService.completeOnboarding(MEMBER_ID, createRequest());

assertThat(response.onboardingCompleted()).isTrue();
verify(routineGroupRepository)
.existsByIdAndMember_Id(ROUTINE_GROUP_ID, MEMBER_ID);
}

@Test
void rejectsUnownedRoutineGroupEvenWhenOnboardingIsAlreadyCompleted() {
Member member = createMember(true);
when(memberRepository.findByIdForUpdate(MEMBER_ID)).thenReturn(Optional.of(member));
when(routineGroupRepository.existsByIdAndMember_Id(ROUTINE_GROUP_ID, MEMBER_ID))
.thenReturn(false);

assertThatThrownBy(() ->
onboardingCommandService.completeOnboarding(MEMBER_ID, createRequest()))
.isInstanceOfSatisfying(BusinessException.class, exception ->
assertThat(exception.getBaseCode())
.isEqualTo(ErrorStatus.ROUTINE_GROUP_NOT_FOUND));
}

@Test
void rejectsRoutineGroupNotOwnedByMember() {
Member member = createMember(false);
when(memberRepository.findByIdForUpdate(MEMBER_ID)).thenReturn(Optional.of(member));
when(routineGroupRepository.existsByIdAndMember_Id(ROUTINE_GROUP_ID, MEMBER_ID))
.thenReturn(false);

assertThatThrownBy(() ->
onboardingCommandService.completeOnboarding(MEMBER_ID, createRequest()))
.isInstanceOfSatisfying(BusinessException.class, exception ->
assertThat(exception.getBaseCode())
.isEqualTo(ErrorStatus.ROUTINE_GROUP_NOT_FOUND));

assertThat(member.getOnboardingCompleted()).isFalse();
}

@Test
void rejectsMissingMember() {
when(memberRepository.findByIdForUpdate(MEMBER_ID)).thenReturn(Optional.empty());

assertThatThrownBy(() ->
onboardingCommandService.completeOnboarding(MEMBER_ID, createRequest()))
.isInstanceOfSatisfying(BusinessException.class, exception ->
assertThat(exception.getBaseCode())
.isEqualTo(ErrorStatus.MEMBER_NOT_FOUND));
}

private OnboardingRequestDTO.CompleteRequest createRequest() {
return new OnboardingRequestDTO.CompleteRequest(ROUTINE_GROUP_ID);
}

private Member createMember(boolean onboardingCompleted) {
return Member.builder()
.id(MEMBER_ID)
.oauthId("google-member-id")
.nickname("모루")
.role(Role.MEMBER)
.loginType(LoginType.GOOGLE)
.onboardingCompleted(onboardingCompleted)
.build();
}
}
Loading