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 3b851ef77..0ea4b29a5 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -101,6 +101,9 @@ public enum ErrorCode { // news INVALID_NEWS_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 소식지만 제어할 수 있습니다."), + // mentor + CHANNEL_SEQUENCE_NOT_UNIQUE(HttpStatus.BAD_REQUEST.value(), "채널의 순서가 중복되었습니다."), + CHANNEL_REGISTRATION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST.value(), "등록 가능한 채널 수를 초과하였습니다."), // database DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java new file mode 100644 index 000000000..ba8bc0911 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java @@ -0,0 +1,44 @@ +package com.example.solidconnection.mentor.controller; + +import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.mentor.dto.MentorMyPageResponse; +import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; +import com.example.solidconnection.mentor.service.MentorMyPageService; +import com.example.solidconnection.security.annotation.RequireRoleAccess; +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/mentor/my") +@RestController +public class MentorMyPageController { + + private final MentorMyPageService mentorMyPageService; + + @RequireRoleAccess(roles = Role.MENTOR) + @GetMapping + public ResponseEntity getMentorMyPage( + @AuthorizedUser SiteUser siteUser + ) { + MentorMyPageResponse mentorMyPageResponse = mentorMyPageService.getMentorMyPage(siteUser); + return ResponseEntity.ok(mentorMyPageResponse); + } + + @RequireRoleAccess(roles = Role.MENTOR) + @PutMapping + public ResponseEntity updateMentorMyPage( + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody MentorMyPageUpdateRequest mentorMyPageUpdateRequest + ) { + mentorMyPageService.updateMentorMyPage(siteUser, mentorMyPageUpdateRequest); + return ResponseEntity.ok().build(); + } +} 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 846b8e625..b3c12bff7 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Channel.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Channel.java @@ -45,6 +45,12 @@ public class Channel { @ManyToOne(fetch = FetchType.LAZY) private Mentor mentor; + public Channel(int sequence, ChannelType type, String url) { + this.sequence = sequence; + this.type = type; + this.url = url; + } + 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 542972b9e..253bf666d 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java @@ -53,4 +53,23 @@ public class Mentor { public void increaseMenteeCount() { this.menteeCount++; } + + public void updateIntroduction(String introduction) { + this.introduction = introduction; + } + + public void updatePassTip(String passTip) { + this.passTip = passTip; + } + + public void updateChannels(List channels) { + this.channels.clear(); + if (channels == null || channels.isEmpty()) { + return; + } + for (Channel channel : channels) { + channel.updateMentor(this); + this.channels.add(channel); + } + } } diff --git a/src/main/java/com/example/solidconnection/mentor/dto/ChannelRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/ChannelRequest.java new file mode 100644 index 000000000..9172262ae --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/ChannelRequest.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.ChannelType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.URL; + +public record ChannelRequest( + @NotNull(message = "채널 종류를 입력해주세요.") + ChannelType type, + + @NotBlank(message = "채널 URL을 입력해주세요.") + @URL + String url +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java new file mode 100644 index 000000000..91077051f --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.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 MentorMyPageResponse( + long id, + String profileImageUrl, + String nickname, + ExchangeStatus exchangeStatus, + String country, + String universityName, + int menteeCount, + boolean hasBadge, + String introduction, + List channels +) { + + public static MentorMyPageResponse of(Mentor mentor, SiteUser siteUser) { + return new MentorMyPageResponse( + mentor.getId(), + siteUser.getProfileImageUrl(), + siteUser.getNickname(), + siteUser.getExchangeStatus(), + "국가", // todo: 교환학생 기록이 인증되면 추가 + "대학 이름", + mentor.getMenteeCount(), + mentor.isHasBadge(), + mentor.getIntroduction(), + mentor.getChannels().stream() + .map(ChannelResponse::from) + .toList() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageUpdateRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageUpdateRequest.java new file mode 100644 index 000000000..5a6790aeb --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageUpdateRequest.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.mentor.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; + +import java.util.List; + +public record MentorMyPageUpdateRequest( + @NotBlank(message = "자기소개를 입력해주세요.") + String introduction, + + @NotBlank(message = "합격 레시피를 입력해주세요.") + String passTip, + + @Valid + List channels +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewsResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewsResponse.java new file mode 100644 index 000000000..cb49ba602 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewsResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.mentor.dto; + +import java.util.List; + +public record MentorPreviewsResponse( + List content, + int nextPageNumber +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java new file mode 100644 index 000000000..6a2d47ded --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.dto.ChannelRequest; +import com.example.solidconnection.mentor.dto.MentorMyPageResponse; +import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.solidconnection.common.exception.ErrorCode.CHANNEL_REGISTRATION_LIMIT_EXCEEDED; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class MentorMyPageService { + + private static final int CHANNEL_REGISTRATION_LIMIT = 4; + private static final int CHANNEL_SEQUENCE_START_NUMBER = 1; + + private final MentorRepository mentorRepository; + + @Transactional(readOnly = true) + public MentorMyPageResponse getMentorMyPage(SiteUser siteUser) { + Mentor mentor = mentorRepository.findBySiteUserId(siteUser.getId()) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + return MentorMyPageResponse.of(mentor, siteUser); + } + + @Transactional + public void updateMentorMyPage(SiteUser siteUser, MentorMyPageUpdateRequest request) { + validateChannelRegistrationLimit(request.channels()); + Mentor mentor = mentorRepository.findBySiteUserId(siteUser.getId()) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + + mentor.updateIntroduction(request.introduction()); + mentor.updatePassTip(request.passTip()); + updateChannel(request.channels(), mentor); + } + + private void validateChannelRegistrationLimit(List channelRequests) { + if (channelRequests.size() > CHANNEL_REGISTRATION_LIMIT) { + throw new CustomException(CHANNEL_REGISTRATION_LIMIT_EXCEEDED); + } + } + + private void updateChannel(List channelRequests, Mentor mentor) { + int sequence = CHANNEL_SEQUENCE_START_NUMBER; + List newChannels = new ArrayList<>(); + for (ChannelRequest request : channelRequests) { + newChannels.add(new Channel(sequence++, request.type(), request.url())); + } + mentor.updateChannels(newChannels); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/repository/ChannelRepositoryForTest.java b/src/test/java/com/example/solidconnection/mentor/repository/ChannelRepositoryForTest.java new file mode 100644 index 000000000..9e6fee1de --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/repository/ChannelRepositoryForTest.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.Channel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ChannelRepositoryForTest extends JpaRepository { + + List findAllByMentorId(long mentorId); +} diff --git a/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java index 0ef517c34..c68deedb7 100644 --- a/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java @@ -1,7 +1,6 @@ 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; @@ -29,10 +28,10 @@ class MentorBatchQueryRepositoryTest { private MentorFixture mentorFixture; @Autowired - private SiteUserFixture siteUserFixture; + private MentoringFixture mentoringFixture; @Autowired - private MentoringFixture mentoringFixture; + private SiteUserFixture siteUserFixture; private long universityId = 1L; // todo: 멘토 인증 기능 추가 변경 필요 private Mentor mentor1, mentor2; @@ -65,7 +64,7 @@ void setUp() { @Test void 멘토_ID_와_현재_사용자의_지원_여부를_매핑한다() { // given - Mentoring 대기중_멘토링 = mentoringFixture.대기중_멘토링(mentor1.getId(), currentUser.getId()); + mentoringFixture.대기중_멘토링(mentor1.getId(), currentUser.getId()); List mentors = List.of(mentor1, mentor2); // when diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java new file mode 100644 index 000000000..b7c5724a7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java @@ -0,0 +1,126 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.mentor.domain.Channel; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.dto.ChannelRequest; +import com.example.solidconnection.mentor.dto.ChannelResponse; +import com.example.solidconnection.mentor.dto.MentorMyPageResponse; +import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; +import com.example.solidconnection.mentor.fixture.ChannelFixture; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.repository.ChannelRepositoryForTest; +import com.example.solidconnection.mentor.repository.MentorRepository; +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 java.util.List; + +import static com.example.solidconnection.mentor.domain.ChannelType.BLOG; +import static com.example.solidconnection.mentor.domain.ChannelType.INSTAGRAM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("멘토 마이페이지 서비스 테스트") +class MentorMyPageServiceTest { + + @Autowired + private MentorMyPageService mentorMyPageService; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private ChannelFixture channelFixture; + + @Autowired + private MentorRepository mentorRepository; + + @Autowired + private ChannelRepositoryForTest channelRepositoryForTest; + + private SiteUser mentorUser; + private Mentor mentor; + private long universityId = 1L; + + @BeforeEach + void setUp() { + mentorUser = siteUserFixture.멘토(1, "멘토"); + mentor = mentorFixture.멘토(mentorUser.getId(), universityId); + } + + @Nested + class 멘토의_마이_페이지를_조회한다 { + + @Test + void 성공적으로_조회한다() { + // given + Channel channel1 = channelFixture.채널(1, mentor); + Channel channel2 = channelFixture.채널(2, mentor); + + // when + MentorMyPageResponse response = mentorMyPageService.getMentorMyPage(mentorUser); + + // 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()) + ); + } + } + + @Nested + class 멘토의_마이_페이지를_수정한다 { + + @Test + void 멘토_정보를_수정한다() { + // given + String newIntroduction = "새로운 자기소개"; + String newPassTip = "새로운 합격 팁"; + MentorMyPageUpdateRequest request = new MentorMyPageUpdateRequest(newIntroduction, newPassTip, List.of()); + + // when + mentorMyPageService.updateMentorMyPage(mentorUser, request); + + // then + Mentor updatedMentor = mentorRepository.findById(mentor.getId()).get(); + assertAll( + () -> assertThat(updatedMentor.getIntroduction()).isEqualTo(newIntroduction), + () -> assertThat(updatedMentor.getPassTip()).isEqualTo(newPassTip) + ); + } + + @Test + void 채널_정보를_수정한다() { + // given + List newChannels = List.of( + new ChannelRequest(BLOG, "https://blog.com"), + new ChannelRequest(INSTAGRAM, "https://instagram.com") + ); + MentorMyPageUpdateRequest request = new MentorMyPageUpdateRequest("introduction", "passTip", newChannels); + + // when + mentorMyPageService.updateMentorMyPage(mentorUser, request); + + // then + List updatedChannels = channelRepositoryForTest.findAllByMentorId(mentor.getId()); + assertAll( + () -> assertThat(updatedChannels).extracting(Channel::getType) + .containsExactly(BLOG, INSTAGRAM), + () -> assertThat(updatedChannels).extracting(Channel::getUrl) + .containsExactly("https://blog.com", "https://instagram.com") + ); + } + } +}