diff --git a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java index d2dce76c..ab6c2c36 100644 --- a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java +++ b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java @@ -1,6 +1,8 @@ package kr.touroot.global.auth.dto; +import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.constraints.NotNull; +@Hidden public record MemberAuth(@NotNull Long memberId) { } diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java new file mode 100644 index 00000000..9fca1559 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -0,0 +1,59 @@ +package kr.touroot.member.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTraveloguesResponse; +import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.member.service.MyPageFacadeService; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.converters.models.PageableAsQueryParam; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "마이 페이지") +@RequiredArgsConstructor +@RestController +@RequestMapping("api/v1/member/me") +public class MyPageController { + + private final MyPageFacadeService myPageFacadeService; + + @GetMapping("/profile") + public ResponseEntity readProfile(@NotNull MemberAuth memberAuth) { + ProfileResponse data = myPageFacadeService.readProfile(memberAuth); + return ResponseEntity.ok(data); + } + + @PageableAsQueryParam + @GetMapping("/travelogues") + public ResponseEntity> readTravelogues( + @NotNull MemberAuth memberAuth, + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); + return ResponseEntity.ok(data); + } + + @PageableAsQueryParam + @GetMapping("/travel-plans") + public ResponseEntity> readTravelPlans( + @NotNull MemberAuth memberAuth, + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); + return ResponseEntity.ok(data); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java new file mode 100644 index 00000000..52b55599 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java @@ -0,0 +1,19 @@ +package kr.touroot.member.dto; + +import kr.touroot.travelplan.domain.TravelPlan; +import lombok.Builder; + +import java.time.LocalDate; + +@Builder +public record MyTravelPlanResponse(long id, String title, LocalDate startDate, LocalDate endDate) { + + public static MyTravelPlanResponse of(TravelPlan travelPlan, int period) { + return MyTravelPlanResponse.builder() + .id(travelPlan.getId()) + .title(travelPlan.getTitle()) + .startDate(travelPlan.getStartDate()) + .endDate(travelPlan.getStartDate().plusDays(period)) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java new file mode 100644 index 00000000..7966c8fa --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java @@ -0,0 +1,23 @@ +package kr.touroot.member.dto; + +import kr.touroot.travelogue.domain.Travelogue; +import lombok.Builder; + +import java.time.format.DateTimeFormatter; + +@Builder +public record MyTraveloguesResponse(long id, String title, String thumbnailUrl, String createdAt) { + + public static MyTraveloguesResponse from(Travelogue travelogue) { + String createdAt = travelogue.getCreatedAt() + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + return MyTraveloguesResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .createdAt(createdAt) + .thumbnailUrl(travelogue.getThumbnail()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java b/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java new file mode 100644 index 00000000..3a961ef5 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java @@ -0,0 +1,15 @@ +package kr.touroot.member.dto; + +import kr.touroot.member.domain.Member; +import lombok.Builder; + +@Builder +public record ProfileResponse(String profileImageUrl, String nickname) { + + public static ProfileResponse from(Member member) { + return ProfileResponse.builder() + .profileImageUrl(member.getProfileImageUrl()) + .nickname(member.getNickname()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java new file mode 100644 index 00000000..a8fad42f --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -0,0 +1,48 @@ +package kr.touroot.member.service; + +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTraveloguesResponse; +import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.service.TravelogueService; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.service.TravelPlanService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class MyPageFacadeService { + + private final MemberService memberService; + private final TravelogueService travelogueService; + private final TravelPlanService travelPlanService; + + public ProfileResponse readProfile(MemberAuth memberAuth) { + Member member = memberService.getById(memberAuth.memberId()); + return ProfileResponse.from(member); + } + + public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { + Member member = memberService.getById(memberAuth.memberId()); + Page travelogues = travelogueService.findAllByMember(member, pageable); + + return travelogues.map(MyTraveloguesResponse::from); + } + + public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { + Member member = memberService.getById(memberAuth.memberId()); + Page travelPlans = travelPlanService.getAllByAuthor(member, pageable); + + return travelPlans.map(this::getMyTravelPlanResponse); + } + + private MyTravelPlanResponse getMyTravelPlanResponse(TravelPlan travelPlan) { + int period = travelPlanService.calculateTravelPeriod(travelPlan); + return MyTravelPlanResponse.of(travelPlan, period); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java index 5e17ae01..60609f7b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java @@ -1,7 +1,12 @@ package kr.touroot.travelogue.repository; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface TravelogueRepository extends JpaRepository { + + Page findAllByAuthor(Member author, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index dc9ca69e..352a7004 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -32,4 +32,8 @@ public Travelogue getTravelogueById(Long id) { public Page findAll(Pageable pageable) { return travelogueRepository.findAll(pageable); } + + public Page findAllByMember(Member member, Pageable pageable) { + return travelogueRepository.findAllByAuthor(member, pageable); + } } diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java index 0489b9fa..b0786bcf 100644 --- a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java @@ -1,7 +1,12 @@ package kr.touroot.travelplan.repository; +import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface TravelPlanRepository extends JpaRepository { + + Page findAllByAuthor(Member member, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index bc1b79eb..b4d16ddb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -21,6 +21,8 @@ import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; import kr.touroot.travelplan.repository.TravelPlanRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -121,4 +123,13 @@ private List getTravelPlanPlaceResponses(TravelPlanDay .map(TravelPlanPlaceResponse::from) .toList(); } + + public Page getAllByAuthor(Member member, Pageable pageable) { + return travelPlanRepository.findAllByAuthor(member, pageable); + } + + public int calculateTravelPeriod(TravelPlan travelPlan) { + return travelPlanDayRepository.findByPlan(travelPlan) + .size(); + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index 561de6ef..e8c06393 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -152,7 +152,7 @@ void readTravelPlanWithNonExist() { @Test void readTravelPlanWithNotAuthor() { // given - long id = testHelper.initTravelPlanTestData(member); + long id = testHelper.initTravelPlanTestData(member).getId(); Member notAuthor = testHelper.initMemberTestData(); String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 3e8e7726..69d6e445 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -59,7 +59,7 @@ public static TravelPlanPlace getTravelPlanPlace(String description, int order, return new TravelPlanPlace(description, order, day, place); } - public long initTravelPlanTestData(Member author) { + public TravelPlan initTravelPlanTestData(Member author) { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); Place place = getPlace("장소", "37.5175896", "127.0867236", ""); @@ -68,7 +68,9 @@ public long initTravelPlanTestData(Member author) { travelPlanRepository.save(travelPlan); travelPlanDayRepository.save(travelPlanDay); placeRepository.save(place); - return travelPlanPlaceRepository.save(travelPlanPlace).getId(); + travelPlanPlaceRepository.save(travelPlanPlace); + + return travelPlan; } public Member initMemberTestData() { diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index a9c8a1c1..6ec7bb19 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -5,6 +5,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; @@ -107,7 +108,7 @@ void createTravelPlanWithInvalidStartDate() { @Test void readTravelPlan() { // given - Long id = testHelper.initTravelPlanTestData(author); + Long id = testHelper.initTravelPlanTestData(author).getId(); // when TravelPlanResponse actual = travelPlanService.readTravelPlan(id, memberAuth); @@ -133,7 +134,7 @@ void readTravelPlanWitNonExist() { @Test void readTravelPlanWithNotAuthor() { // given - Long id = testHelper.initTravelPlanTestData(author); + Long id = testHelper.initTravelPlanTestData(author).getId(); MemberAuth notAuthor = new MemberAuth(testHelper.initMemberTestData().getId()); // when & then @@ -141,4 +142,17 @@ void readTravelPlanWithNotAuthor() { .isInstanceOf(ForbiddenException.class) .hasMessage("여행 계획은 작성자만 조회할 수 있습니다."); } + + @DisplayName("여행 계획 서비스는 여행 계획 일자를 계산해 반환한다.") + @Test + void calculateTravelPeriod() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + + // when + int actual = travelPlanService.calculateTravelPeriod(travelPlan); + + // then + assertThat(actual).isEqualTo(1); + } }