diff --git a/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponse.java b/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponse.java new file mode 100644 index 000000000..407c2951a --- /dev/null +++ b/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponse.java @@ -0,0 +1,13 @@ +package com.staccato.member.service.dto.response; + +import com.staccato.member.domain.Member; + +public record MemberResponse( + Long memberId, + String nickName, + String memberImage +) { + public MemberResponse(Member member) { + this(member.getId(), member.getNickname(), member.getImageUrl()); + } +} diff --git a/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponses.java b/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponses.java new file mode 100644 index 000000000..86d5978b4 --- /dev/null +++ b/backend/src/main/java/com/staccato/member/service/dto/response/MemberResponses.java @@ -0,0 +1,13 @@ +package com.staccato.member.service.dto.response; + +import java.util.List; + +import com.staccato.member.domain.Member; + +public record MemberResponses(List members) { + public static MemberResponses from(List members) { + return new MemberResponses(members.stream() + .map(MemberResponse::new) + .toList()); + } +} diff --git a/backend/src/main/java/com/staccato/travel/controller/TravelController.java b/backend/src/main/java/com/staccato/travel/controller/TravelController.java index 577df80aa..74ea6c359 100644 --- a/backend/src/main/java/com/staccato/travel/controller/TravelController.java +++ b/backend/src/main/java/com/staccato/travel/controller/TravelController.java @@ -5,14 +5,17 @@ import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.staccato.config.auth.MemberId; import com.staccato.travel.service.TravelService; import com.staccato.travel.service.dto.request.TravelRequest; +import com.staccato.travel.service.dto.response.TravelResponses; import lombok.RequiredArgsConstructor; @@ -27,4 +30,11 @@ public ResponseEntity createTravel(@Valid @RequestBody TravelRequest trave long travelId = travelService.createTravel(travelRequest, memberId); return ResponseEntity.created(URI.create("/travels/" + travelId)).build(); } + + @GetMapping + public ResponseEntity readAllTravels( + @MemberId Long memberId, + @RequestParam(value = "year", required = false) Integer year) { + return ResponseEntity.ok(travelService.readAllTravels(memberId, year)); + } } diff --git a/backend/src/main/java/com/staccato/travel/domain/Travel.java b/backend/src/main/java/com/staccato/travel/domain/Travel.java index cc171ff71..72103850d 100644 --- a/backend/src/main/java/com/staccato/travel/domain/Travel.java +++ b/backend/src/main/java/com/staccato/travel/domain/Travel.java @@ -15,6 +15,7 @@ import com.staccato.config.domain.BaseEntity; import com.staccato.exception.StaccatoException; +import com.staccato.member.domain.Member; import lombok.AccessLevel; import lombok.Builder; @@ -62,4 +63,10 @@ private void validateDate(LocalDate startAt, LocalDate endAt) { public void addTravelMember(TravelMember travelMember) { travelMembers.add(travelMember); } + + public List getMates() { + return travelMembers.stream() + .map(TravelMember::getMember) + .toList(); + } } diff --git a/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepository.java b/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepository.java new file mode 100644 index 000000000..9274a4968 --- /dev/null +++ b/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepository.java @@ -0,0 +1,16 @@ +package com.staccato.travel.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.staccato.travel.domain.TravelMember; + +public interface TravelMemberRepository extends JpaRepository { + List findAllByMemberId(long memberId); + + @Query("SELECT tm FROM TravelMember tm WHERE tm.member.id = :memberId AND YEAR(tm.travel.startAt) = :year") + List findAllByMemberIdAndTravelStartAtYear(@Param("memberId") long memberId, @Param("year") int year); +} diff --git a/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepostiory.java b/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepostiory.java deleted file mode 100644 index b8eacc7a7..000000000 --- a/backend/src/main/java/com/staccato/travel/repository/TravelMemberRepostiory.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.staccato.travel.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.staccato.travel.domain.TravelMember; - -public interface TravelMemberRepostiory extends JpaRepository { -} diff --git a/backend/src/main/java/com/staccato/travel/service/TravelService.java b/backend/src/main/java/com/staccato/travel/service/TravelService.java index ac762ed90..5d0285b84 100644 --- a/backend/src/main/java/com/staccato/travel/service/TravelService.java +++ b/backend/src/main/java/com/staccato/travel/service/TravelService.java @@ -1,5 +1,8 @@ package com.staccato.travel.service; +import java.util.List; +import java.util.Optional; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -7,9 +10,10 @@ import com.staccato.member.repository.MemberRepository; import com.staccato.travel.domain.Travel; import com.staccato.travel.domain.TravelMember; -import com.staccato.travel.repository.TravelMemberRepostiory; +import com.staccato.travel.repository.TravelMemberRepository; import com.staccato.travel.repository.TravelRepository; import com.staccato.travel.service.dto.request.TravelRequest; +import com.staccato.travel.service.dto.response.TravelResponses; import lombok.RequiredArgsConstructor; @@ -18,7 +22,7 @@ @Transactional(readOnly = true) public class TravelService { private final TravelRepository travelRepository; - private final TravelMemberRepostiory travelMemberRepostiory; + private final TravelMemberRepository travelMemberRepository; private final MemberRepository memberRepository; @Transactional @@ -28,17 +32,40 @@ public long createTravel(TravelRequest travelRequest, Long memberId) { return travel.getId(); } - private TravelMember saveTravelMember(Long memberId, Travel travel) { + private void saveTravelMember(Long memberId, Travel travel) { Member member = getMemberById(memberId); TravelMember mate = TravelMember.builder() .travel(travel) .member(member) .build(); - return travelMemberRepostiory.save(mate); + travelMemberRepository.save(mate); } private Member getMemberById(long memberId) { return memberRepository.findById(memberId) .orElseThrow(() -> new IllegalArgumentException("Invalid Operation")); } + + public TravelResponses readAllTravels(long memberId, Integer year) { + return Optional.ofNullable(year) + .map(y -> readAllByYear(memberId, y)) + .orElseGet(() -> readAll(memberId)); + } + + private TravelResponses readAll(long memberId) { + List travelMembers = travelMemberRepository.findAllByMemberId(memberId); + return getTravelDetailResponses(travelMembers); + } + + private TravelResponses readAllByYear(long memberId, Integer year) { + List travelMembers = travelMemberRepository.findAllByMemberIdAndTravelStartAtYear(memberId, year); + return getTravelDetailResponses(travelMembers); + } + + private TravelResponses getTravelDetailResponses(List travelMembers) { + List travels = travelMembers.stream() + .map(TravelMember::getTravel) + .toList(); + return TravelResponses.from(travels); + } } diff --git a/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponse.java b/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponse.java new file mode 100644 index 000000000..2c3b54e11 --- /dev/null +++ b/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponse.java @@ -0,0 +1,28 @@ +package com.staccato.travel.service.dto.response; + +import java.time.LocalDate; + +import com.staccato.member.service.dto.response.MemberResponses; +import com.staccato.travel.domain.Travel; + +public record TravelResponse( + Long travelId, + String travelThumbnail, + String travelTitle, + String description, + LocalDate startAt, + LocalDate endAt, + MemberResponses mates +) { + public TravelResponse(Travel travel) { + this( + travel.getId(), + travel.getThumbnailUrl(), + travel.getTitle(), + travel.getDescription(), + travel.getStartAt(), + travel.getEndAt(), + MemberResponses.from(travel.getMates()) + ); + } +} diff --git a/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponses.java b/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponses.java new file mode 100644 index 000000000..a4ddf1982 --- /dev/null +++ b/backend/src/main/java/com/staccato/travel/service/dto/response/TravelResponses.java @@ -0,0 +1,15 @@ +package com.staccato.travel.service.dto.response; + +import java.util.List; + +import com.staccato.travel.domain.Travel; + +public record TravelResponses( + List travels +) { + public static TravelResponses from(List travels) { + return new TravelResponses(travels.stream() + .map(TravelResponse::new) + .toList()); + } +} diff --git a/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java b/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java index 358d45fe9..42c698037 100644 --- a/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java +++ b/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -101,4 +103,63 @@ void failCreateTravel(TravelRequest travelRequest, String expectedMessage) { .body("message", is(expectedMessage)) .body("status", is(HttpStatus.BAD_REQUEST.toString())); } + + @DisplayName("사용자의 모든 여행 상세 목록을 조회한다.") + @TestFactory + Stream findAllTravels() { + return Stream.of( + createTravel(2024), + createTravel(2024), + DynamicTest.dynamicTest("사용자가 타임라인을 조회하면 2개의 여행 목록이 조회된다.", () -> + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, USER_AUTHORIZATION) + .when().log().all() + .get("/travels") + .then().log().all() + .assertThat().statusCode(HttpStatus.OK.value()) + .body("travels.size()", is(2))) + ); + } + + @DisplayName("사용자가 2023년도에 다녀온 모든 여행 상세 목록을 조회한다.") + @TestFactory + Stream findAllTravelsOn2023() { + return Stream.of( + createTravel(2023), + createTravel(2024), + DynamicTest.dynamicTest("사용자가 타임라인에서 2023년도를 선택하면 1개의 여행 목록이 조회된다.", () -> + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, USER_AUTHORIZATION) + .param("year", 2023) + .when().log().all() + .get("/travels") + .then().log().all() + .assertThat().statusCode(HttpStatus.OK.value()) + .body("size()", is(1))) + ); + } + + private DynamicTest createTravel(int year) { + return DynamicTest.dynamicTest("사용자가 새로운 여행 상세를 추가한다.", () -> + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, USER_AUTHORIZATION) + .body(createTravelRequest(year)) + .when().log().all() + .post("/travels") + .then().log().all() + .assertThat().statusCode(HttpStatus.CREATED.value()) + .header(HttpHeaders.LOCATION, containsString("/travels/"))); + } + + private TravelRequest createTravelRequest(int year) { + return new TravelRequest( + "https://example.com/travels/geumohrm.jpg", + year + "여름 휴가", + "친구들과 함께한 여름 휴가 여행", + LocalDate.of(year, 7, 1), + LocalDate.of(year, 7, 10)); + } } diff --git a/backend/src/test/java/com/staccato/travel/repository/TravelMemberRepositoryTest.java b/backend/src/test/java/com/staccato/travel/repository/TravelMemberRepositoryTest.java new file mode 100644 index 000000000..2001bdeec --- /dev/null +++ b/backend/src/test/java/com/staccato/travel/repository/TravelMemberRepositoryTest.java @@ -0,0 +1,53 @@ +package com.staccato.travel.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.staccato.member.domain.Member; +import com.staccato.member.repository.MemberRepository; +import com.staccato.travel.domain.Travel; +import com.staccato.travel.domain.TravelMember; + +@DataJpaTest +class TravelMemberRepositoryTest { + @Autowired + private TravelMemberRepository travelMemberRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private TravelRepository travelRepository; + + @DisplayName("사용자 식별자와 년도로 여행 상세 목록을 조회한다.") + @Test + void findAllByMemberIdAndTravelStartAtYear() { + // given + Member member = memberRepository.save(Member.builder().nickname("staccato").build()); + Travel travel = travelRepository.save(createTravel(LocalDate.of(2023, 7, 1), LocalDate.of(2023, 7, 10))); + Travel travel2 = travelRepository.save(createTravel(LocalDate.of(2023, 12, 31), LocalDate.of(2024, 1, 10))); + Travel travel3 = travelRepository.save(createTravel(LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 10))); + travelMemberRepository.save(new TravelMember(member, travel)); + travelMemberRepository.save(new TravelMember(member, travel2)); + travelMemberRepository.save(new TravelMember(member, travel3)); + + // when + List result = travelMemberRepository.findAllByMemberIdAndTravelStartAtYear(member.getId(), 2023); + + // then + assertThat(result).hasSize(2); + } + + private static Travel createTravel(LocalDate startAt, LocalDate endAt) { + return Travel.builder() + .title("여행") + .startAt(startAt) + .endAt(endAt) + .build(); + } +} diff --git a/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java b/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java index cdf16d399..bc1d426ad 100644 --- a/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java +++ b/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java @@ -1,20 +1,25 @@ package com.staccato.travel.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDate; +import java.util.stream.Stream; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import com.staccato.ServiceSliceTest; import com.staccato.member.domain.Member; import com.staccato.member.repository.MemberRepository; import com.staccato.travel.domain.TravelMember; -import com.staccato.travel.repository.TravelMemberRepostiory; +import com.staccato.travel.repository.TravelMemberRepository; import com.staccato.travel.service.dto.request.TravelRequest; +import com.staccato.travel.service.dto.response.TravelResponses; class TravelServiceTest extends ServiceSliceTest { @Autowired @@ -22,29 +27,56 @@ class TravelServiceTest extends ServiceSliceTest { @Autowired private MemberRepository memberRepository; @Autowired - private TravelMemberRepostiory travelMemberRepostiory; + private TravelMemberRepository travelMemberRepository; + + static Stream yearProvider() { + return Stream.of( + Arguments.of(null, 2), + Arguments.of(2023, 1) + ); + } @DisplayName("여행 상세 정보를 기반으로, 여행 상세를 생성하고 작성자를 저장한다.") @Test void createTravel() { // given - TravelRequest travelRequest = new TravelRequest( - "https://example.com/travels/geumohrm.jpg", - "2023 여름 휴가", - "친구들과 함께한 여름 휴가 여행", - LocalDate.of(2023, 7, 1), - LocalDate.of(2023, 7, 10) - ); + TravelRequest travelRequest = createTravelRequest(2024); Member member = memberRepository.save(Member.builder().nickname("staccato").build()); // when long travelId = travelService.createTravel(travelRequest, member.getId()); - TravelMember travelMember = travelMemberRepostiory.findAll().get(0); + TravelMember travelMember = travelMemberRepository.findAll().get(0); // then assertAll( - () -> Assertions.assertThat(travelMember.getMember().getId()).isEqualTo(member.getId()), - () -> Assertions.assertThat(travelMember.getTravel().getId()).isEqualTo(travelId) + () -> assertThat(travelMember.getMember().getId()).isEqualTo(member.getId()), + () -> assertThat(travelMember.getTravel().getId()).isEqualTo(travelId) + ); + } + + @DisplayName("조건에 따라 여행 상세 목록을 조회한다.") + @MethodSource("yearProvider") + @ParameterizedTest + void readAllTravels(Integer year, int expectedSize) { + // given + Member member = memberRepository.save(Member.builder().nickname("staccato").build()); + travelService.createTravel(createTravelRequest(2023), member.getId()); + travelService.createTravel(createTravelRequest(2024), member.getId()); + + // when + TravelResponses travelResponses = travelService.readAllTravels(member.getId(), year); + + // then + assertThat(travelResponses.travels()).hasSize(expectedSize); + } + + private static TravelRequest createTravelRequest(int year) { + return new TravelRequest( + "https://example.com/travels/geumohrm.jpg", + year + " 여름 휴가", + "친구들과 함께한 여름 휴가 여행", + LocalDate.of(year, 7, 1), + LocalDate.of(2024, 7, 10) ); } }