Uh oh!
There was an error while loading. Please reload this page.
- 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
Uh oh!
There was an error while loading. Please reload this page.
feat: 멘토 조회 기능 구현 #370
Changes from all commits
f3f704d21e9a4eb6107e873ada9c3f9870860553b3b2f6e351864b0a236c6664717e7a7c8419b3d83350c5c3761dec6e034487fbfa3f5950fcc9244e40c943554118891cef418095d362836715f917a0bd906c7d58fd4753c6acb4File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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 쪽에서 말고 여기서 정렬조건을 구현하신 이유가 혹시 있나요? 성능상 차이는 없지만 궁금해서 여쭤봅니다 ㅎㅎ CollaboratorAuthor 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 순서로 정렬하는 로직이 어디있나요?
| ||
| 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개인가 그러니 별 상관 없겠죠?
| ||
| 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; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
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.
이렇게 표기하니 더 가독성이 좋은 거 같네요!