diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..e8612f41 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,20 @@ +package woowacourse.touroot.global.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException exception) { + log.info("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage()); + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); + return ResponseEntity.badRequest() + .body(data); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java b/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java new file mode 100644 index 00000000..8418e1c5 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java @@ -0,0 +1,4 @@ +package woowacourse.touroot.global.exception.dto; + +public record ExceptionResponse(String message) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java index de03821f..9b5ad32a 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java @@ -1,19 +1,22 @@ package woowacourse.touroot.travelplan.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -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.RestController; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import org.springframework.web.bind.annotation.*; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; import woowacourse.touroot.travelplan.service.TravelPlanService; -@Tag(name = "여행기") +@Tag(name = "여행 계획") @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/travel-plans") @@ -21,11 +24,39 @@ public class TravelPlanController { private final TravelPlanService travelPlanService; - @Operation(summary = "여행기 생성") + @Operation( + summary = "여행 계획 생성", + responses = { + @ApiResponse( + responseCode = "400", + description = "Body에 유효하지 않은 값이 존재하거나 지난 날짜에 대한 계획을 생성할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + } + ) @PostMapping - public ResponseEntity createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) { + public ResponseEntity createTravelPlan( + @Valid @RequestBody TravelPlanCreateRequest request + ) { TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request); - return ResponseEntity.ok() - .body(data); + return ResponseEntity.ok(data); + } + + @Operation( + summary = "여행 계획 상세 조회", + responses = { + @ApiResponse( + responseCode = "400", + description = "존재하지 않은 여행 계획을 조회할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + } + ) + @GetMapping("/{id}") + public ResponseEntity readTravelPlan( + @Parameter(description = "여행 계획 id") @PathVariable Long id + ) { + TravelPlanResponse data = travelPlanService.readTravelPlan(id); + return ResponseEntity.ok(data); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java index fe08d0d3..a5c909b0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; +import java.time.LocalDate; import java.util.List; @Getter @@ -32,4 +33,9 @@ public class TravelPlanDay extends BaseEntity { public TravelPlanDay(int order, TravelPlan plan) { this(null, order, plan, null); } + + public LocalDate getCurrentDate() { + LocalDate startDate = plan.getStartDate(); + return startDate.plusDays(order); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java similarity index 94% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java index 99aedbca..12aa0ed3 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java similarity index 90% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java index 5ba1bd42..ddac4d0f 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java similarity index 93% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 182cf88b..08cc8dd6 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -1,17 +1,19 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.travelplan.domain.TravelPlanDay; import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +@Builder public record PlanPlaceCreateRequest( - @Schema(description = "여행 장소 이름", example = "신나는 여행 장소") + @Schema(description = "여행 장소 이름", example = "잠실한강공원") @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, - @Schema(description = "여행 장소 설명", example = "잠실한강공원") + @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description, @Schema(description = "여행 장소 순서", example = "1") @NotNull diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index e88e2fce..ab988ebb 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -1,13 +1,15 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import woowacourse.touroot.travelplan.domain.TravelPlan; import java.time.LocalDate; import java.util.List; +@Builder public record TravelPlanCreateRequest( @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") @NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java index 26b147be..66c1cd08 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java new file mode 100644 index 00000000..62493e38 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record TravelPlanDayResponse( + @Schema(description = "여행 일자") LocalDate date, + @Schema(description = "여행 장소별 정보") List places +) { + + public static TravelPlanDayResponse of( + TravelPlanDay planDay, + List places + ) { + return TravelPlanDayResponse.builder() + .date(planDay.getCurrentDate()) + .places(places) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java new file mode 100644 index 00000000..0824ecb8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java @@ -0,0 +1,19 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.place.domain.Place; + +@Builder +public record TravelPlanLocationResponse( + @Schema(description = "여행 장소 위도") String lat, + @Schema(description = "여행 계획 경도") String lng +) { + + public static TravelPlanLocationResponse from(Place place) { + return TravelPlanLocationResponse.builder() + .lat(place.getLatitude()) + .lng(place.getLongitude()) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java new file mode 100644 index 00000000..118e67cb --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; + +@Builder +public record TravelPlanPlaceResponse( + @Schema(description = "여행 장소 이름") String placeName, + @Schema(description = "여행 장소 위치") TravelPlanLocationResponse location, + @Schema(description = "여행 장소 설명") String description +) { + + public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { + Place place = planPlace.getPlace(); + TravelPlanLocationResponse locationResponse = TravelPlanLocationResponse.from(place); + + return TravelPlanPlaceResponse.builder() + .placeName(place.getName()) + .location(locationResponse) + .description(planPlace.getDescription()) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java new file mode 100644 index 00000000..ecc6d580 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java @@ -0,0 +1,26 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.travelplan.domain.TravelPlan; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record TravelPlanResponse( + @Schema(description = "여행 계획 id") Long id, + @Schema(description = "여행 계획 제목") String title, + @Schema(description = "여행 시작일") LocalDate startDate, + @Schema(description = "여행 계획 날짜별 정보") List days +) { + + public static TravelPlanResponse of(TravelPlan travelPlan, List days) { + return TravelPlanResponse.builder() + .id(travelPlan.getId()) + .title(travelPlan.getTitle()) + .startDate(travelPlan.getStartDate()) + .days(days) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java index f962ea03..e4ef1623 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java @@ -3,18 +3,24 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.dto.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.PlanPlaceCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanDayResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanPlaceResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; import woowacourse.touroot.travelplan.repository.TravelPlanRepository; +import java.util.Comparator; import java.util.List; @RequiredArgsConstructor @@ -39,6 +45,7 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) { for (PlanDayCreateRequest dayRequest : request.days()) { + // TODO: order는 배열 index로 변경 TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan)); createPlanPlace(dayRequest.places(), travelPlanDay); } @@ -46,6 +53,7 @@ private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTrav private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { for (PlanPlaceCreateRequest planRequest : request) { + // TODO: order는 배열 index로 변경 Place place = getPlace(planRequest); travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place)); } @@ -58,4 +66,29 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) { planRequest.location().lng() ).orElseGet(() -> placeRepository.save(planRequest.toPlace())); } + + @Transactional(readOnly = true) + public TravelPlanResponse readTravelPlan(Long planId) { + TravelPlan travelPlan = getTravelPlanById(planId); + return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + } + + private TravelPlan getTravelPlanById(Long planId) { + return travelPlanRepository.findById(planId) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); + } + + private List getTravelPlanDayResponses(TravelPlan travelPlan) { + return travelPlan.getDays().stream() + .sorted(Comparator.comparing(TravelPlanDay::getOrder)) + .map(day -> TravelPlanDayResponse.of(day, getTravelPlanPlaceResponses(day))) + .toList(); + } + + private List getTravelPlanPlaceResponses(TravelPlanDay day) { + return day.getPlaces().stream() + .sorted(Comparator.comparing(TravelPlanPlace::getOrder)) + .map(TravelPlanPlaceResponse::from) + .toList(); + } } diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java new file mode 100644 index 00000000..9504e570 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -0,0 +1,134 @@ +package woowacourse.touroot.travelplan.controller; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.server.LocalServerPort; +import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.utils.DatabaseCleaner; +import woowacourse.touroot.utils.TestFixture; + +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.Matchers.is; + +@DisplayName("여행 계획 컨트롤러") +@AcceptanceTest +class TravelPlanControllerTest { + + @LocalServerPort + private int port; + private final DatabaseCleaner databaseCleaner; + private final TestFixture testFixture; + + @Autowired + public TravelPlanControllerTest(DatabaseCleaner databaseCleaner, TestFixture testFixture) { + this.databaseCleaner = databaseCleaner; + this.testFixture = testFixture; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행 계획 컨트롤러는 생성 요청이 들어올 때 200을 응답한다.") + @Test + void createTravelPlan() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/api/v1/travel-plans") + .then().log().all() + .statusCode(200) + .body("id", is(1)); + } + + @DisplayName("여행 계획 컨트롤러는 지난 날짜로 생성 요청이 들어올 때 400을 응답한다.") + @Test + void createTravelPlanWithInvalidStartDate() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MIN) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/api/v1/travel-plans") + .then().log().all() + .statusCode(400) + .body("message", is("지난 날짜에 대한 계획은 작성할 수 없습니다.")); + } + + @DisplayName("여행 계획 컨트롤러는 상세 조회 요청이 들어오면 200을 응답한다.") + @Test + void readTravelPlan() { + // given + testFixture.initTravelPlanTestData(); + long id = 1L; + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().log().all() + .get("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(200) + .body("id", is(1)); + } + + @DisplayName("여행 계획 컨트롤러는 존재하지 않는 상세 조회 요청이 들어오면 400을 응답한다.") + @Test + void readTravelPlanWithNonExist() { + // given + long id = 1L; + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().log().all() + .get("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행 계획입니다.")); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java new file mode 100644 index 00000000..25bba77e --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java @@ -0,0 +1,121 @@ +package woowacourse.touroot.travelplan.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; +import woowacourse.touroot.utils.DatabaseCleaner; +import woowacourse.touroot.utils.TestFixture; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("여행 계획 서비스") +@ActiveProfiles("test") +// TODO: 양방향 해결 후 @DataJpaTest로 변경 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +class TravelPlanServiceTest { + + private final TravelPlanService travelPlanService; + private final DatabaseCleaner databaseCleaner; + private final TestFixture testFixture; + + @Autowired + public TravelPlanServiceTest( + TravelPlanService travelPlanService, + DatabaseCleaner databaseCleaner, + TestFixture testFixture + ) { + this.travelPlanService = travelPlanService; + this.databaseCleaner = databaseCleaner; + this.testFixture = testFixture; + } + + @DisplayName("여행 계획 서비스는 여행 계획 생성 시 생성된 id를 응답한다.") + @Test + void createTravelPlan() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayCreateRequest)) + .build(); + + // when + TravelPlanCreateResponse actual = travelPlanService.createTravelPlan(request); + + // then + assertThat(actual.id()).isEqualTo(1L); + } + + @DisplayName("여행 계획 서비스는 지난 날짜로 여행 계획 생성 시 예외를 반환한다.") + @Test + void createTravelPlanWithInvalidStartDate() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MIN) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then= + assertThatThrownBy(() -> travelPlanService.createTravelPlan(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } + + @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") + @Test + void readTravelPlan() { + // given + databaseCleaner.executeTruncate(); + testFixture.initTravelPlanTestData(); + Long id = 1L; + + // when + TravelPlanResponse actual = travelPlanService.readTravelPlan(id); + + // then + assertThat(actual.id()).isEqualTo(id); + } + + @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") + @Test + void readTravelPlanWitNonExist() { + // given + databaseCleaner.executeTruncate(); + Long id = 1L; + + // when & then + assertThatThrownBy(() -> travelPlanService.readTravelPlan(id)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행 계획입니다."); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java index 32a27c68..98625352 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -13,6 +13,14 @@ import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; import woowacourse.touroot.travelogue.repository.TravelogueRepository; +import woowacourse.touroot.travelplan.domain.TravelPlan; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanRepository; + +import java.time.LocalDate; @Component @Profile("test") @@ -33,6 +41,14 @@ public class TestFixture { @Autowired private PlaceRepository placeRepository; + @Autowired + private TravelPlanRepository travelPlanRepository; + @Autowired + private TravelPlanDayRepository travelPlanDayRepository; + @Autowired + private TravelPlanPlaceRepository travelPlanPlaceRepository; + + public static Travelogue getTravelogue(String name, String thumbnail) { return new Travelogue(name, thumbnail); } @@ -54,6 +70,18 @@ public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, Trav return new TraveloguePhoto(key, order, traveloguePlace); } + public static TravelPlan getTravelPlan(String title, LocalDate startDate) { + return new TravelPlan(title, startDate); + } + + public static TravelPlanDay getTravelPlanDay(int order, TravelPlan travelPlan) { + return new TravelPlanDay(order, travelPlan); + } + + public static TravelPlanPlace getTravelPlanPlace(String description, int order, Place place, TravelPlanDay day) { + return new TravelPlanPlace(description, order, day, place); + } + public void initTravelogueTestData() { Travelogue travelogue = getTravelogue("여행기 1", "썸네일.png"); TravelogueDay travelogueDay = getTravelogueDay(1, travelogue); @@ -67,4 +95,16 @@ public void initTravelogueTestData() { traveloguePlaceRepository.save(traveloguePlace); traveloguePhotoRepository.save(traveloguePhoto); } + + public void initTravelPlanTestData() { + TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX); + TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); + Place place = getPlace("장소", "37.5175896", "127.0867236", ""); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); + + travelPlanRepository.save(travelPlan); + travelPlanDayRepository.save(travelPlanDay); + placeRepository.save(place); + travelPlanPlaceRepository.save(travelPlanPlace); + } }