-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 멘토 조회 기능 구현 #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 멘토 조회 기능 구현 #370
Changes from all commits
f3f704d
21e9a4e
b6107e8
73ada9c
3f98708
60553b3
b2f6e35
1864b0a
236c666
4717e7a
7c8419b
3d83350
c5c3761
dec6e03
4487fbf
a3f5950
fcc9244
e40c943
5541188
91cef41
8095d36
2836715
f917a0b
d906c7d
58fd475
3c6acb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.example.solidconnection.common.dto; | ||
|
|
||
| import org.springframework.data.domain.Slice; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record SliceResponse<T>( | ||
| List<T> content, | ||
| int nextPageNumber | ||
| ) { | ||
|
|
||
| private static final int NO_NEXT_PAGE = -1; | ||
| private static final int BASE_NUMBER = 1; // 1-based | ||
|
|
||
| public static <T, R> SliceResponse<R> of(List<R> content, Slice<T> slice) { | ||
| int nextPageNumber = slice.hasNext() | ||
| ? slice.getNumber() + BASE_NUMBER + 1 | ||
| : NO_NEXT_PAGE; | ||
| return new SliceResponse<>(content, nextPageNumber); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MentorDetailResponse> getMentorDetails( | ||
| @AuthorizedUser SiteUser siteUser, | ||
| @PathVariable("mentor-id") Long mentorId | ||
| ) { | ||
| MentorDetailResponse response = mentorQueryService.getMentorDetails(mentorId, siteUser); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @GetMapping | ||
| public ResponseEntity<SliceResponse<MentorPreviewResponse>> 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) | ||
|
Comment on lines
+45
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Repository 쪽에서 말고 여기서 정렬조건을 구현하신 이유가 혹시 있나요? 성능상 차이는 없지만 궁금해서 여쭤봅니다 ㅎㅎ
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 하면 고정된 정렬 기준이 아니라, 프론트가 요청하는 정렬도 적용할 수 있어서요! |
||
| }) | ||
| Pageable pageable | ||
| ) { | ||
| SliceResponse<MentorPreviewResponse> response = mentorQueryService.getMentorPreviews(region, siteUser, pageable); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ChannelResponse> 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 | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ChannelResponse> 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: 교환학생 기록이 인증되면 추가 | ||
|
Comment on lines
+29
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain1. TODO 주석 처리 계획을 명확히 해주세요 현재 다음 사항들을 확인해 보시겠어요? 🏁 Script executed: #!/bin/bash
# 하드코딩된 값들이 다른 곳에도 있는지 확인
rg -A 2 -B 2 '"국가"'
rg -A 2 -B 2 '"대학 이름"'Length of output: 2570 임시값 SOS! TODO 하드코딩 처리 계획을 명확히 해주세요 작은 경고등이 반짝하며 임시값들이 프로덕션에 숨어들 준비를 하고 있습니다.
추가 제안 사항입니다. 🤖 Prompt for AI Agents |
||
| mentor.getMenteeCount(), | ||
| mentor.isHasBadge(), | ||
| mentor.getIntroduction(), | ||
| mentor.getChannels().stream().map(ChannelResponse::from).toList(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 채널이 sequence 순서로 정렬하는 로직이 어디있나요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그런 로직도 필요하겠네요..! 반영했습니다 🫡 91cef41 |
||
| isApplied | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Channel, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 없이 하나의 쿼리로 조회 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 이렇게 N+1 해결하는 것도 정말 좋은 거 같습니다! 함수명을 잘지어주시니 아주 잘 이해가 되네요! stream은 제가 공부를 조금 더 해보겠습니다.. 추가로 채널같은 경우는 N+1이 그대로 발생하는데 최대 2개인가 그러니 별 상관 없겠죠?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
그래도 N+1 방지하는게 좋을 것 같습니다. 제가 놓친 부분이네요😅 짚어주셔서 감사합니다~! 반영했습니다. fcc9244 |
||
|
|
||
| private final SiteUserRepository siteUserRepository; | ||
| private final MentoringRepository mentoringRepository; | ||
|
|
||
| public Map<Long, SiteUser> getMentorIdToSiteUserMap(List<Mentor> mentors) { | ||
| List<Long> mentorUserIds = mentors.stream().map(Mentor::getSiteUserId).toList(); | ||
| List<SiteUser> mentorUsers = siteUserRepository.findAllById(mentorUserIds); | ||
| Map<Long, SiteUser> 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<Long, Boolean> getMentorIdToIsApplied(List<Mentor> mentors, long currentUserId) { | ||
| List<Long> mentorIds = mentors.stream().map(Mentor::getId).toList(); | ||
| List<Mentoring> appliedMentorings = mentoringRepository.findAllByMentorIdInAndMenteeId(mentorIds, currentUserId); | ||
| Set<Long> appliedMentorIds = appliedMentorings.stream() | ||
| .map(Mentoring::getMentorId) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| return mentors.stream().collect(Collectors.toMap( | ||
| Mentor::getId, | ||
| mentor -> appliedMentorIds.contains(mentor.getId()) | ||
| )); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Mentor, Long> { | ||
|
|
||
| boolean existsBySiteUserId(long siteUserId); | ||
|
|
||
| Optional<Mentor> findBySiteUserId(long siteUserId); | ||
|
|
||
| boolean existsBySiteUserId(long siteUserId); | ||
| Slice<Mentor> findAllBy(Pageable pageable); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MentorPreviewResponse> getMentorPreviews(String region, SiteUser siteUser, Pageable pageable) { // todo: 멘토의 '인증' 작업 후 region 필터링 추가 | ||
| Slice<Mentor> mentorSlice = mentorRepository.findAllBy(pageable); | ||
| List<Mentor> mentors = mentorSlice.toList(); | ||
| List<MentorPreviewResponse> content = getMentorPreviewResponses(mentors, siteUser); | ||
|
|
||
| return SliceResponse.of(content, mentorSlice); | ||
| } | ||
|
|
||
| private List<MentorPreviewResponse> getMentorPreviewResponses(List<Mentor> mentors, SiteUser siteUser) { | ||
| Map<Long, SiteUser> mentorIdToSiteUser = mentorBatchQueryRepository.getMentorIdToSiteUserMap(mentors); | ||
| Map<Long, Boolean> mentorIdToIsApplied = mentorBatchQueryRepository.getMentorIdToIsApplied(mentors, siteUser.getId()); | ||
|
|
||
| List<MentorPreviewResponse> 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 표기하니 더 가독성이 좋은 거 같네요!