From 9094b8a580f7c5891eb6daf4095bc6664aac5619 Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 13:46:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[FEATURE]=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=85=8B=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20Attraction=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hyu/erica/capstone/domain/Attraction.java | 82 +++++++++---------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/main/java/hyu/erica/capstone/domain/Attraction.java b/src/main/java/hyu/erica/capstone/domain/Attraction.java index 4675762..4063588 100644 --- a/src/main/java/hyu/erica/capstone/domain/Attraction.java +++ b/src/main/java/hyu/erica/capstone/domain/Attraction.java @@ -22,70 +22,64 @@ public class Attraction { @Id - @Column(name = "UC_SEQ") - private Long ucSeq; + @Column(name = "content_id") + private Long contentId; - @Column(name = "MAIN_TITLE") - private String mainTitle; + @Column(name = "content_name") + private String contentName; - @Column(name = "GUGUN_NM") - private String gugunNm; + @Column(name = "district") + private String district; - @Column(name = "LAT") - private Double lat; + @Column(name = "latitude") + private Double latitude; - @Column(name = "LNG") - private Double lng; + @Column(name = "longitude") + private Double longitude; - @Column(name = "PLACE") - private String place; + @Column(name = "travel_destination") + private String travelDestination; - @Column(name = "TITLE") + @Column(name = "title") private String title; - @Column(name = "SUBTITLE") + @Column(name = "subtitle") private String subtitle; - @Column(name = "MAIN_PLACE") - private String mainPlace; + @Column(name = "address") + private String address; - @Column(name = "ADDR1") - private String addr1; + @Column(name = "contact") + private String contact; - @Column(name = "ADDR2") - private String addr2; + @Column(name = "homepage") + private String homepage; - @Column(name = "CNTCT_TEL") - private String cntctTel; + @Column(name = "transportation_info", columnDefinition = "TEXT") + private String transportationInfo; - @Column(name = "HOMEPAGE_URL") - private String homepageUrl; + @Column(name = "operating_days") + private String operatingDays; - @Column(name = "TRFC_INFO", columnDefinition = "TEXT") - private String trfcInfo; + @Column(name = "closed_days") + private String closedDays; - @Column(name = "USAGE_DAY") - private String usageDay; + @Column(name = "operating_hours") + private String operatingHours; - @Column(name = "HLDY_INFO") - private String hldyInfo; + @Column(name = "admission_fee") + private String admissionFee; - @Column(name = "USAGE_DAY_WEEK_AND_TIME", columnDefinition = "TEXT") - private String usageDayWeekAndTime; + @Column(name = "amenities") + private String amenities; - @Column(name = "USAGE_AMOUNT") - private String usageAmount; + @Column(name = "image_url") + private String imageUrl; - @Column(name = "MIDDLE_SIZE_RM1") - private String middleSizeRm1; + @Column(name = "thumbnail_image_url") + private String thumbnailImageUrl; - @Column(name = "MAIN_IMG_NORMAL") - private String mainImgNormal; + @Column(name = "details", columnDefinition = "LONGTEXT") + private String details; - @Column(name = "MAIN_IMG_THUMB") - private String mainImgThumb; - - @Lob - @Column(name = "ITEMCNTNTS") - private String itemcntnts; // ITEMCNTNTS (상세 설명) } From 66fda61e8ed35b2e1206815e30b09a23d5128e0c Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 13:46:49 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[FEATURE]=20=EC=BB=AC=EB=9F=BC=20=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hyu/erica/capstone/repository/AttractionRepository.java | 4 ++-- .../dto/tripPlan/response/AttractionDetailResponseDTO.java | 6 +++--- .../dto/tripPlan/response/AttractionListResponseDTO.java | 2 +- .../dto/tripPlan/response/AttractionSearchResponseDTO.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/hyu/erica/capstone/repository/AttractionRepository.java b/src/main/java/hyu/erica/capstone/repository/AttractionRepository.java index 7060e93..6bc3bbf 100644 --- a/src/main/java/hyu/erica/capstone/repository/AttractionRepository.java +++ b/src/main/java/hyu/erica/capstone/repository/AttractionRepository.java @@ -6,9 +6,9 @@ import org.springframework.stereotype.Repository; @Repository + public interface AttractionRepository extends JpaRepository { - // 키워드 검색 - List findByMainTitleContaining(String keyword); + List findByContentNameContaining(String contentName); } diff --git a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionDetailResponseDTO.java b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionDetailResponseDTO.java index d4cc3d8..2bccf00 100644 --- a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionDetailResponseDTO.java +++ b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionDetailResponseDTO.java @@ -5,8 +5,8 @@ public record AttractionDetailResponseDTO (Long attractionId, String name, String imageUrl, String address, String phone, String description, String usageDay) { public static AttractionDetailResponseDTO of(Attraction attraction) { - return new AttractionDetailResponseDTO(attraction.getUcSeq(), - attraction.getMainTitle(), attraction.getMainImgNormal(), attraction.getAddr1(), - attraction.getCntctTel(), attraction.getSubtitle(), attraction.getUsageDayWeekAndTime()); + return new AttractionDetailResponseDTO(attraction.getContentId(), + attraction.getContentName(), attraction.getImageUrl(), attraction.getAddress(), + attraction.getContact(), attraction.getSubtitle(), attraction.getClosedDays()); } } diff --git a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionListResponseDTO.java b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionListResponseDTO.java index 68c1047..da53553 100644 --- a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionListResponseDTO.java +++ b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionListResponseDTO.java @@ -7,7 +7,7 @@ public record AttractionListResponseDTO(List attractions, public static AttractionListResponseDTO of(List attractions) { List responseDTOS = attractions.stream() - .map(attraction -> new AttractionResponseDTO(attraction.getUcSeq(), attraction.getMainTitle(), attraction.getMainImgNormal())) + .map(attraction -> new AttractionResponseDTO(attraction.getContentId(), attraction.getContentName(), attraction.getImageUrl())) .toList(); return new AttractionListResponseDTO(responseDTOS, attractions.size()); } diff --git a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionSearchResponseDTO.java b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionSearchResponseDTO.java index 356c3b1..593289a 100644 --- a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionSearchResponseDTO.java +++ b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/AttractionSearchResponseDTO.java @@ -9,8 +9,8 @@ public record AttractionSearchResponseDTO(List attractions, private record AttractionSearchDTO (Long attractionId, String title, String imageUrl, String address, String usageDay) { private static AttractionSearchDTO of(Attraction attraction) { - return new AttractionSearchDTO(attraction.getUcSeq(), attraction.getTitle(), attraction.getMainImgNormal(), - attraction.getAddr1(), attraction.getUsageDayWeekAndTime()); + return new AttractionSearchDTO(attraction.getContentId(), attraction.getTitle(), attraction.getImageUrl(), + attraction.getAddress(), attraction.getClosedDays()); } } public static AttractionSearchResponseDTO of(List attractions) { From 02ecdde8773a06f75da4a3b804d3d471c61058bf Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 13:47:03 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[FEATURE]=20=EC=BB=AC=EB=9F=BC=20=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20-=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/TripPlanQueryServiceImpl.java | 2 +- .../service/util/CsvImportService.java | 43 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/main/java/hyu/erica/capstone/service/tripPlan/impl/TripPlanQueryServiceImpl.java b/src/main/java/hyu/erica/capstone/service/tripPlan/impl/TripPlanQueryServiceImpl.java index 0bc5217..9973baf 100644 --- a/src/main/java/hyu/erica/capstone/service/tripPlan/impl/TripPlanQueryServiceImpl.java +++ b/src/main/java/hyu/erica/capstone/service/tripPlan/impl/TripPlanQueryServiceImpl.java @@ -61,7 +61,7 @@ public AttractionDetailResponseDTO getRecommendAttractionDetail(Long attractionI @Override public AttractionSearchResponseDTO searchRecommendAttraction(String keyword) { - List attractions = attractionRepository.findByMainTitleContaining(keyword); + List attractions = attractionRepository.findByContentNameContaining(keyword); return AttractionSearchResponseDTO.of(attractions); } diff --git a/src/main/java/hyu/erica/capstone/service/util/CsvImportService.java b/src/main/java/hyu/erica/capstone/service/util/CsvImportService.java index 31c3e1a..d4f74d8 100644 --- a/src/main/java/hyu/erica/capstone/service/util/CsvImportService.java +++ b/src/main/java/hyu/erica/capstone/service/util/CsvImportService.java @@ -38,31 +38,32 @@ public void importAttraction(MultipartFile file) { for (CSVRecord record : csvParser) { Attraction spot = Attraction.builder() - .ucSeq(Long.parseLong(record.get("UC_SEQ"))) - .mainTitle(record.get("MAIN_TITLE")) - .gugunNm(record.get("GUGUN_NM")) - .lat(parseDouble(record.get("LAT"))) - .lng(parseDouble(record.get("LNG"))) - .place(record.get("PLACE")) - .title(record.get("TITLE")) - .subtitle(record.get("SUBTITLE")) - .mainPlace(record.get("MAIN_PLACE")) - .addr1(record.get("ADDR1")) - .addr2(record.get("ADDR2")) - .cntctTel(record.get("CNTCT_TEL")) - .homepageUrl(record.get("HOMEPAGE_URL")) - .trfcInfo(trimString(record.get("TRFC_INFO"), 1000)) - .usageDay(record.get("USAGE_DAY")) - .hldyInfo(record.get("HLDY_INFO")) - .usageDayWeekAndTime(record.get("USAGE_DAY_WEEK_AND_TIME")) - .usageAmount(record.get("USAGE_AMOUNT")) - .middleSizeRm1(record.get("MIDDLE_SIZE_RM1")) - .mainImgNormal(record.get("MAIN_IMG_NORMAL")) - .mainImgThumb(record.get("MAIN_IMG_THUMB")) + .contentId(Long.parseLong(record.get("content_id"))) + .contentName(record.get("content_name")) + .district(record.get("district")) + .latitude(parseDouble(record.get("latitude"))) + .longitude(parseDouble(record.get("longitude"))) + .travelDestination(record.get("travel_destination")) + .title(record.get("title")) + .subtitle(record.get("subtitle")) + .address(record.get("address")) + .contact(record.get("contact")) + .homepage(record.get("homepage")) + .transportationInfo(trimString(record.get("transportation_info"), 1000)) + .operatingDays(record.get("operating_days")) + .closedDays(record.get("closed_days")) + .operatingHours(record.get("operating_hours")) + .admissionFee(record.get("admission_fee")) + .amenities(record.get("amenities")) + .imageUrl(record.get("image_url")) + .thumbnailImageUrl(record.get("thumbnail_image_url")) + .details(record.get("details")) .build(); touristSpots.add(spot); } + + attractionRepository.saveAll(touristSpots); } catch (Exception e) { e.printStackTrace(); From ca5f5b65cdfb8feaf93d41a9444796956fc71707 Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 13:47:36 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[FEATURE]=20AI=20API=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B9=84=EB=8F=99=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hyu/erica/capstone/client/PlanClient.java | 2 +- .../PreferAttractionRepository.java | 1 + .../PreferRestaurantRepository.java | 1 + .../capstone/service/async/AsyncService.java | 26 ++++++ .../style/impl/StyleCommandServiceImpl.java | 88 +++++++++++-------- 5 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 src/main/java/hyu/erica/capstone/service/async/AsyncService.java diff --git a/src/main/java/hyu/erica/capstone/client/PlanClient.java b/src/main/java/hyu/erica/capstone/client/PlanClient.java index 8d26c93..e9ce0bf 100644 --- a/src/main/java/hyu/erica/capstone/client/PlanClient.java +++ b/src/main/java/hyu/erica/capstone/client/PlanClient.java @@ -12,6 +12,6 @@ public interface PlanClient { @GetMapping("/restaurants/search") RestaurantRequestDTO getRestaurants(@RequestParam String query); - @GetMapping("/attractions/search") + @GetMapping("/attraction/search") AttractionRequestDTO getAttractions(@RequestParam String query); } diff --git a/src/main/java/hyu/erica/capstone/repository/PreferAttractionRepository.java b/src/main/java/hyu/erica/capstone/repository/PreferAttractionRepository.java index 1ce3cd3..75e3e29 100644 --- a/src/main/java/hyu/erica/capstone/repository/PreferAttractionRepository.java +++ b/src/main/java/hyu/erica/capstone/repository/PreferAttractionRepository.java @@ -10,4 +10,5 @@ public interface PreferAttractionRepository extends JpaRepository { List findAllByTripPlanId(Long tripPlanId); + boolean existsByAttraction_ContentIdAndUserId(Long attractionId, Long userId); } diff --git a/src/main/java/hyu/erica/capstone/repository/PreferRestaurantRepository.java b/src/main/java/hyu/erica/capstone/repository/PreferRestaurantRepository.java index 79c8ae9..098c306 100644 --- a/src/main/java/hyu/erica/capstone/repository/PreferRestaurantRepository.java +++ b/src/main/java/hyu/erica/capstone/repository/PreferRestaurantRepository.java @@ -8,4 +8,5 @@ @Repository public interface PreferRestaurantRepository extends JpaRepository { List findAllByTripPlanId(Long tripPlanId); + boolean existsByRestaurantIdAndUserId(Long restaurantId, Long userId); } diff --git a/src/main/java/hyu/erica/capstone/service/async/AsyncService.java b/src/main/java/hyu/erica/capstone/service/async/AsyncService.java new file mode 100644 index 0000000..9f32d3c --- /dev/null +++ b/src/main/java/hyu/erica/capstone/service/async/AsyncService.java @@ -0,0 +1,26 @@ +package hyu.erica.capstone.service.async; + +import hyu.erica.capstone.client.PlanClient; +import hyu.erica.capstone.web.dto.client.AttractionRequestDTO; +import hyu.erica.capstone.web.dto.client.RestaurantRequestDTO; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AsyncService { + + private final PlanClient planClient; + + @Async + public CompletableFuture getAttractionsAsync(String prompt) { + return CompletableFuture.completedFuture(planClient.getAttractions(prompt)); + } + + @Async + public CompletableFuture getRestaurantsAsync(String prompt) { + return CompletableFuture.completedFuture(planClient.getRestaurants(prompt)); + } +} diff --git a/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java b/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java index edd7ea3..4dd0f19 100644 --- a/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java +++ b/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java @@ -3,11 +3,13 @@ import hyu.erica.capstone.api.code.status.ErrorStatus; import hyu.erica.capstone.api.exception.GeneralException; import hyu.erica.capstone.client.PlanClient; +import hyu.erica.capstone.domain.Attraction; import hyu.erica.capstone.domain.Restaurant; import hyu.erica.capstone.domain.Style; import hyu.erica.capstone.domain.TripPlan; import hyu.erica.capstone.domain.User; import hyu.erica.capstone.domain.enums.City; +import hyu.erica.capstone.domain.mapping.PreferAttraction; import hyu.erica.capstone.domain.mapping.PreferRestaurant; import hyu.erica.capstone.repository.AttractionRepository; import hyu.erica.capstone.repository.PreferAttractionRepository; @@ -16,7 +18,9 @@ import hyu.erica.capstone.repository.StyleRepository; import hyu.erica.capstone.repository.TripPlanRepository; import hyu.erica.capstone.repository.UserRepository; +import hyu.erica.capstone.service.async.AsyncService; import hyu.erica.capstone.service.style.StyleCommandService; +import hyu.erica.capstone.web.dto.client.AttractionRequestDTO; import hyu.erica.capstone.web.dto.client.RestaurantRequestDTO; import hyu.erica.capstone.web.dto.style.request.UserStyleRequestDTO; import hyu.erica.capstone.web.dto.style.response.UserStyleFinalResponseDTO; @@ -24,6 +28,7 @@ import hyu.erica.capstone.web.dto.style.response.UserStyleResponseDTO; import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,7 +47,7 @@ public class StyleCommandServiceImpl implements StyleCommandService { private final PreferRestaurantRepository preferRestaurantRepository; private final PreferAttractionRepository preferAttractionRepository; - private final PlanClient planClient; + private final AsyncService asyncService; private final static String TITLE = "여행 계획"; private final static String PROFILE_IMAGE = "https://i.imgur.com/3zX2Z1b.png"; @@ -73,21 +78,18 @@ public UserStyleResponseDTO updateStyle(Long userId, Long styleId, UserStyleRequ return UserStyleResponseDTO.of(save); } - @Override public UserStyleFinalResponseDTO submitStyle(Long styleId, Long userId) { - Style style = styleRepository.findById(styleId).orElseThrow( () -> new GeneralException(ErrorStatus._STYLE_NOT_FOUND)); - User user = userRepository.findById(userId).orElseThrow( () -> new GeneralException(ErrorStatus._USER_NOT_FOUND)); + Style style = styleRepository.findById(styleId) + .orElseThrow(() -> new GeneralException(ErrorStatus._STYLE_NOT_FOUND)); + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(ErrorStatus._USER_NOT_FOUND)); - StringBuilder sb = new StringBuilder(); - sb.append("여행 지역 : ").append(style.getCity().name()).append("\n") - .append("시작 날짜 : ").append(style.getStartDate()).append("\n") - .append("종료 날짜 : ").append(style.getEndDate()).append("\n") - .append("선호 활동 : ").append(style.getPreferActivity()).append("\n") - .append("추가 요구 사항 : ").append(style.getRequirement()); -// -// AttractionRequestDTO attractions = planClient.getAttractions(sb.toString()); - RestaurantRequestDTO restaurants = planClient.getRestaurants(sb.toString()); + String prompt = buildPrompt(style); + + // 1. API 병렬 호출 + CompletableFuture attractionFuture = asyncService.getAttractionsAsync(prompt); + CompletableFuture restaurantFuture = asyncService.getRestaurantsAsync(prompt); TripPlan tripPlan = TripPlan.builder() .user(user) @@ -97,32 +99,48 @@ public UserStyleFinalResponseDTO submitStyle(Long styleId, Long userId) { .profileImage(PROFILE_IMAGE) .build(); - - List restaurantIds = restaurants.restaurant_ids(); - - for (Long restaurantId : restaurantIds) { - Restaurant restaurant = restaurantRepository.findById(restaurantId).orElseThrow( - () -> new GeneralException(ErrorStatus._RESTAURANT_NOT_FOUND)); - preferRestaurantRepository.save(PreferRestaurant.builder() - .restaurant(restaurant) - .user(user) - .isPrefer(true) - .tripPlan(tripPlan) - .build()); + // 2. 결과 대기 + AttractionRequestDTO attractions = attractionFuture.join(); + RestaurantRequestDTO restaurants = restaurantFuture.join(); + + for (Long restaurantId : restaurants.restaurant_ids()) { + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new GeneralException(ErrorStatus._RESTAURANT_NOT_FOUND)); + if (!preferRestaurantRepository.existsByRestaurantIdAndUserId(restaurantId, user.getId())) { + preferRestaurantRepository.save(PreferRestaurant.builder() + .restaurant(restaurant) + .user(user) + .isPrefer(true) + .tripPlan(tripPlan) + .build()); + } } -// List attractionIds = attractions.attraction_ids(); -// for (Long attractionId : attractionIds) { -// Attraction attraction = attractionRepository.findById(attractionId).orElseThrow( -// () -> new GeneralException(ErrorStatus._ATTRACTION_NOT_FOUND)); -// preferAttractionRepository.save(PreferAttraction.builder() -// .attraction(attraction) -// .user(user) -// .build()); -// } + for (Long attractionId : attractions.attraction_ids()) { + Attraction attraction = attractionRepository.findById(attractionId) + .orElseThrow(() -> new GeneralException(ErrorStatus._ATTRACTION_NOT_FOUND)); + if (!preferAttractionRepository.existsByAttraction_ContentIdAndUserId(attractionId, user.getId())) { + preferAttractionRepository.save(PreferAttraction.builder() + .attraction(attraction) + .user(user) + .tripPlan(tripPlan) + .build()); + } + } TripPlan save = tripPlanRepository.save(tripPlan); + return UserStyleFinalResponseDTO.of(restaurants.restaurant_ids(), attractions.attraction_ids(), save.getId()); + } - return UserStyleFinalResponseDTO.of(restaurants.restaurant_ids(), List.of(), save.getId()); + private String buildPrompt(Style style) { + return new StringBuilder() + .append("여행 지역 : ").append(style.getCity().name()).append("\n") + .append("시작 날짜 : ").append(style.getStartDate()).append("\n") + .append("종료 날짜 : ").append(style.getEndDate()).append("\n") + .append("선호 활동 : ").append(style.getPreferActivity()).append("\n") + .append("추가 요구 사항 : ").append(style.getRequirement()) + .toString(); } + + } From f6c9dd71bdfdf4a9e45fb847026ed6646a864f38 Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 14:44:43 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[FEATURE]=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9D=84=20=EC=9C=84=ED=95=9C=20@EnableAsync?= =?UTF-8?q?=20=EC=95=A0=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/hyu/erica/capstone/CapstoneApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/hyu/erica/capstone/CapstoneApplication.java b/src/main/java/hyu/erica/capstone/CapstoneApplication.java index 0043d10..a305ec3 100644 --- a/src/main/java/hyu/erica/capstone/CapstoneApplication.java +++ b/src/main/java/hyu/erica/capstone/CapstoneApplication.java @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableJpaAuditing @EnableFeignClients +@EnableAsync public class CapstoneApplication { public static void main(String[] args) { From 544a0d5d21b7e1f8cc8a11762675440a4326ff42 Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 14:45:11 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[FEATURE]=20=EC=97=AC=ED=96=89=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=20=EC=83=9D=EC=84=B1=20=EC=A7=84=EC=B2=99=EB=8F=84=20?= =?UTF-8?q?=ED=8C=8C=EC=95=85=20=EC=9C=84=ED=95=9C=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/hyu/erica/capstone/domain/TripPlan.java | 10 ++++++++++ .../erica/capstone/domain/enums/TripPlanStatus.java | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 src/main/java/hyu/erica/capstone/domain/enums/TripPlanStatus.java diff --git a/src/main/java/hyu/erica/capstone/domain/TripPlan.java b/src/main/java/hyu/erica/capstone/domain/TripPlan.java index ef7d519..fd5a7d1 100644 --- a/src/main/java/hyu/erica/capstone/domain/TripPlan.java +++ b/src/main/java/hyu/erica/capstone/domain/TripPlan.java @@ -2,10 +2,13 @@ import static jakarta.persistence.GenerationType.*; +import hyu.erica.capstone.domain.enums.TripPlanStatus; import hyu.erica.capstone.domain.mapping.PreferAttraction; import hyu.erica.capstone.domain.mapping.PreferRestaurant; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -45,6 +48,9 @@ public class TripPlan { private LocalDate endDate; + @Enumerated(value = EnumType.STRING) + private TripPlanStatus tripPlanStatus; + @ManyToOne @JoinColumn(name = "user_id") private User user; @@ -56,5 +62,9 @@ public class TripPlan { private List preferRestaurants; + public void setStatus(TripPlanStatus status) { + this.tripPlanStatus = status; + } + } diff --git a/src/main/java/hyu/erica/capstone/domain/enums/TripPlanStatus.java b/src/main/java/hyu/erica/capstone/domain/enums/TripPlanStatus.java new file mode 100644 index 0000000..79c13d5 --- /dev/null +++ b/src/main/java/hyu/erica/capstone/domain/enums/TripPlanStatus.java @@ -0,0 +1,5 @@ +package hyu.erica.capstone.domain.enums; + +public enum TripPlanStatus { + PROGRESSING, DONE, FAILED +} From 1df249922e2809b039d0e20c5268a1be36fb0e24 Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 14:45:21 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[FEATURE]=20=EC=97=AC=ED=96=89=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=20=EC=83=9D=EC=84=B1=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capstone/service/async/AsyncService.java | 26 ----- .../async/StyleBackgroundTaskService.java | 109 ++++++++++++++++++ .../service/style/StyleCommandService.java | 3 +- .../style/impl/StyleCommandServiceImpl.java | 80 +++---------- .../response/TripPlanResponseDTO.java | 10 ++ 5 files changed, 136 insertions(+), 92 deletions(-) delete mode 100644 src/main/java/hyu/erica/capstone/service/async/AsyncService.java create mode 100644 src/main/java/hyu/erica/capstone/service/async/StyleBackgroundTaskService.java create mode 100644 src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/TripPlanResponseDTO.java diff --git a/src/main/java/hyu/erica/capstone/service/async/AsyncService.java b/src/main/java/hyu/erica/capstone/service/async/AsyncService.java deleted file mode 100644 index 9f32d3c..0000000 --- a/src/main/java/hyu/erica/capstone/service/async/AsyncService.java +++ /dev/null @@ -1,26 +0,0 @@ -package hyu.erica.capstone.service.async; - -import hyu.erica.capstone.client.PlanClient; -import hyu.erica.capstone.web.dto.client.AttractionRequestDTO; -import hyu.erica.capstone.web.dto.client.RestaurantRequestDTO; -import java.util.concurrent.CompletableFuture; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AsyncService { - - private final PlanClient planClient; - - @Async - public CompletableFuture getAttractionsAsync(String prompt) { - return CompletableFuture.completedFuture(planClient.getAttractions(prompt)); - } - - @Async - public CompletableFuture getRestaurantsAsync(String prompt) { - return CompletableFuture.completedFuture(planClient.getRestaurants(prompt)); - } -} diff --git a/src/main/java/hyu/erica/capstone/service/async/StyleBackgroundTaskService.java b/src/main/java/hyu/erica/capstone/service/async/StyleBackgroundTaskService.java new file mode 100644 index 0000000..5e78c1a --- /dev/null +++ b/src/main/java/hyu/erica/capstone/service/async/StyleBackgroundTaskService.java @@ -0,0 +1,109 @@ +package hyu.erica.capstone.service.async; + +import hyu.erica.capstone.api.code.status.ErrorStatus; +import hyu.erica.capstone.api.exception.GeneralException; +import hyu.erica.capstone.client.PlanClient; +import hyu.erica.capstone.domain.Attraction; +import hyu.erica.capstone.domain.Restaurant; +import hyu.erica.capstone.domain.Style; +import hyu.erica.capstone.domain.TripPlan; +import hyu.erica.capstone.domain.User; +import hyu.erica.capstone.domain.enums.TripPlanStatus; +import hyu.erica.capstone.domain.mapping.PreferAttraction; +import hyu.erica.capstone.domain.mapping.PreferRestaurant; +import hyu.erica.capstone.repository.AttractionRepository; +import hyu.erica.capstone.repository.PreferAttractionRepository; +import hyu.erica.capstone.repository.PreferRestaurantRepository; +import hyu.erica.capstone.repository.RestaurantRepository; +import hyu.erica.capstone.repository.TripPlanRepository; +import hyu.erica.capstone.repository.UserRepository; +import hyu.erica.capstone.web.dto.client.AttractionRequestDTO; +import hyu.erica.capstone.web.dto.client.RestaurantRequestDTO; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StyleBackgroundTaskService { + + private final PlanClient planClient; + private final TripPlanRepository tripPlanRepository; + private final UserRepository userRepository; + private final RestaurantRepository restaurantRepository; + private final AttractionRepository attractionRepository; + private final PreferRestaurantRepository preferRestaurantRepository; + private final PreferAttractionRepository preferAttractionRepository; + + @Async + @Transactional + public void handleTripPlanDetails(Long tripPlanId, Style style, User user) { + TripPlan tripPlan = tripPlanRepository.findById(tripPlanId) + .orElseThrow(() -> new GeneralException(ErrorStatus._TRIP_PLAN_NOT_FOUND)); + + log.info("handleTripPlanDetails: " + tripPlanId); + + User managedUser = userRepository.findById(user.getId()) + .orElseThrow(() -> new GeneralException(ErrorStatus._USER_NOT_FOUND)); + + + try { + String prompt = buildPrompt(style); + + // 외부 API 병렬 호출 + CompletableFuture attractionFuture = + CompletableFuture.supplyAsync(() -> planClient.getAttractions(prompt)); + CompletableFuture restaurantFuture = + CompletableFuture.supplyAsync(() -> planClient.getRestaurants(prompt)); + + AttractionRequestDTO attractions = attractionFuture.join(); + RestaurantRequestDTO restaurants = restaurantFuture.join(); + + // 레스토랑 저장 + for (Long restaurantId : restaurants.restaurant_ids()) { + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new GeneralException(ErrorStatus._RESTAURANT_NOT_FOUND)); + preferRestaurantRepository.save(PreferRestaurant.builder() + .restaurant(restaurant) + .user(managedUser) + .isPrefer(true) + .tripPlan(tripPlan) + .build()); + } + + // 어트랙션 저장 + for (Long attractionId : attractions.attraction_ids()) { + Attraction attraction = attractionRepository.findById(attractionId) + .orElseThrow(() -> new GeneralException(ErrorStatus._ATTRACTION_NOT_FOUND)); + preferAttractionRepository.save(PreferAttraction.builder() + .attraction(attraction) + .user(managedUser) + .tripPlan(tripPlan) + .build()); + } + + tripPlan.setStatus(TripPlanStatus.DONE); + + } catch (Exception e) { + tripPlan.setStatus(TripPlanStatus.FAILED); + log.info(e.toString()); + // 로그 남기기 + } + + tripPlanRepository.save(tripPlan); + } + + private String buildPrompt(Style style) { + return new StringBuilder() + .append("여행 지역 : ").append(style.getCity().name()).append("\n") + .append("시작 날짜 : ").append(style.getStartDate()).append("\n") + .append("종료 날짜 : ").append(style.getEndDate()).append("\n") + .append("선호 활동 : ").append(style.getPreferActivity()).append("\n") + .append("추가 요구 사항 : ").append(style.getRequirement()) + .toString(); + } +} diff --git a/src/main/java/hyu/erica/capstone/service/style/StyleCommandService.java b/src/main/java/hyu/erica/capstone/service/style/StyleCommandService.java index 98658c4..84820e4 100644 --- a/src/main/java/hyu/erica/capstone/service/style/StyleCommandService.java +++ b/src/main/java/hyu/erica/capstone/service/style/StyleCommandService.java @@ -4,6 +4,7 @@ import hyu.erica.capstone.web.dto.style.response.UserStyleFinalResponseDTO; import hyu.erica.capstone.web.dto.style.response.UserStyleInitResponseDTO; import hyu.erica.capstone.web.dto.style.response.UserStyleResponseDTO; +import hyu.erica.capstone.web.dto.tripPlan.response.TripPlanResponseDTO; public interface StyleCommandService { @@ -11,5 +12,5 @@ public interface StyleCommandService { UserStyleResponseDTO updateStyle(Long userId, Long styleId, UserStyleRequestDTO request); - UserStyleFinalResponseDTO submitStyle(Long styleId, Long userId); + TripPlanResponseDTO submitStyle(Long styleId, Long userId); } diff --git a/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java b/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java index 4dd0f19..e3345ec 100644 --- a/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java +++ b/src/main/java/hyu/erica/capstone/service/style/impl/StyleCommandServiceImpl.java @@ -2,15 +2,11 @@ import hyu.erica.capstone.api.code.status.ErrorStatus; import hyu.erica.capstone.api.exception.GeneralException; -import hyu.erica.capstone.client.PlanClient; -import hyu.erica.capstone.domain.Attraction; -import hyu.erica.capstone.domain.Restaurant; import hyu.erica.capstone.domain.Style; import hyu.erica.capstone.domain.TripPlan; import hyu.erica.capstone.domain.User; import hyu.erica.capstone.domain.enums.City; -import hyu.erica.capstone.domain.mapping.PreferAttraction; -import hyu.erica.capstone.domain.mapping.PreferRestaurant; +import hyu.erica.capstone.domain.enums.TripPlanStatus; import hyu.erica.capstone.repository.AttractionRepository; import hyu.erica.capstone.repository.PreferAttractionRepository; import hyu.erica.capstone.repository.PreferRestaurantRepository; @@ -18,17 +14,14 @@ import hyu.erica.capstone.repository.StyleRepository; import hyu.erica.capstone.repository.TripPlanRepository; import hyu.erica.capstone.repository.UserRepository; -import hyu.erica.capstone.service.async.AsyncService; +import hyu.erica.capstone.service.async.StyleBackgroundTaskService; import hyu.erica.capstone.service.style.StyleCommandService; -import hyu.erica.capstone.web.dto.client.AttractionRequestDTO; -import hyu.erica.capstone.web.dto.client.RestaurantRequestDTO; import hyu.erica.capstone.web.dto.style.request.UserStyleRequestDTO; -import hyu.erica.capstone.web.dto.style.response.UserStyleFinalResponseDTO; import hyu.erica.capstone.web.dto.style.response.UserStyleInitResponseDTO; import hyu.erica.capstone.web.dto.style.response.UserStyleResponseDTO; -import java.util.List; +import hyu.erica.capstone.web.dto.tripPlan.response.TripPlanResponseDTO; +import jakarta.persistence.EntityManager; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,16 +31,13 @@ @RequiredArgsConstructor public class StyleCommandServiceImpl implements StyleCommandService { + private final EntityManager entityManager; private final UserRepository userRepository; private final StyleRepository styleRepository; - private final RestaurantRepository restaurantRepository; - private final AttractionRepository attractionRepository; private final TripPlanRepository tripPlanRepository; - private final PreferRestaurantRepository preferRestaurantRepository; - private final PreferAttractionRepository preferAttractionRepository; - private final AsyncService asyncService; + private final StyleBackgroundTaskService asyncService; private final static String TITLE = "여행 계획"; private final static String PROFILE_IMAGE = "https://i.imgur.com/3zX2Z1b.png"; @@ -79,68 +69,28 @@ public UserStyleResponseDTO updateStyle(Long userId, Long styleId, UserStyleRequ return UserStyleResponseDTO.of(save); } @Override - public UserStyleFinalResponseDTO submitStyle(Long styleId, Long userId) { + public TripPlanResponseDTO submitStyle(Long styleId, Long userId) { + Style style = styleRepository.findById(styleId) .orElseThrow(() -> new GeneralException(ErrorStatus._STYLE_NOT_FOUND)); User user = userRepository.findById(userId) .orElseThrow(() -> new GeneralException(ErrorStatus._USER_NOT_FOUND)); - String prompt = buildPrompt(style); - - // 1. API 병렬 호출 - CompletableFuture attractionFuture = asyncService.getAttractionsAsync(prompt); - CompletableFuture restaurantFuture = asyncService.getRestaurantsAsync(prompt); - TripPlan tripPlan = TripPlan.builder() .user(user) .startDate(style.getStartDate()) .endDate(style.getEndDate()) .title(TITLE) .profileImage(PROFILE_IMAGE) + .tripPlanStatus(TripPlanStatus.PROGRESSING) .build(); - // 2. 결과 대기 - AttractionRequestDTO attractions = attractionFuture.join(); - RestaurantRequestDTO restaurants = restaurantFuture.join(); - - for (Long restaurantId : restaurants.restaurant_ids()) { - Restaurant restaurant = restaurantRepository.findById(restaurantId) - .orElseThrow(() -> new GeneralException(ErrorStatus._RESTAURANT_NOT_FOUND)); - if (!preferRestaurantRepository.existsByRestaurantIdAndUserId(restaurantId, user.getId())) { - preferRestaurantRepository.save(PreferRestaurant.builder() - .restaurant(restaurant) - .user(user) - .isPrefer(true) - .tripPlan(tripPlan) - .build()); - } - } - - for (Long attractionId : attractions.attraction_ids()) { - Attraction attraction = attractionRepository.findById(attractionId) - .orElseThrow(() -> new GeneralException(ErrorStatus._ATTRACTION_NOT_FOUND)); - if (!preferAttractionRepository.existsByAttraction_ContentIdAndUserId(attractionId, user.getId())) { - preferAttractionRepository.save(PreferAttraction.builder() - .attraction(attraction) - .user(user) - .tripPlan(tripPlan) - .build()); - } - } - - TripPlan save = tripPlanRepository.save(tripPlan); - return UserStyleFinalResponseDTO.of(restaurants.restaurant_ids(), attractions.attraction_ids(), save.getId()); - } - - private String buildPrompt(Style style) { - return new StringBuilder() - .append("여행 지역 : ").append(style.getCity().name()).append("\n") - .append("시작 날짜 : ").append(style.getStartDate()).append("\n") - .append("종료 날짜 : ").append(style.getEndDate()).append("\n") - .append("선호 활동 : ").append(style.getPreferActivity()).append("\n") - .append("추가 요구 사항 : ").append(style.getRequirement()) - .toString(); - } + TripPlan saved = tripPlanRepository.save(tripPlan); + entityManager.flush(); + // 비동기 처리 시작 + asyncService.handleTripPlanDetails(saved.getId(), style, user); + return TripPlanResponseDTO.of(saved.getId(), saved.getTripPlanStatus()); + } } diff --git a/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/TripPlanResponseDTO.java b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/TripPlanResponseDTO.java new file mode 100644 index 0000000..d4363c8 --- /dev/null +++ b/src/main/java/hyu/erica/capstone/web/dto/tripPlan/response/TripPlanResponseDTO.java @@ -0,0 +1,10 @@ +package hyu.erica.capstone.web.dto.tripPlan.response; + +import hyu.erica.capstone.domain.enums.TripPlanStatus; + +public record TripPlanResponseDTO(Long tripPlanId, TripPlanStatus status) { + + public static TripPlanResponseDTO of(Long tripPlanId, TripPlanStatus status) { + return new TripPlanResponseDTO(tripPlanId, status); + } +} From 641bfbdc12899e8136d78e0fa4769658dbb7c10d Mon Sep 17 00:00:00 2001 From: msk226 Date: Sun, 23 Mar 2025 14:49:00 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[FEATURE]=20=EB=AA=85=EC=84=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hyu/erica/capstone/web/controller/TripPlanController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/hyu/erica/capstone/web/controller/TripPlanController.java b/src/main/java/hyu/erica/capstone/web/controller/TripPlanController.java index 9602342..31824e4 100644 --- a/src/main/java/hyu/erica/capstone/web/controller/TripPlanController.java +++ b/src/main/java/hyu/erica/capstone/web/controller/TripPlanController.java @@ -33,7 +33,7 @@ public class TripPlanController { // 선택지 확인 (여행지) - @Tag(name = "[선택지 확인]", description = "선택지 확인 API") + @Tag(name = "선택지 확인", description = "선택지 확인 API") @Operation(summary = "선택지 (여행지) 확인", description = """ ### 선택지 확인 API @@ -158,7 +158,7 @@ public ApiResponse searchRestaurant( ### Path Variables - tripPlansIds: 여행 계획 ID (List) """) - @PostMapping("/restaurants/final") + @PostMapping("/{tripPlansId}/restaurants/final") public ApiResponse finalRestaurant( @PathVariable Long tripPlansId, @RequestBody SaveRestaurantRequestDTO request) {