diff --git a/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java b/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java new file mode 100644 index 000000000..3f91dc7c4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.common.dto; + +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record SliceResponse( + List content, + int nextPageNumber +) { + + private static final int NO_NEXT_PAGE = -1; + private static final int BASE_NUMBER = 1; // 1-based + + public static SliceResponse of(List content, Slice slice) { + int nextPageNumber = slice.hasNext() + ? slice.getNumber() + BASE_NUMBER + 1 + : NO_NEXT_PAGE; + return new SliceResponse<>(content, nextPageNumber); + } +} diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index ee3f8e112..3b851ef77 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -43,6 +43,7 @@ public enum ErrorCode { GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."), LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."), NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."), + MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 멘토입니다."), // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), @@ -106,7 +107,6 @@ public enum ErrorCode { // mentor ALREADY_MENTOR(HttpStatus.BAD_REQUEST.value(), "이미 멘토로 등록된 사용자입니다."), - MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 사용자는 멘토로 등록되어 있지 않습니다."), MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."), UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java new file mode 100644 index 000000000..ea60b180c --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.mentor.controller; + +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.mentor.dto.MentorDetailResponse; +import com.example.solidconnection.mentor.dto.MentorPreviewResponse; +import com.example.solidconnection.mentor.service.MentorQueryService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +@RequiredArgsConstructor +@RequestMapping("/mentors") +@RestController +public class MentorController { + + private final MentorQueryService mentorQueryService; + + @GetMapping("/{mentor-id}") + public ResponseEntity getMentorDetails( + @AuthorizedUser SiteUser siteUser, + @PathVariable("mentor-id") Long mentorId + ) { + MentorDetailResponse response = mentorQueryService.getMentorDetails(mentorId, siteUser); + return ResponseEntity.ok(response); + } + + @GetMapping + public ResponseEntity> getMentorPreviews( + @AuthorizedUser SiteUser siteUser, + @RequestParam("region") String region, + + @PageableDefault(size = 3, sort = "menteeCount", direction = DESC) + @SortDefaults({ + @SortDefault(sort = "menteeCount", direction = Sort.Direction.DESC), + @SortDefault(sort = "id", direction = Sort.Direction.ASC) + }) + Pageable pageable + ) { + SliceResponse response = mentorQueryService.getMentorPreviews(region, siteUser, pageable); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/Channel.java b/src/main/java/com/example/solidconnection/mentor/domain/Channel.java index 33c4f72c3..846b8e625 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Channel.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Channel.java @@ -44,4 +44,8 @@ public class Channel { @ManyToOne(fetch = FetchType.LAZY) private Mentor mentor; + + public void updateMentor(Mentor mentor) { + this.mentor = mentor; + } } diff --git a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java index 87fe963a1..542972b9e 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java @@ -7,10 +7,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; import java.util.ArrayList; import java.util.List; @@ -43,6 +45,8 @@ public class Mentor { @Column private long universityId; + @BatchSize(size = 10) + @OrderBy("sequence ASC") @OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true) private List channels = new ArrayList<>(); diff --git a/src/main/java/com/example/solidconnection/mentor/dto/ChannelResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/ChannelResponse.java new file mode 100644 index 000000000..cc6de7c71 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/ChannelResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.ChannelType; + +public record ChannelResponse( + ChannelType type, + String url +) { + + public static ChannelResponse from(Channel channel) { + return new ChannelResponse( + channel.getType(), + channel.getUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java new file mode 100644 index 000000000..cf0fbc98a --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; +import com.example.solidconnection.siteuser.domain.SiteUser; + +import java.util.List; + +public record MentorDetailResponse( + long id, + String nickname, + String profileImageUrl, + ExchangeStatus exchangeStatus, + String country, + String universityName, + int menteeCount, + boolean hasBadge, + String introduction, + List channels, + String passTip, + boolean isApplied +) { + + public static MentorDetailResponse of(Mentor mentor, SiteUser mentorUser, boolean isApplied) { + return new MentorDetailResponse( + mentor.getId(), + mentorUser.getNickname(), + mentorUser.getProfileImageUrl(), + mentorUser.getExchangeStatus(), + "국가", // todo: 교환학생 기록이 인증되면 추가 + "대학 이름", // todo: 교환학생 기록이 인증되면 추가 + mentor.getMenteeCount(), + mentor.isHasBadge(), + mentor.getIntroduction(), + mentor.getChannels().stream().map(ChannelResponse::from).toList(), + mentor.getPassTip(), + isApplied + ); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java new file mode 100644 index 000000000..bba3010f4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; +import com.example.solidconnection.siteuser.domain.SiteUser; + +import java.util.List; + +public record MentorPreviewResponse( + long id, + String nickname, + String profileImageUrl, + ExchangeStatus exchangeStatus, + String country, + String universityName, + int menteeCount, + boolean hasBadge, + String introduction, + List channels, + boolean isApplied +) { + + public static MentorPreviewResponse of(Mentor mentor, SiteUser mentorUser, boolean isApplied) { + return new MentorPreviewResponse( + mentor.getId(), + mentorUser.getNickname(), + mentorUser.getProfileImageUrl(), + mentorUser.getExchangeStatus(), + "국가", // todo: 교환학생 기록이 인증되면 추가 + "대학 이름", // todo: 교환학생 기록이 인증되면 추가 + mentor.getMenteeCount(), + mentor.isHasBadge(), + mentor.getIntroduction(), + mentor.getChannels().stream().map(ChannelResponse::from).toList(), + isApplied + ); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/ChannelRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/ChannelRepository.java new file mode 100644 index 000000000..571e4fb21 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/repository/ChannelRepository.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.Channel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChannelRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java new file mode 100644 index 000000000..026c7aef5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java @@ -0,0 +1,56 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION; + +@Repository +@RequiredArgsConstructor +public class MentorBatchQueryRepository { // 연관관계가 설정되지 않은 엔티티들을 N+1 없이 하나의 쿼리로 조회 + + private final SiteUserRepository siteUserRepository; + private final MentoringRepository mentoringRepository; + + public Map getMentorIdToSiteUserMap(List mentors) { + List mentorUserIds = mentors.stream().map(Mentor::getSiteUserId).toList(); + List mentorUsers = siteUserRepository.findAllById(mentorUserIds); + Map mentorUserIdToSiteUserMap = mentorUsers.stream() + .collect(Collectors.toMap(SiteUser::getId, Function.identity())); + + return mentors.stream().collect(Collectors.toMap( + Mentor::getId, + mentor -> { + SiteUser mentorUser = mentorUserIdToSiteUserMap.get(mentor.getSiteUserId()); + if (mentorUser == null) { // site_user.id == mentor.site_user_id 에 해당하는게 없으면 정합성 문제가 발생한 것 + throw new CustomException(DATA_INTEGRITY_VIOLATION, "mentor에 해당하는 siteUser 존재하지 않음"); + } + return mentorUser; + } + )); + } + + public Map getMentorIdToIsApplied(List mentors, long currentUserId) { + List mentorIds = mentors.stream().map(Mentor::getId).toList(); + List appliedMentorings = mentoringRepository.findAllByMentorIdInAndMenteeId(mentorIds, currentUserId); + Set appliedMentorIds = appliedMentorings.stream() + .map(Mentoring::getMentorId) + .collect(Collectors.toSet()); + + return mentors.stream().collect(Collectors.toMap( + Mentor::getId, + mentor -> appliedMentorIds.contains(mentor.getId()) + )); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java index a8d6c91c3..aa8328ba7 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java @@ -1,13 +1,17 @@ package com.example.solidconnection.mentor.repository; import com.example.solidconnection.mentor.domain.Mentor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface MentorRepository extends JpaRepository { + boolean existsBySiteUserId(long siteUserId); + Optional findBySiteUserId(long siteUserId); - boolean existsBySiteUserId(long siteUserId); + Slice findAllBy(Pageable pageable); } diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java index d0bb648eb..76a3758f2 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java @@ -2,12 +2,22 @@ import com.example.solidconnection.mentor.domain.Mentoring; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; public interface MentoringRepository extends JpaRepository { + int countByMentorIdAndCheckedAtIsNull(long mentorId); + + boolean existsByMentorIdAndMenteeId(long mentorId, long menteeId); + List findAllByMentorId(long mentorId); - int countByMentorIdAndCheckedAtIsNull(long mentorId); + @Query(""" + SELECT m FROM Mentoring m + WHERE m.mentorId IN :mentorIds AND m.menteeId = :menteeId + """) + List findAllByMentorIdInAndMenteeId(@Param("mentorIds") List mentorIds, @Param("menteeId") long menteeId); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java new file mode 100644 index 000000000..a7c46e285 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java @@ -0,0 +1,67 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.dto.MentorDetailResponse; +import com.example.solidconnection.mentor.dto.MentorPreviewResponse; +import com.example.solidconnection.mentor.repository.MentorBatchQueryRepository; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.mentor.repository.MentoringRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class MentorQueryService { + + private final MentorRepository mentorRepository; + private final MentoringRepository mentoringRepository; + private final SiteUserRepository siteUserRepository; + private final MentorBatchQueryRepository mentorBatchQueryRepository; + + @Transactional(readOnly = true) + public MentorDetailResponse getMentorDetails(long mentorId, SiteUser currentUser) { + Mentor mentor = mentorRepository.findById(mentorId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + SiteUser mentorUser = siteUserRepository.findById(mentor.getSiteUserId()) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + boolean isApplied = mentoringRepository.existsByMentorIdAndMenteeId(mentorId, currentUser.getId()); + + return MentorDetailResponse.of(mentor, mentorUser, isApplied); + } + + @Transactional(readOnly = true) + public SliceResponse getMentorPreviews(String region, SiteUser siteUser, Pageable pageable) { // todo: 멘토의 '인증' 작업 후 region 필터링 추가 + Slice mentorSlice = mentorRepository.findAllBy(pageable); + List mentors = mentorSlice.toList(); + List content = getMentorPreviewResponses(mentors, siteUser); + + return SliceResponse.of(content, mentorSlice); + } + + private List getMentorPreviewResponses(List mentors, SiteUser siteUser) { + Map mentorIdToSiteUser = mentorBatchQueryRepository.getMentorIdToSiteUserMap(mentors); + Map mentorIdToIsApplied = mentorBatchQueryRepository.getMentorIdToIsApplied(mentors, siteUser.getId()); + + List mentorPreviews = new ArrayList<>(); + for (Mentor mentor : mentors) { + SiteUser mentorUser = mentorIdToSiteUser.get(mentor.getId()); + boolean isApplied = mentorIdToIsApplied.get(mentor.getId()); + MentorPreviewResponse response = MentorPreviewResponse.of(mentor, mentorUser, isApplied); + mentorPreviews.add(response); + } + return mentorPreviews; + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixture.java b/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixture.java new file mode 100644 index 000000000..d3c27c114 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixture.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.ChannelType; +import com.example.solidconnection.mentor.domain.Mentor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChannelFixture { + + private final ChannelFixtureBuilder channelFixtureBuilder; + + public Channel 채널(int sequence, Mentor mentor) { + return channelFixtureBuilder.channel() + .sequence(sequence) + .type(ChannelType.YOUTUBE) + .url("https://www.youtube.com/channel" + sequence) + .mentor(mentor) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixtureBuilder.java b/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixtureBuilder.java new file mode 100644 index 000000000..670b0e8b4 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/ChannelFixtureBuilder.java @@ -0,0 +1,56 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.ChannelType; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.repository.ChannelRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChannelFixtureBuilder { + + private final ChannelRepository channelRepository; + + private int sequence; + private ChannelType type; + private String url; + private Mentor mentor; + + public ChannelFixtureBuilder channel() { + return new ChannelFixtureBuilder(channelRepository); + } + + public ChannelFixtureBuilder sequence(int sequence) { + this.sequence = sequence; + return this; + } + + public ChannelFixtureBuilder type(ChannelType type) { + this.type = type; + return this; + } + + public ChannelFixtureBuilder url(String url) { + this.url = url; + return this; + } + + public ChannelFixtureBuilder mentor(Mentor mentor) { + this.mentor = mentor; + return this; + } + + public Channel create() { + Channel channel = new Channel( + null, + sequence, + type, + url, + null + ); + channel.updateMentor(mentor); + return channelRepository.save(channel); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java index 40718b2da..b612a9417 100644 --- a/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java @@ -15,7 +15,7 @@ public class MentorFixture { .siteUserId(siteUserId) .universityId(universityId) .introduction("멘토 소개") - .passTip("멘토 팁") + .passTip("합격 팁") .create(); } } diff --git a/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java new file mode 100644 index 000000000..0ef517c34 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java @@ -0,0 +1,80 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.fixture.MentoringFixture; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("멘토 배치 조회 레포지토리 테스트") +@TestContainerSpringBootTest +class MentorBatchQueryRepositoryTest { + + @Autowired + private MentorBatchQueryRepository mentorBatchQueryRepository; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private MentoringFixture mentoringFixture; + + private long universityId = 1L; // todo: 멘토 인증 기능 추가 변경 필요 + private Mentor mentor1, mentor2; + private SiteUser mentorUser1, mentorUser2, currentUser; + + @BeforeEach + void setUp() { + currentUser = siteUserFixture.사용자(1, "사용자"); + mentorUser1 = siteUserFixture.사용자(2, "멘토1"); + mentorUser2 = siteUserFixture.사용자(3, "멘토2"); + mentor1 = mentorFixture.멘토(mentorUser1.getId(), universityId); + mentor2 = mentorFixture.멘토(mentorUser2.getId(), universityId); + } + + @Test + void 멘토_ID_와_멘토_사용자를_매핑한다() { + // given + List mentors = List.of(mentor1, mentor2); + + // when + Map mentorIdToSiteUser = mentorBatchQueryRepository.getMentorIdToSiteUserMap(mentors); + + // then + assertAll( + () -> assertThat(mentorIdToSiteUser.get(mentor1.getId()).getId()).isEqualTo(mentorUser1.getId()), + () -> assertThat(mentorIdToSiteUser.get(mentor2.getId()).getId()).isEqualTo(mentorUser2.getId()) + ); + } + + @Test + void 멘토_ID_와_현재_사용자의_지원_여부를_매핑한다() { + // given + Mentoring 대기중_멘토링 = mentoringFixture.대기중_멘토링(mentor1.getId(), currentUser.getId()); + List mentors = List.of(mentor1, mentor2); + + // when + Map mentorIdToIsApplied = mentorBatchQueryRepository.getMentorIdToIsApplied(mentors, currentUser.getId()); + + // then + assertAll( + () -> assertThat(mentorIdToIsApplied.get(mentor1.getId())).isTrue(), + () -> assertThat(mentorIdToIsApplied.get(mentor2.getId())).isFalse() + ); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java new file mode 100644 index 000000000..421d24a12 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java @@ -0,0 +1,192 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.dto.ChannelResponse; +import com.example.solidconnection.mentor.dto.MentorDetailResponse; +import com.example.solidconnection.mentor.dto.MentorPreviewResponse; +import com.example.solidconnection.mentor.fixture.ChannelFixture; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.fixture.MentoringFixture; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("멘토 조회 서비스 테스트") +@TestContainerSpringBootTest +class MentorQueryServiceTest { + + @Autowired + private MentorQueryService mentorQueryService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private MentoringFixture mentoringFixture; + + @Autowired + private ChannelFixture channelFixture; + + private long universityId = 1L; // todo: 멘토 인증 기능 추가 변경 필요 + private String region = "아시아"; + + @Nested + class 멘토_단일_조회_성공 { + + @Test + void 멘토_정보를_조회한다() { + // given + SiteUser siteUser = siteUserFixture.사용자(); + SiteUser mentorUser = siteUserFixture.사용자(1, "멘토"); + Mentor mentor = mentorFixture.멘토(mentorUser.getId(), universityId); + Channel channel1 = channelFixture.채널(1, mentor); + Channel channel2 = channelFixture.채널(2, mentor); + + // when + MentorDetailResponse response = mentorQueryService.getMentorDetails(mentor.getId(), siteUser); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(mentor.getId()), + () -> assertThat(response.nickname()).isEqualTo(mentorUser.getNickname()), + () -> assertThat(response.channels()).extracting(ChannelResponse::url) + .containsExactly(channel1.getUrl(), channel2.getUrl()) + ); + } + + @Test + void 멘토에_대한_나의_멘토링_신청_여부를_조회한다() { + // given + SiteUser mentorUser = siteUserFixture.사용자(1, "멘토"); + Mentor mentor = mentorFixture.멘토(mentorUser.getId(), universityId); + + SiteUser notAppliedUser = siteUserFixture.사용자(2, "멘토링 지원 안한 사용자"); + SiteUser appliedUser = siteUserFixture.사용자(3, "멘토링 지원한 사용자"); + mentoringFixture.대기중_멘토링(mentor.getId(), appliedUser.getId()); + + // when + MentorDetailResponse notAppliedResponse = mentorQueryService.getMentorDetails(mentor.getId(), notAppliedUser); + MentorDetailResponse appliedResponse = mentorQueryService.getMentorDetails(mentor.getId(), appliedUser); + + // then + assertAll( + () -> assertThat(notAppliedResponse.isApplied()).isFalse(), + () -> assertThat(appliedResponse.isApplied()).isTrue() + ); + } + } + + @Nested + class 멘토_단일_조회_실패 { + + @Test + void 존재하지_않는_멘토를_조회하면_예외_응답을_반환한다() { + // given + long notExistingMentorId = 999L; + + // when & then + assertThatCode(() -> mentorQueryService.getMentorDetails(notExistingMentorId, siteUserFixture.사용자())) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.MENTOR_NOT_FOUND.getMessage()); + } + } + + @Nested + class 멘토_미리보기_목록_조회 { + + private static final int NO_NEXT_PAGE_NUMBER = -1; + + private Mentor mentor1, mentor2; + private SiteUser mentorUser1, mentorUser2, currentUser; + + @BeforeEach + void setUp() { + currentUser = siteUserFixture.사용자(1, "사용자1"); + mentorUser1 = siteUserFixture.사용자(2, "멘토1"); + mentorUser2 = siteUserFixture.사용자(3, "멘토2"); + mentor1 = mentorFixture.멘토(mentorUser1.getId(), universityId); + mentor2 = mentorFixture.멘토(mentorUser2.getId(), universityId); + } + + @Test + void 멘토_미리보기_목록의_정보를_조회한다() { + // given + Channel channel1 = channelFixture.채널(1, mentor1); + Channel channel2 = channelFixture.채널(2, mentor2); + + // when + SliceResponse response = mentorQueryService.getMentorPreviews(region, currentUser, PageRequest.of(0, 10)); + + // then + Map mentorPreviewMap = response.content().stream() + .collect(Collectors.toMap(MentorPreviewResponse::id, Function.identity())); + + assertAll( + () -> assertThat(mentorPreviewMap.get(mentor1.getId())).extracting(MentorPreviewResponse::nickname) + .isEqualTo(mentorUser1.getNickname()), + () -> assertThat(mentorPreviewMap.get(mentor1.getId()).channels()).extracting(ChannelResponse::url) + .containsOnly(channel1.getUrl()), + () -> assertThat(mentorPreviewMap.get(mentor2.getId())).extracting(MentorPreviewResponse::nickname) + .isEqualTo(mentorUser2.getNickname()), + () -> assertThat(mentorPreviewMap.get(mentor2.getId()).channels()).extracting(ChannelResponse::url) + .containsOnly(channel2.getUrl()) + ); + } + + @Test + void 멘토들에_대한_나의_멘토링_지원_여부를_조회한다() { + // given + mentoringFixture.대기중_멘토링(mentor1.getId(), currentUser.getId()); + + // when + SliceResponse response = mentorQueryService.getMentorPreviews(region, currentUser, PageRequest.of(0, 10)); + + // then + Map mentorPreviewMap = response.content().stream() + .collect(Collectors.toMap(MentorPreviewResponse::id, Function.identity())); + assertAll( + () -> assertThat(mentorPreviewMap.get(mentor1.getId()).isApplied()).isTrue(), + () -> assertThat(mentorPreviewMap.get(mentor2.getId()).isApplied()).isFalse() + ); + } + + @Test + void 다음_페이지_번호를_응답한다() { + // given + SliceResponse response = mentorQueryService.getMentorPreviews(region, currentUser, PageRequest.of(0, 1)); + + // then + assertThat(response.nextPageNumber()).isEqualTo(2); + } + + @Test + void 다음_페이지가_없으면_페이지_없음을_의미하는_값을_응답한다() { + // given + SliceResponse response = mentorQueryService.getMentorPreviews(region, currentUser, PageRequest.of(0, 10)); + + // then + assertThat(response.nextPageNumber()).isEqualTo(NO_NEXT_PAGE_NUMBER); + } + } +}