Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,31 @@ public ApiResponse<TripPlanResponse.TripPlanStatusDTO> 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<TripPlanResponse.TripPlanDTO> 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);
Comment on lines +80 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

@Valid 어노테이션 누락 및 import 개선이 필요합니다.

요청 본문에 @Valid 어노테이션을 추가하여 DTO 필드 검증을 활성화하세요. 또한 완전한 클래스명 대신 import 문을 사용하면 가독성이 향상됩니다.

+import com.example.triptalk.domain.tripPlan.dto.TripPlanRequest;
+import jakarta.validation.Valid;
     public ApiResponse<TripPlanResponse.TripPlanDTO> createTripPlanFromFastAPI(
-            @RequestBody com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.CreateFromFastAPIDTO request,
+            @Valid @RequestBody TripPlanRequest.CreateFromFastAPIDTO request,
             HttpServletRequest httpRequest
     ) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/controller/TripPlanController.java
around lines 80 to 86, the controller method is missing the @Valid annotation on
the request body and uses a fully-qualified DTO type; add an import for the DTO
(com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.CreateFromFastAPIDTO)
and add an import for javax.validation.Valid (or jakarta.validation.Valid if
your project uses Jakarta), then change the method parameter to use the imported
type and annotate it with @Valid before @RequestBody (e.g. public
ApiResponse<...> createTripPlanFromFastAPI(@Valid @RequestBody
CreateFromFastAPIDTO request, HttpServletRequest httpRequest) { ... }) so bean
validation runs and the code is more readable.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static List<TripPlanResponse.TransportationDTO> toTransportationDTO(List
.map(t -> TripPlanResponse.TransportationDTO.builder()
.origin(t.getOrigin())
.destination(t.getDestination())
.name(t.getName())
.name(t.getAirlineName())
.price(t.getPrice())
.build())
.toList();
Expand Down Expand Up @@ -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<TravelStyle> 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<TripHighlight> toTripHighlightEntities(
List<String> 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();
Comment on lines +234 to +241
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "TripTransportation.java" -type f

Repository: TripTalk/BE_SpringBoot

Length of output: 149


🏁 Script executed:

cat -n ./src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.java

Repository: TripTalk/BE_SpringBoot

Length of output: 1007


🏁 Script executed:

sed -n '227,242p' ./src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java | cat -n

Repository: TripTalk/BE_SpringBoot

Length of output: 798


중복된 .airlineName() 호출을 제거하십시오.

lines 237-238에서 .airlineName(dto.getName())이 중복으로 호출되고 있습니다. TripTransportation 엔티티에는 airlineName 필드만 존재하고 name 필드가 없으므로, 중복된 호출은 불필요합니다. 한 줄을 삭제하여 다음과 같이 수정하세요:

 return TripTransportation.builder()
         .origin(dto.getOrigin())
         .destination(dto.getDestination())
         .airlineName(dto.getName())
-        .airlineName(dto.getName())  // DB 호환을 위해 name 필드에도 동일한 값 저장
         .price(dto.getPrice())
         .tripPlan(tripPlan)
         .build();
🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java
around lines 234 to 241, remove the duplicated .airlineName(dto.getName()) call
(lines 237-238) so the builder only sets airlineName once; keep the single
.airlineName(dto.getName()) and delete the redundant line to avoid unnecessary
duplicate assignment for the TripTransportation entity.

}

/**
* FastAPI 숙소 리스트를 TripAccommodation 엔티티 리스트로 변환
*/
public static List<TripAccommodation> toTripAccommodationEntities(
List<com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.AccommodationDTO> 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);
}
}
Comment on lines +277 to +289
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

LocalTime.parse()에 대한 예외 처리가 필요합니다.

Line 282에서 LocalTime.parse(scheduleDTO.getTime())을 호출할 때 예외 처리가 없습니다. time 값이 null이거나 잘못된 형식인 경우 DateTimeParseException이 발생하여 전체 여행 계획 저장이 실패할 수 있습니다.

다음 중 하나의 방법으로 수정을 권장합니다:

