Skip to content

Commit b640883

Browse files
nayonsosowhqtker
andauthored
feat: 멘토 조회 기능 구현 (#370)
* feat: 멘토 단일 조회 응답 dto 생성 * feat: 멘토, 멘토링, 채널 레포지토리 생성 * feat: 멘토 단일 조회 서비스 함수 생성 * feat: 멘토 단일 조회 컨트롤러 생성 * test: 멘토 관련 픽스처 생성 * refactor: channel 연관관계 편의 메서드 추가 * test: channel 픽스쳐 생성 * test: 멘토 단일 조회 테스트 코드 작성 * feat: 멘토 미리보기 목록 조회 dto 생성 * feat: 멘토 미리보기 Batch 조회를 위한 레포지토리 생성 - Mentor와 SiteUser는 id 만 참조하는 관계이다. - Mentor와 Mentoring도 id 만 참조하는 관계이다. - "멘토 목록"에 대해서 매번 siteUser, mentoring과 join 하면 N+1 이 발생한다. - 이를 해결하기 위해, 한번에 조회하고 매핑하여 1번의 쿼리로 해결한다. * feat: 멘토 미리보기 목록 서비스 함수 생성 * feat: 멘토 미리보기 목록 컨트롤러 생성 * test: 멘토 미리보기 목록 조회 테스트 코드 작성 * test: 멘토 배치 조회 레포지토리 테스트 코드 작성 * style: 개행 삭제 * refactor: Page가 아니라 Slice를 반환받도록 - Page를 사용할 시, 추가로 발생하는 count 쿼리 방지 * refactor: Channel N+1 문제 해결 * refactor: SliceResponse로 대체 * refactor: 멘토 목록 조회 정렬 정책 구체화 - 기획팀께 답변 받은 내용 적용 * feat: 채널 응답을 sequence 오름차순으로 정렬하는 기능 구현 * refactor: SliceResponse 정적 팩터리 메서드 사용하도록 * refactor: 자연스럽게 읽히도록 파라미터 순서 변경 * refactor: 함수 이름이 의미를 드러내도록 이름 변경 * refactor: JPA 표준 따르도록 함수이름 변경 - findBy -> findAllBy * refactor: 멘토 객체에서 채널의 순서를 정렬해 가지고 있도록 - test도 더 정확하게 수정 --------- Co-authored-by: seonghyeok <chosh2001@naver.com>
1 parent 7ef425a commit b640883

18 files changed

Lines changed: 678 additions & 4 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.solidconnection.common.dto;
2+
3+
import org.springframework.data.domain.Slice;
4+
5+
import java.util.List;
6+
7+
public record SliceResponse<T>(
8+
List<T> content,
9+
int nextPageNumber
10+
) {
11+
12+
private static final int NO_NEXT_PAGE = -1;
13+
private static final int BASE_NUMBER = 1; // 1-based
14+
15+
public static <T, R> SliceResponse<R> of(List<R> content, Slice<T> slice) {
16+
int nextPageNumber = slice.hasNext()
17+
? slice.getNumber() + BASE_NUMBER + 1
18+
: NO_NEXT_PAGE;
19+
return new SliceResponse<>(content, nextPageNumber);
20+
}
21+
}

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public enum ErrorCode {
4343
GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."),
4444
LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."),
4545
NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."),
46+
MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 멘토입니다."),
4647

4748
// auth
4849
USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."),
@@ -106,7 +107,6 @@ public enum ErrorCode {
106107

107108
// mentor
108109
ALREADY_MENTOR(HttpStatus.BAD_REQUEST.value(), "이미 멘토로 등록된 사용자입니다."),
109-
MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 사용자는 멘토로 등록되어 있지 않습니다."),
110110
MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."),
111111
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
112112
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.example.solidconnection.mentor.controller;
2+
3+
import com.example.solidconnection.common.dto.SliceResponse;
4+
import com.example.solidconnection.common.resolver.AuthorizedUser;
5+
import com.example.solidconnection.mentor.dto.MentorDetailResponse;
6+
import com.example.solidconnection.mentor.dto.MentorPreviewResponse;
7+
import com.example.solidconnection.mentor.service.MentorQueryService;
8+
import com.example.solidconnection.siteuser.domain.SiteUser;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.data.domain.Sort;
12+
import org.springframework.data.web.PageableDefault;
13+
import org.springframework.data.web.SortDefault;
14+
import org.springframework.data.web.SortDefault.SortDefaults;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.GetMapping;
17+
import org.springframework.web.bind.annotation.PathVariable;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RequestParam;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
import static org.springframework.data.domain.Sort.Direction.DESC;
23+
24+
@RequiredArgsConstructor
25+
@RequestMapping("/mentors")
26+
@RestController
27+
public class MentorController {
28+
29+
private final MentorQueryService mentorQueryService;
30+
31+
@GetMapping("/{mentor-id}")
32+
public ResponseEntity<MentorDetailResponse> getMentorDetails(
33+
@AuthorizedUser SiteUser siteUser,
34+
@PathVariable("mentor-id") Long mentorId
35+
) {
36+
MentorDetailResponse response = mentorQueryService.getMentorDetails(mentorId, siteUser);
37+
return ResponseEntity.ok(response);
38+
}
39+
40+
@GetMapping
41+
public ResponseEntity<SliceResponse<MentorPreviewResponse>> getMentorPreviews(
42+
@AuthorizedUser SiteUser siteUser,
43+
@RequestParam("region") String region,
44+
45+
@PageableDefault(size = 3, sort = "menteeCount", direction = DESC)
46+
@SortDefaults({
47+
@SortDefault(sort = "menteeCount", direction = Sort.Direction.DESC),
48+
@SortDefault(sort = "id", direction = Sort.Direction.ASC)
49+
})
50+
Pageable pageable
51+
) {
52+
SliceResponse<MentorPreviewResponse> response = mentorQueryService.getMentorPreviews(region, siteUser, pageable);
53+
return ResponseEntity.ok(response);
54+
}
55+
}

src/main/java/com/example/solidconnection/mentor/domain/Channel.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ public class Channel {
4444

4545
@ManyToOne(fetch = FetchType.LAZY)
4646
private Mentor mentor;
47+
48+
public void updateMentor(Mentor mentor) {
49+
this.mentor = mentor;
50+
}
4751
}

src/main/java/com/example/solidconnection/mentor/domain/Mentor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import jakarta.persistence.GenerationType;
88
import jakarta.persistence.Id;
99
import jakarta.persistence.OneToMany;
10+
import jakarta.persistence.OrderBy;
1011
import lombok.AccessLevel;
1112
import lombok.AllArgsConstructor;
1213
import lombok.Getter;
1314
import lombok.NoArgsConstructor;
15+
import org.hibernate.annotations.BatchSize;
1416

1517
import java.util.ArrayList;
1618
import java.util.List;
@@ -43,6 +45,8 @@ public class Mentor {
4345
@Column
4446
private long universityId;
4547

48+
@BatchSize(size = 10)
49+
@OrderBy("sequence ASC")
4650
@OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true)
4751
private List<Channel> channels = new ArrayList<>();
4852

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.example.solidconnection.mentor.dto;
2+
3+
import com.example.solidconnection.mentor.domain.Channel;
4+
import com.example.solidconnection.mentor.domain.ChannelType;
5+
6+
public record ChannelResponse(
7+
ChannelType type,
8+
String url
9+
) {
10+
11+
public static ChannelResponse from(Channel channel) {
12+
return new ChannelResponse(
13+
channel.getType(),
14+
channel.getUrl()
15+
);
16+
}
17+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.example.solidconnection.mentor.dto;
2+
3+
import com.example.solidconnection.mentor.domain.Mentor;
4+
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
5+
import com.example.solidconnection.siteuser.domain.SiteUser;
6+
7+
import java.util.List;
8+
9+
public record MentorDetailResponse(
10+
long id,
11+
String nickname,
12+
String profileImageUrl,
13+
ExchangeStatus exchangeStatus,
14+
String country,
15+
String universityName,
16+
int menteeCount,
17+
boolean hasBadge,
18+
String introduction,
19+
List<ChannelResponse> channels,
20+
String passTip,
21+
boolean isApplied
22+
) {
23+
24+
public static MentorDetailResponse of(Mentor mentor, SiteUser mentorUser, boolean isApplied) {
25+
return new MentorDetailResponse(
26+
mentor.getId(),
27+
mentorUser.getNickname(),
28+
mentorUser.getProfileImageUrl(),
29+
mentorUser.getExchangeStatus(),
30+
"국가", // todo: 교환학생 기록이 인증되면 추가
31+
"대학 이름", // todo: 교환학생 기록이 인증되면 추가
32+
mentor.getMenteeCount(),
33+
mentor.isHasBadge(),
34+
mentor.getIntroduction(),
35+
mentor.getChannels().stream().map(ChannelResponse::from).toList(),
36+
mentor.getPassTip(),
37+
isApplied
38+
);
39+
}
40+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.example.solidconnection.mentor.dto;
2+
3+
import com.example.solidconnection.mentor.domain.Mentor;
4+
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
5+
import com.example.solidconnection.siteuser.domain.SiteUser;
6+
7+
import java.util.List;
8+
9+
public record MentorPreviewResponse(
10+
long id,
11+
String nickname,
12+
String profileImageUrl,
13+
ExchangeStatus exchangeStatus,
14+
String country,
15+
String universityName,
16+
int menteeCount,
17+
boolean hasBadge,
18+
String introduction,
19+
List<ChannelResponse> channels,
20+
boolean isApplied
21+
) {
22+
23+
public static MentorPreviewResponse of(Mentor mentor, SiteUser mentorUser, boolean isApplied) {
24+
return new MentorPreviewResponse(
25+
mentor.getId(),
26+
mentorUser.getNickname(),
27+
mentorUser.getProfileImageUrl(),
28+
mentorUser.getExchangeStatus(),
29+
"국가", // todo: 교환학생 기록이 인증되면 추가
30+
"대학 이름", // todo: 교환학생 기록이 인증되면 추가
31+
mentor.getMenteeCount(),
32+
mentor.isHasBadge(),
33+
mentor.getIntroduction(),
34+
mentor.getChannels().stream().map(ChannelResponse::from).toList(),
35+
isApplied
36+
);
37+
}
38+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.example.solidconnection.mentor.repository;
2+
3+
import com.example.solidconnection.mentor.domain.Channel;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface ChannelRepository extends JpaRepository<Channel, Long> {
7+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.example.solidconnection.mentor.repository;
2+
3+
import com.example.solidconnection.common.exception.CustomException;
4+
import com.example.solidconnection.mentor.domain.Mentor;
5+
import com.example.solidconnection.mentor.domain.Mentoring;
6+
import com.example.solidconnection.siteuser.domain.SiteUser;
7+
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Repository;
10+
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.function.Function;
15+
import java.util.stream.Collectors;
16+
17+
import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION;
18+
19+
@Repository
20+
@RequiredArgsConstructor
21+
public class MentorBatchQueryRepository { // 연관관계가 설정되지 않은 엔티티들을 N+1 없이 하나의 쿼리로 조회
22+
23+
private final SiteUserRepository siteUserRepository;
24+
private final MentoringRepository mentoringRepository;
25+
26+
public Map<Long, SiteUser> getMentorIdToSiteUserMap(List<Mentor> mentors) {
27+
List<Long> mentorUserIds = mentors.stream().map(Mentor::getSiteUserId).toList();
28+
List<SiteUser> mentorUsers = siteUserRepository.findAllById(mentorUserIds);
29+
Map<Long, SiteUser> mentorUserIdToSiteUserMap = mentorUsers.stream()
30+
.collect(Collectors.toMap(SiteUser::getId, Function.identity()));
31+
32+
return mentors.stream().collect(Collectors.toMap(
33+
Mentor::getId,
34+
mentor -> {
35+
SiteUser mentorUser = mentorUserIdToSiteUserMap.get(mentor.getSiteUserId());
36+
if (mentorUser == null) { // site_user.id == mentor.site_user_id 에 해당하는게 없으면 정합성 문제가 발생한 것
37+
throw new CustomException(DATA_INTEGRITY_VIOLATION, "mentor에 해당하는 siteUser 존재하지 않음");
38+
}
39+
return mentorUser;
40+
}
41+
));
42+
}
43+
44+
public Map<Long, Boolean> getMentorIdToIsApplied(List<Mentor> mentors, long currentUserId) {
45+
List<Long> mentorIds = mentors.stream().map(Mentor::getId).toList();
46+
List<Mentoring> appliedMentorings = mentoringRepository.findAllByMentorIdInAndMenteeId(mentorIds, currentUserId);
47+
Set<Long> appliedMentorIds = appliedMentorings.stream()
48+
.map(Mentoring::getMentorId)
49+
.collect(Collectors.toSet());
50+
51+
return mentors.stream().collect(Collectors.toMap(
52+
Mentor::getId,
53+
mentor -> appliedMentorIds.contains(mentor.getId())
54+
));
55+
}
56+
}

0 commit comments

Comments
 (0)