diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/controller/TripPlanController.java b/src/main/java/com/example/triptalk/domain/tripPlan/controller/TripPlanController.java index e8f09cc..2665eba 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/controller/TripPlanController.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/controller/TripPlanController.java @@ -58,4 +58,31 @@ public ApiResponse markTripPlanAsTraveled( TripPlanResponse.TripPlanStatusDTO response = tripPlanService.changeTripPlanStatusToTraveled(tripPlanId, userId); return ApiResponse.onSuccess(response); } + + @PostMapping("/from-fastapi") + @Operation( + summary = "FastAPI 생성 여행 계획 저장", + description = """ + **FastAPI에서 생성된 여행 계획을 DB에 저장합니다.** + + ### 📝 저장 데이터 + - 여행 기본 정보 (제목, 출발지, 목적지, 날짜, 예산 등) + - 하이라이트 목록 + - 일별 상세 일정 (DailySchedule + ScheduleItem) + - 교통편 정보 (출발편, 귀환편) + - 숙소 정보 + + ### 🔐 인증 + - Authorization 헤더에 Bearer 토큰 필요 + - 로그인한 사용자의 여행 계획으로 저장 + """ + ) + public ApiResponse createTripPlanFromFastAPI( + @RequestBody com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.CreateFromFastAPIDTO request, + HttpServletRequest httpRequest + ) { + Long userId = authUtil.getUserIdFromRequest(httpRequest); + TripPlanResponse.TripPlanDTO response = tripPlanService.createTripPlanFromFastAPI(userId, request); + return ApiResponse.onSuccess(response); + } } diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java b/src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java index a3c3693..1dabf70 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java @@ -51,7 +51,7 @@ private static List toTransportationDTO(List .map(t -> TripPlanResponse.TransportationDTO.builder() .origin(t.getOrigin()) .destination(t.getDestination()) - .name(t.getName()) + .name(t.getAirlineName()) .price(t.getPrice()) .build()) .toList(); @@ -144,4 +144,150 @@ public static TripPlanResponse.TripPlanStatusDTO toTripPlanStatusDTO(TripPlan tr .status(tripPlan.getStatus().name()) .build(); } + + // ========== FastAPI 데이터 변환 메서드 ========== + + /** + * FastAPI 요청 DTO를 TripPlan 엔티티로 변환 + */ + public static TripPlan toTripPlanEntity( + com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.CreateFromFastAPIDTO request, + com.example.triptalk.domain.user.entity.User user + ) { + // TravelStyles 한글 문자열을 Enum으로 변환 + Set travelStyleSet = new HashSet<>(); + if (request.getTravelStyles() != null) { + for (String styleStr : request.getTravelStyles()) { + TravelStyle style = mapKoreanToTravelStyle(styleStr); + if (style != null) { + travelStyleSet.add(style); + } + } + } + + return TripPlan.builder() + .title(request.getTitle()) + .destination(request.getDestination()) + .departure(request.getDeparture()) + .startDate(request.getStartDate()) + .endDate(request.getEndDate()) + .companions(request.getCompanions()) + .budget(request.getBudget()) + .travelStyles(travelStyleSet) + .imgUrl("https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=800") + .status(com.example.triptalk.domain.tripPlan.enums.TripStatus.PLANNED) + .user(user) + .build(); + } + + /** + * 한글 문자열을 TravelStyle Enum으로 매핑 + */ + private static TravelStyle mapKoreanToTravelStyle(String korean) { + if (korean == null) { + return null; + } + + return switch (korean.trim()) { + case "체험·액티비티" -> TravelStyle.ACTIVITY; + case "자연과 함께" -> TravelStyle.NATURE; + case "여유롭게 힐링" -> TravelStyle.HEALING; + case "여행지 느낌 물씬" -> TravelStyle.LOCAL_VIBE; + case "관광보다 먹방" -> TravelStyle.FOOD_FOCUS; + case "SNS 핫플레이스" -> TravelStyle.HOTPLACE; + case "유명 관광지는 필수" -> TravelStyle.MUST_VISIT; + case "문화·예술·역사" -> TravelStyle.CULTURE; + case "쇼핑은 열정적으로" -> TravelStyle.SHOPPING; + default -> null; // 매칭되지 않는 스타일은 무시 + }; + } + + + /** + * FastAPI 하이라이트 리스트를 TripHighlight 엔티티 리스트로 변환 + */ + public static List toTripHighlightEntities( + List highlights, + TripPlan tripPlan + ) { + if (highlights == null) { + return List.of(); + } + return highlights.stream() + .map(content -> TripHighlight.builder() + .content(content) + .tripPlan(tripPlan) + .build()) + .toList(); + } + + /** + * FastAPI 교통편 DTO를 TripTransportation 엔티티로 변환 + */ + public static TripTransportation toTripTransportationEntity( + com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.TransportationDTO dto, + TripPlan tripPlan + ) { + if (dto == null) { + return null; + } + return TripTransportation.builder() + .origin(dto.getOrigin()) + .destination(dto.getDestination()) + .airlineName(dto.getName()) + .airlineName(dto.getName()) // DB 호환을 위해 name 필드에도 동일한 값 저장 + .price(dto.getPrice()) + .tripPlan(tripPlan) + .build(); + } + + /** + * FastAPI 숙소 리스트를 TripAccommodation 엔티티 리스트로 변환 + */ + public static List toTripAccommodationEntities( + List accommodations, + TripPlan tripPlan + ) { + if (accommodations == null) { + return List.of(); + } + return accommodations.stream() + .map(dto -> TripAccommodation.builder() + .name(dto.getName()) + .address(dto.getAddress()) + .pricePerNight(dto.getPricePerNight()) + .tripPlan(tripPlan) + .build()) + .toList(); + } + + /** + * FastAPI 일별 일정 DTO를 DailySchedule 엔티티로 변환 + */ + public static DailySchedule toDailyScheduleEntity( + com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.DailyScheduleDTO dto, + TripPlan tripPlan + ) { + DailySchedule dailySchedule = DailySchedule.builder() + .day(dto.getDay()) + .date(dto.getDate()) + .tripPlan(tripPlan) + .build(); + + // ScheduleItems 추가 + if (dto.getSchedules() != null) { + for (com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.ScheduleDTO scheduleDTO : dto.getSchedules()) { + ScheduleItem scheduleItem = ScheduleItem.builder() + .orderIndex(scheduleDTO.getOrderIndex()) + .time(java.time.LocalTime.parse(scheduleDTO.getTime())) + .title(scheduleDTO.getTitle()) + .description(scheduleDTO.getDescription()) + .dailySchedule(dailySchedule) + .build(); + dailySchedule.getScheduleItems().add(scheduleItem); + } + } + + return dailySchedule; + } } diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/dto/TripPlanRequest.java b/src/main/java/com/example/triptalk/domain/tripPlan/dto/TripPlanRequest.java new file mode 100644 index 0000000..5911892 --- /dev/null +++ b/src/main/java/com/example/triptalk/domain/tripPlan/dto/TripPlanRequest.java @@ -0,0 +1,137 @@ +package com.example.triptalk.domain.tripPlan.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +public class TripPlanRequest { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "FastAPI에서 생성된 여행 계획 생성 요청") + public static class CreateFromFastAPIDTO { + + @Schema(description = "여행 제목", example = "제주 액티비티 탐험 5박 6일 여행") + private String title; + + @Schema(description = "목적지", example = "제주도") + private String destination; + + @Schema(description = "출발지", example = "서울") + private String departure; + + @Schema(description = "여행 시작일", example = "2025-12-10") + private LocalDate startDate; + + @Schema(description = "여행 종료일", example = "2025-12-15") + private LocalDate endDate; + + @Schema(description = "동행인", example = "친구") + private String companions; + + @Schema(description = "예산", example = "70만원") + private String budget; + + @Schema(description = "여행 스타일 리스트") + private List travelStyles; + + @Schema(description = "여행 하이라이트") + private List highlights; + + @Schema(description = "일별 일정") + private List dailySchedules; + + @Schema(description = "출발 교통편") + private TransportationDTO outboundTransportation; + + @Schema(description = "귀환 교통편") + private TransportationDTO returnTransportation; + + @Schema(description = "숙소 리스트") + private List accommodations; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "일별 일정") + public static class DailyScheduleDTO { + + @Schema(description = "일차", example = "1") + private Integer day; + + @Schema(description = "날짜", example = "2025-12-10") + private LocalDate date; + + @Schema(description = "일정 항목 리스트") + private List schedules; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "일정 항목") + public static class ScheduleDTO { + + @Schema(description = "순서", example = "1") + @JsonProperty("order_index") + private Integer orderIndex; + + @Schema(description = "시간", example = "07:30") + private String time; + + @Schema(description = "제목", example = "비행기 탑승") + private String title; + + @Schema(description = "설명", example = "김포 출발 제주행") + private String description; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "교통편 정보") + public static class TransportationDTO { + + @Schema(description = "출발지", example = "김포공항") + private String origin; + + @Schema(description = "도착지", example = "제주공항") + private String destination; + + @Schema(description = "교통편명", example = "진에어LJ313") + private String name; + + @Schema(description = "가격", example = "55000") + private Integer price; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "숙소 정보") + public static class AccommodationDTO { + + @Schema(description = "숙소명", example = "메종글래드 제주") + private String name; + + @Schema(description = "주소", example = "제주시 노연로 80") + private String address; + + @Schema(description = "1박 가격", example = "100000") + private Integer pricePerNight; + } +} + diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/entity/ScheduleItem.java b/src/main/java/com/example/triptalk/domain/tripPlan/entity/ScheduleItem.java index 6d64500..bbf9e1f 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/entity/ScheduleItem.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/entity/ScheduleItem.java @@ -21,10 +21,10 @@ public class ScheduleItem extends BaseEntity { @Column(nullable = false) private LocalTime time; - @Column(length = 10, nullable = false) + @Column(length = 50, nullable = false) private String title; - @Column(length = 20, nullable = false) + @Column(length = 100, nullable = false) private String description; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripAccommodation.java b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripAccommodation.java index 24c88b5..2824e12 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripAccommodation.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripAccommodation.java @@ -12,10 +12,10 @@ @Entity public class TripAccommodation extends BaseEntity { - @Column(length = 20, nullable = false) + @Column(length = 100, nullable = false) private String name; - @Column(length = 20, nullable = false) + @Column(length = 100, nullable = false) private String address; @Column(nullable = false) diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripHighlight.java b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripHighlight.java index d8e956d..db8c3ff 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripHighlight.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripHighlight.java @@ -12,7 +12,7 @@ @Entity public class TripHighlight extends BaseEntity { - @Column(length = 15, nullable = false) + @Column(length = 100, nullable = false) private String content; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripPlan.java b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripPlan.java index e05999c..134e697 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripPlan.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripPlan.java @@ -18,13 +18,13 @@ @Entity public class TripPlan extends BaseEntity { - @Column(length = 20, nullable = false) + @Column(length = 100, nullable = false) private String title; - @Column(length = 10, nullable = false) + @Column(length = 50, nullable = false) private String destination; - @Column(length = 10, nullable = false) + @Column(length = 50, nullable = false) private String departure; @Column(nullable = false) @@ -33,10 +33,10 @@ public class TripPlan extends BaseEntity { @Column(nullable = false) private LocalDate endDate; - @Column(length = 30, nullable = false) + @Column(length = 50, nullable = false) private String companions; - @Column(length = 10, nullable = false) + @Column(length = 30, nullable = false) private String budget; @ElementCollection(fetch = FetchType.LAZY) diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.java b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.java index 95c7f78..8d592ea 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.java @@ -12,14 +12,14 @@ @Entity public class TripTransportation extends BaseEntity { - @Column(length = 10, nullable = false) + @Column(length = 50, nullable = false) private String origin; - @Column(length = 10, nullable = false) + @Column(length = 50, nullable = false) private String destination; - @Column(length = 10, nullable = false) - private String name; + @Column(name = "airline_name", length = 50, nullable = false) + private String airlineName; @Column(nullable = false) private Integer price; diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanService.java b/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanService.java index a6f969d..9b2cebc 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanService.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanService.java @@ -1,5 +1,6 @@ package com.example.triptalk.domain.tripPlan.service; +import com.example.triptalk.domain.tripPlan.dto.TripPlanRequest; import com.example.triptalk.domain.tripPlan.dto.TripPlanResponse; import com.example.triptalk.domain.tripPlan.enums.TripStatus; @@ -13,4 +14,7 @@ public interface TripPlanService { // 여행 상태 변경: PLANNED -> TRAVELED TripPlanResponse.TripPlanStatusDTO changeTripPlanStatusToTraveled(Long tripPlanId, Long userId); + + // FastAPI에서 생성된 여행 계획 저장 + TripPlanResponse.TripPlanDTO createTripPlanFromFastAPI(Long userId, TripPlanRequest.CreateFromFastAPIDTO request); } diff --git a/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanServiceImpl.java b/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanServiceImpl.java index 07b9fce..2d75b6d 100644 --- a/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanServiceImpl.java +++ b/src/main/java/com/example/triptalk/domain/tripPlan/service/TripPlanServiceImpl.java @@ -2,7 +2,7 @@ import com.example.triptalk.domain.tripPlan.converter.TripPlanConverter; import com.example.triptalk.domain.tripPlan.dto.TripPlanResponse; -import com.example.triptalk.domain.tripPlan.entity.TripPlan; +import com.example.triptalk.domain.tripPlan.entity.*; import com.example.triptalk.domain.tripPlan.enums.TripStatus; import com.example.triptalk.domain.tripPlan.repository.*; import com.example.triptalk.global.apiPayload.code.status.ErrorStatus; @@ -25,6 +25,7 @@ public class TripPlanServiceImpl implements TripPlanService { private final TripAccommodationRepository tripAccommodationRepository; private final DailyScheduleRepository dailyScheduleRepository; private final TripHighlightRepository tripHighlightRepository; + private final com.example.triptalk.domain.user.repository.UserRepository userRepository; @Override public TripPlanResponse.TripPlanDTO getTripPlan(Long tripPlanId, Long userId) { @@ -93,4 +94,63 @@ public TripPlanResponse.TripPlanStatusDTO changeTripPlanStatusToTraveled(Long tr return TripPlanConverter.toTripPlanStatusDTO(tripPlan); } + + @Override + @Transactional + public TripPlanResponse.TripPlanDTO createTripPlanFromFastAPI(Long userId, com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.CreateFromFastAPIDTO request) { + // 1. User 조회 + com.example.triptalk.domain.user.entity.User user = userRepository.findById(userId) + .orElseThrow(() -> new ErrorHandler(ErrorStatus.USER_NOT_FOUND)); + + // 2. TripPlan 생성 (Converter 사용) + TripPlan tripPlan = TripPlanConverter.toTripPlanEntity(request, user); + tripPlan = tripPlanRepository.save(tripPlan); + + TripPlan finalTripPlan = tripPlan; + + // 3. Highlights 저장 (Converter 사용) + java.util.List highlights = TripPlanConverter.toTripHighlightEntities( + request.getHighlights(), + finalTripPlan + ); + tripHighlightRepository.saveAll(highlights); + + // 4. Transportations 저장 (Converter 사용) + TripTransportation outbound = TripPlanConverter.toTripTransportationEntity( + request.getOutboundTransportation(), + finalTripPlan + ); + if (outbound != null) { + tripTransportationRepository.save(outbound); + } + + TripTransportation returnTransport = TripPlanConverter.toTripTransportationEntity( + request.getReturnTransportation(), + finalTripPlan + ); + if (returnTransport != null) { + tripTransportationRepository.save(returnTransport); + } + + // 5. Accommodations 저장 (Converter 사용) + java.util.List accommodations = TripPlanConverter.toTripAccommodationEntities( + request.getAccommodations(), + finalTripPlan + ); + tripAccommodationRepository.saveAll(accommodations); + + // 6. DailySchedules 저장 (Converter 사용) + if (request.getDailySchedules() != null) { + for (com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.DailyScheduleDTO dailyScheduleDTO : request.getDailySchedules()) { + DailySchedule dailySchedule = TripPlanConverter.toDailyScheduleEntity( + dailyScheduleDTO, + finalTripPlan + ); + dailyScheduleRepository.save(dailySchedule); + } + } + + // 7. 저장된 데이터 조회 및 반환 + return getTripPlan(finalTripPlan.getId(), userId); + } }