Skip to content

Commit

Permalink
[Feature] - 여행 계획 작성 API 구현 (#50)
Browse files Browse the repository at this point in the history
* feat: TravelPlan 관련 Entity 추가

* feat: 여행기 작성 API 구현

* feat: swagger 설명 추가

* feat: 여행 계획 작성 시 지난 날짜에 대한 검증 추가

* refactor: TravelPlanService method 분리

* refactor: PlanPlaceRequest에 toPlace 추가

* style: TravelPlanControllere 개행 정리
  • Loading branch information
eunjungL authored Jul 18, 2024
1 parent 138681a commit a12eb33
Show file tree
Hide file tree
Showing 17 changed files with 376 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package woowacourse.touroot.global.exception;

public class BadRequestException extends RuntimeException {

public BadRequestException(String message) {
super(message);
}
}
15 changes: 10 additions & 5 deletions backend/src/main/java/woowacourse/touroot/place/domain/Place.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package woowacourse.touroot.place.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class Place extends BaseEntity {

Expand All @@ -26,4 +27,8 @@ public class Place extends BaseEntity {
private String longitude;

private String googlePlaceId;

public Place(String name, String latitude, String longitude) {
this(null, name, latitude, longitude, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.place.domain.Place;

import java.util.Optional;

public interface PlaceRepository extends JpaRepository<Place, Long> {

Optional<Place> findByNameAndLatitudeAndLongitude(String name, String lat, String lng);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package woowacourse.touroot.travelplan.controller;

import io.swagger.v3.oas.annotations.Operation;
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 woowacourse.touroot.travelplan.service.TravelPlanService;

@Tag(name = "여행기")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/travel-plans")
public class TravelPlanController {

private final TravelPlanService travelPlanService;

@Operation(summary = "여행기 생성")
@PostMapping
public ResponseEntity<TravelPlanCreateResponse> createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) {
TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request);
return ResponseEntity.ok()
.body(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;
import woowacourse.touroot.global.exception.BadRequestException;

import java.time.LocalDate;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlan extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false)
private LocalDate startDate;

@OneToMany(mappedBy = "plan")
private List<TravelPlanDay> days;

public TravelPlan(String title, LocalDate startDate) {
this(null, title, startDate, null);
}

public void validateStartDate() {
if (startDate.isBefore(LocalDate.now())) {
throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;

import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlanDay extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "plan_day_order", nullable = false)
Integer order;

@JoinColumn(name = "plan_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private TravelPlan plan;

@OneToMany(mappedBy = "day")
private List<TravelPlanPlace> places;

public TravelPlanDay(int order, TravelPlan plan) {
this(null, order, plan, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;
import woowacourse.touroot.place.domain.Place;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlanPlace extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String description;

@Column(name = "plan_place_order", nullable = false)
private Integer order;

@JoinColumn(name = "plan_day_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private TravelPlanDay day;

@JoinColumn(name = "place_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Place place;

public TravelPlanPlace(String description, int order, TravelPlanDay day, Place place) {
this(null, description, order, day, place);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import woowacourse.touroot.travelplan.domain.TravelPlan;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;

import java.util.List;

public record PlanDayCreateRequest(
@Schema(description = "여행 계획 날짜", example = "1")
@NotNull(message = "날짜는 비어있을 수 없습니다.")
@Min(value = 0, message = "날짜는 1 이상이어야 합니다.")
int day,
@Schema(description = "여행 장소 정보")
@NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") List<PlanPlaceCreateRequest> places
) {

public TravelPlanDay toPlanDay(TravelPlan plan) {
return new TravelPlanDay(day, plan);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record PlanLocationCreateRequest(
@Schema(description = "여행 장소 위도", example = "37.5175896")
@NotNull(message = "위도는 비어있을 수 없습니다.")
String lat,
@Schema(description = "여행 장소 경도", example = "127.0867236")
@NotNull(message = "경도는 비어있을 수 없습니다.")
String lng
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import woowacourse.touroot.place.domain.Place;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;

public record PlanPlaceCreateRequest(
@Schema(description = "여행 장소 이름", example = "신나는 여행 장소")
@NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName,
@Schema(description = "여행 장소 설명", example = "잠실한강공원")
String description,
@Schema(description = "여행 장소 순서", example = "1")
@NotNull
@Min(value = 0, message = "순서는 1 이상이어야 합니다.")
int order,
@NotNull PlanLocationCreateRequest location
) {

public TravelPlanPlace toPlanPlace(TravelPlanDay day, Place place) {
return new TravelPlanPlace(description, order, day, place);
}

public Place toPlace() {
return new Place(placeName, location.lat(), location.lng());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import woowacourse.touroot.travelplan.domain.TravelPlan;

import java.time.LocalDate;
import java.util.List;

public record TravelPlanCreateRequest(
@Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행")
@NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.")
String title,
@Schema(description = "여행 계획 시작일", example = "2024-11-16")
@NotNull(message = "시작일은 비어있을 수 없습니다.")
LocalDate startDate,
@Schema(description = "여행 날짜 정보")
@NotNull(message = "여행 날짜 정보는 비어있을 수 없습니다.")
List<PlanDayCreateRequest> days
) {

public TravelPlan toTravelPlan() {
return new TravelPlan(title, startDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;

public record TravelPlanCreateResponse(
@Schema(description = "생성된 여행 계획 id")
Long id
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;

public interface TravelPlanDayRepository extends JpaRepository<TravelPlanDay, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;

public interface TravelPlanPlaceRepository extends JpaRepository<TravelPlanPlace, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlan;

public interface TravelPlanRepository extends JpaRepository<TravelPlan, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package woowacourse.touroot.travelplan.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.repository.TravelPlanDayRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanRepository;

import java.util.List;

@RequiredArgsConstructor
@Service
public class TravelPlanService {

private final TravelPlanRepository travelPlanRepository;
private final TravelPlanDayRepository travelPlanDayRepository;
private final TravelPlanPlaceRepository travelPlanPlaceRepository;
private final PlaceRepository placeRepository;

@Transactional
public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request) {
TravelPlan travelPlan = request.toTravelPlan();
travelPlan.validateStartDate();

TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan);
createPlanDay(request, savedTravelPlan);

return new TravelPlanCreateResponse(savedTravelPlan.getId());
}

private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) {
for (PlanDayCreateRequest dayRequest : request.days()) {
TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan));
createPlanPlace(dayRequest.places(), travelPlanDay);
}
}

private void createPlanPlace(List<PlanPlaceCreateRequest> request, TravelPlanDay travelPlanDay) {
for (PlanPlaceCreateRequest planRequest : request) {
Place place = getPlace(planRequest);
travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place));
}
}

private Place getPlace(PlanPlaceCreateRequest planRequest) {
return placeRepository.findByNameAndLatitudeAndLongitude(
planRequest.placeName(),
planRequest.location().lat(),
planRequest.location().lng()
).orElseGet(() -> placeRepository.save(planRequest.toPlace()));
}
}
Loading

0 comments on commit a12eb33

Please sign in to comment.