방법 1: 예외 처리 추가 (권장)

         // ScheduleItems 추가
         if (dto.getSchedules() != null) {
             for (com.example.triptalk.domain.tripPlan.dto.TripPlanRequest.ScheduleDTO scheduleDTO : dto.getSchedules()) {
+                if (scheduleDTO.getTime() == null) {
+                    throw new IllegalArgumentException("일정 시간은 필수입니다");
+                }
+                
+                java.time.LocalTime parsedTime;
+                try {
+                    parsedTime = java.time.LocalTime.parse(scheduleDTO.getTime());
+                } catch (java.time.format.DateTimeParseException e) {
+                    throw new IllegalArgumentException(
+                        "잘못된 시간 형식입니다: " + scheduleDTO.getTime() + " (예: 07:30)", e);
+                }
+                
                 ScheduleItem scheduleItem = ScheduleItem.builder()
                         .orderIndex(scheduleDTO.getOrderIndex())
-                        .time(java.time.LocalTime.parse(scheduleDTO.getTime()))
+                        .time(parsedTime)
                         .title(scheduleDTO.getTitle())
                         .description(scheduleDTO.getDescription())
                         .dailySchedule(dailySchedule)
                         .build();
                 dailySchedule.getScheduleItems().add(scheduleItem);
             }
         }

방법 2: DTO 레벨에서 검증

ScheduleDTO에 @NotBlank@Pattern 어노테이션 추가:

@Schema(description = "시간", example = "07:30")
@NotBlank(message = "일정 시간은 필수입니다")
@Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d$", message = "시간 형식은 HH:mm 이어야 합니다")
private String time;
🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/tripPlan/converter/TripPlanConverter.java
around lines 277 to 289, the call to LocalTime.parse(scheduleDTO.getTime()) can
throw DateTimeParseException or NPE for null/blank input; update the loop to
first validate scheduleDTO.getTime() is not null/blank and then perform parsing
inside a try-catch that catches DateTimeParseException (and optionally
NullPointerException), and on failure throw a clear validation exception (e.g.,
IllegalArgumentException or your app's BadRequestException) with a message
including the offending schedule's orderIndex and raw value so callers get a
meaningful error instead of an obscure parse crash; ensure the rest of the
ScheduleItem construction uses the parsed LocalTime variable.


return dailySchedule;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> travelStyles;

@Schema(description = "여행 하이라이트")
private List<String> highlights;

@Schema(description = "일별 일정")
private List<DailyScheduleDTO> dailySchedules;

@Schema(description = "출발 교통편")
private TransportationDTO outboundTransportation;

@Schema(description = "귀환 교통편")
private TransportationDTO returnTransportation;

@Schema(description = "숙소 리스트")
private List<AccommodationDTO> accommodations;
}
Comment on lines +15 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

필수 필드에 대한 유효성 검증 추가를 권장합니다.

FastAPI에서 받은 데이터를 저장하기 전에 데이터 무결성을 보장하기 위해 필수 필드들에 대한 유효성 검증 어노테이션을 추가하는 것이 좋습니다. 예를 들어:

  • title, destination, departure: @NotBlank
  • startDate, endDate: @NotNull
  • dailySchedules: @NotEmpty 또는 @Valid

이를 통해 잘못된 데이터가 저장되는 것을 사전에 방지할 수 있습니다.

+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
 public class TripPlanRequest {
 
     @Getter
     @Setter
     @NoArgsConstructor
     @AllArgsConstructor
     @Schema(description = "FastAPI에서 생성된 여행 계획 생성 요청")
     public static class CreateFromFastAPIDTO {
 
         @Schema(description = "여행 제목", example = "제주 액티비티 탐험 5박 6일 여행")
+        @NotBlank(message = "여행 제목은 필수입니다")
         private String title;
 
         @Schema(description = "목적지", example = "제주도")
+        @NotBlank(message = "목적지는 필수입니다")
         private String destination;
 
         @Schema(description = "출발지", example = "서울")
+        @NotBlank(message = "출발지는 필수입니다")
         private String departure;
 
         @Schema(description = "여행 시작일", example = "2025-12-10")
+        @NotNull(message = "여행 시작일은 필수입니다")
         private LocalDate startDate;
 
         @Schema(description = "여행 종료일", example = "2025-12-15")
+        @NotNull(message = "여행 종료일은 필수입니다")
         private LocalDate endDate;
 
         // ... 나머지 필드
 
         @Schema(description = "일별 일정")
+        @NotEmpty(message = "일별 일정은 최소 1개 이상이어야 합니다")
+        @Valid
         private List<DailyScheduleDTO> dailySchedules;

Committable suggestion skipped: line range outside the PR's diff.


@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<ScheduleDTO> 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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading
Loading