-
Notifications
You must be signed in to change notification settings - Fork 1
[#22] Feat: 여행 일정 저장 API 구현 #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "TripTransportation.java" -type fRepository: TripTalk/BE_SpringBoot Length of output: 149 🏁 Script executed: cat -n ./src/main/java/com/example/triptalk/domain/tripPlan/entity/TripTransportation.javaRepository: 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 -nRepository: TripTalk/BE_SpringBoot Length of output: 798 중복된 lines 237-238에서 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 |
||
| } | ||
|
|
||
| /** | ||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LocalTime.parse()에 대한 예외 처리가 필요합니다. Line 282에서 다음 중 하나의 방법으로 수정을 권장합니다: 방법 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에 @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 |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필수 필드에 대한 유효성 검증 추가를 권장합니다. FastAPI에서 받은 데이터를 저장하기 전에 데이터 무결성을 보장하기 위해 필수 필드들에 대한 유효성 검증 어노테이션을 추가하는 것이 좋습니다. 예를 들어:
이를 통해 잘못된 데이터가 저장되는 것을 사전에 방지할 수 있습니다. +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;
|
||
|
|
||
| @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; | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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 문을 사용하면 가독성이 향상됩니다.🤖 Prompt for AI Agents