diff --git a/backend/src/main/java/com/isp/backend/domain/gpt/config/GptConfig.java b/backend/src/main/java/com/isp/backend/domain/gpt/config/GptConfig.java index 0e483671..8d8c0338 100644 --- a/backend/src/main/java/com/isp/backend/domain/gpt/config/GptConfig.java +++ b/backend/src/main/java/com/isp/backend/domain/gpt/config/GptConfig.java @@ -30,40 +30,44 @@ public class GptConfig { 2. 하루에 최소 세 개의 명소를 방문하고 싶어. 3. 하루에 최소 9시간을 돌아다닐거야. 4. 3일 간의 여행이면 3일짜리 계획을 만들어주면 돼. - 5. 장소의 좌푯값도 소수점까지 함께 알려줘 - 6. 특수 문자를 사용하지 않고, 아래 예시대로 일정을 작성해줘 + 5. 장소의 좌푯값도 소수점까지 함께 알려줘. + 6. 일정마다 입장료나 이용 비용을 여행지 화폐에 맞춰 작성해줘. + 8. 식사 비용이나 입장료 같은 경우 (0.0)으로 작성해줘. + 9. 특수 문자를 사용하지 않고, 아래 예시대로 일정을 작성해줘. + 10. 일정 외의 다른 주의사항이나 추가 문장은 추가하지마. + 11. 비용은 반드시 숫자로만 추가해줘. 4번의 예시는 아래와 같아 <2024.02.06> - - 세비야 대성당 37.38610100, -5.99220400 - - 알카사르 세비야 37.38338500, -5.99051600 - - 점심식사 에스피넬리 글로리아 37.39404600, -5.99379500 - - 히랄다 탑 37.38603000, -5.99303300 - - 스페인 광장 37.37722200, -5.98694400 - - 저녁식사 바루로 37.37734300, -5.98742700 - - 쇼핑 세비야 엘 코르테 잉글레스 37.38942400, -5.99407200 + - 세비야 대성당:37.38610100, -5.99220400 (11.0) + - 알카사르 세비야:37.38338500, -5.99051600 (12.0) + - 점심식사 에스피넬리 글로리아: 37.39404600, -5.99379500 (0.0) + - 히랄다 탑:37.38603000, -5.99303300 (13.0) + - 스페인 광장:37.37722200, -5.98694400 (0.0) + - 저녁식사 바루로:37.37734300, -5.98742700 (0.0) + - 쇼핑 세비야 엘 코르테 잉글레스:37.38942400, -5.99407200 (0.0) <2024.02.07> - - 세비야 대성당 37.38610100, -5.99220400 - - 알카사르 세비야 37.38338500, -5.99051600 - - 점심식사 에스피넬리 글로리아 37.39404600, -5.99379500 - - 히랄다 탑 37.38603000, -5.99303300 - - 스페인 광장 37.37722200, -5.98694400 - - 저녁식사 바루로 37.37734300, -5.98742700 - - 쇼핑 세비야 엘 코르테 잉글레스 37.38942400, -5.99407200 + - 세비야 대성당:37.38610100, -5.99220400 (11.0) + - 알카사르 세비야:37.38338500, -5.99051600 (12.0) + - 점심식사 에스피넬리 글로리아: 37.39404600, -5.99379500 (0.0) + - 히랄다 탑:37.38603000, -5.99303300 (13.0) + - 스페인 광장:37.37722200, -5.98694400 (0.0) + - 저녁식사 바루로:37.37734300, -5.98742700 (0.0) + - 쇼핑 세비야 엘 코르테 잉글레스:37.38942400, -5.99407200 (0.0) <2024.02.08> - - 세비야 대성당 37.38610100, -5.99220400 - - 알카사르 세비야 37.38338500, -5.99051600 - - 점심식사 에스피넬리 글로리아 37.39404600, -5.99379500 - - 히랄다 탑 37.38603000, -5.99303300 - - 스페인 광장 37.37722200, -5.98694400 - - 저녁식사 바루로 37.37734300, -5.98742700 - - 쇼핑 세비야 엘 코르테 잉글레스 37.38942400, -5.99407200 + - 세비야 대성당:37.38610100, -5.99220400 (11.0) + - 알카사르 세비야:37.38338500, -5.99051600 (12.0) + - 점심식사 에스피넬리 글로리아: 37.39404600, -5.99379500 (0.0) + - 히랄다 탑:37.38603000, -5.99303300 (13.0) + - 스페인 광장:37.37722200, -5.98694400 (0.0) + - 저녁식사 바루로:37.37734300, -5.98742700 (0.0) + - 쇼핑 세비야 엘 코르테 잉글레스:37.38942400, -5.99407200 (0.0) 날짜 형식은 반드시 을 지켜줘. 매일 점심과 저녁은 레스토랑에서 먹을 거니까, 실제로 맛있고 유명한 레스토랑도 일정에 포함시켜줘. diff --git a/backend/src/main/java/com/isp/backend/domain/gpt/constant/ParsingConstants.java b/backend/src/main/java/com/isp/backend/domain/gpt/constant/ParsingConstants.java index dd27dcbc..b54f9952 100644 --- a/backend/src/main/java/com/isp/backend/domain/gpt/constant/ParsingConstants.java +++ b/backend/src/main/java/com/isp/backend/domain/gpt/constant/ParsingConstants.java @@ -1,13 +1,34 @@ package com.isp.backend.domain.gpt.constant; -import java.util.List; - -public class ParsingConstants { - public static final String DATE_REGEX = "(\\d{4}\\.\\d{2}\\.\\d{2})"; - public static final String NEW_LINE_REGEX = "\n"; - public static final String CURRENT_DATE = ""; - public static final String COMMA = ", "; - public static final List FILTER_STRINGS = List.of( - "Message(role=assistant, content=", ")" - ); +import lombok.Getter; + +import javax.swing.*; + +@Getter +public enum ParsingConstants { + COMMA(", "), + ENTRY_SEPARATOR("<"), + LINE_SEPARATOR("\n"), + DATE_SUFFIX(">"), + PRICE_PREFIX("("), + PRICE_SUFFIX(")"), + DETAIL_PREFIX("- "), + PRICE_FREE("무료"), + PRICE_VAR("변동"), + DEFAULT_PRICE(0.0), + DETAIL_SUFFIX(":"), + DEFAULT_COORDINATE(0.0); + private final String stringValue; + private final Double doubleValue; + + ParsingConstants(String stringValue) { + this.stringValue = stringValue; + this.doubleValue = null; + } + + ParsingConstants(Double doubleValue) { + this.stringValue = null; + this.doubleValue = doubleValue; + } + } \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleDetail.java b/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleDetail.java index 99781771..3ae7ca96 100644 --- a/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleDetail.java +++ b/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleDetail.java @@ -7,10 +7,12 @@ @NoArgsConstructor public class GptScheduleDetail { private String detail; + private Double price; private Coordinate coordinate; - public GptScheduleDetail(String detail, Coordinate coordinate) { + public GptScheduleDetail(String detail, Double price, Coordinate coordinate) { this.detail = detail; + this.price = price; this.coordinate = coordinate; } } \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleParser.java b/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleParser.java index ce3914e3..4d9ffdb9 100644 --- a/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleParser.java +++ b/backend/src/main/java/com/isp/backend/domain/gpt/entity/GptScheduleParser.java @@ -1,26 +1,31 @@ package com.isp.backend.domain.gpt.entity; +import com.isp.backend.domain.gpt.constant.ParsingConstants; +import com.isp.backend.global.exception.gpt.NonValidatedParsingException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @RequiredArgsConstructor @Component public class GptScheduleParser { + private static final Pattern LINE_PATTERN = Pattern.compile("-\\s*(.*?):\\s*([\\d.]+,\\s*[\\d.]+)?\\s*\\((.*?)\\)"); + public List parseScheduleText(String scheduleText) { System.out.println(scheduleText); List schedules = new ArrayList<>(); - - String[] entries = scheduleText.split("<"); + String[] entries = scheduleText.split(ParsingConstants.ENTRY_SEPARATOR.getStringValue()); for (String entry : entries) { if (entry.trim().isEmpty()) continue; - String[] lines = entry.split("\n"); - String date = lines[0].trim().replace(">", ""); + String[] lines = entry.split(ParsingConstants.LINE_SEPARATOR.getStringValue()); + String date = parseDate(lines[0]); List scheduleDetails = new ArrayList<>(); @@ -28,33 +33,20 @@ public List parseScheduleText(String scheduleText) { String line = lines[i].trim(); if (line.isEmpty()) continue; - String[] parts = line.split(" "); - StringBuilder detail = new StringBuilder(); - double latitude = 0.0; - double longitude = 0.0; - - for (int j = 0; j < parts.length; j++) { - try { - if (j == parts.length - 2) { - latitude = Double.parseDouble(parts[j].replace(",", "")); - } else if (j == parts.length - 1) { - longitude = Double.parseDouble(parts[j]); - } else { - if (!detail.isEmpty()) detail.append(" "); - detail.append(parts[j]); - } - } catch (NumberFormatException e) { - if (!detail.isEmpty()) detail.append(" "); - detail.append(parts[j]); - } - } + Matcher matcher = LINE_PATTERN.matcher(line); + if (matcher.find()) { + String detail = matcher.group(1); + String coordinatesStr = matcher.group(2); + String priceStr = matcher.group(3); - String detailString = detail.toString().trim(); - if (detailString.startsWith("-")) { - detailString = detailString.substring(1).trim(); - } + Coordinate coordinate = parseCoordinates(coordinatesStr); + double price = parsePriceString(priceStr); - scheduleDetails.add(new GptScheduleDetail(detailString, new Coordinate(latitude, longitude))); + scheduleDetails.add(new GptScheduleDetail(detail, price, coordinate)); + } else { + System.out.println("767867889"); + throw new NonValidatedParsingException(); + } } schedules.add(new GptSchedule(date, scheduleDetails)); @@ -62,4 +54,42 @@ public List parseScheduleText(String scheduleText) { return schedules; } + + private String parseDate(String line) { + return line.trim().replace(ParsingConstants.DATE_SUFFIX.getStringValue(), ""); + } + + private Coordinate parseCoordinates(String coordinatesStr) { + if (coordinatesStr == null || coordinatesStr.isEmpty()) { + return new Coordinate(ParsingConstants.DEFAULT_COORDINATE.getDoubleValue(), ParsingConstants.DEFAULT_COORDINATE.getDoubleValue()); + } + + String[] parts = coordinatesStr.split(","); + if (parts.length != 2) { + System.out.println("1546351"); + throw new NonValidatedParsingException(); + } + + try { + double latitude = Double.parseDouble(parts[0].trim()); + double longitude = Double.parseDouble(parts[1].trim()); + return new Coordinate(latitude, longitude); + } catch (NumberFormatException e) { + System.out.println("1234"); + throw new NonValidatedParsingException(); + } + } + + private double parsePriceString(String priceStr) { + if (ParsingConstants.PRICE_FREE.getStringValue().equals(priceStr)) { + return 0.0; + } + + try { + return Double.parseDouble(priceStr.replaceAll("[^\\d.]", "")); + } catch (NumberFormatException e) { + System.out.println("5678"); + return ParsingConstants.DEFAULT_PRICE.getDoubleValue(); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/domain/gpt/service/GptService.java b/backend/src/main/java/com/isp/backend/domain/gpt/service/GptService.java index cbb9fbb2..21e4abde 100644 --- a/backend/src/main/java/com/isp/backend/domain/gpt/service/GptService.java +++ b/backend/src/main/java/com/isp/backend/domain/gpt/service/GptService.java @@ -1,6 +1,7 @@ package com.isp.backend.domain.gpt.service; import com.isp.backend.domain.country.entity.Country; +import com.isp.backend.domain.country.repository.CountryRepository; import com.isp.backend.domain.gpt.config.GptConfig; import com.isp.backend.domain.gpt.constant.ParsingConstants; import com.isp.backend.domain.gpt.dto.request.GptRequest; @@ -12,6 +13,7 @@ import com.isp.backend.domain.gpt.entity.GptSchedule; import com.isp.backend.domain.gpt.entity.GptScheduleParser; import com.isp.backend.domain.schedule.service.ScheduleService; +import com.isp.backend.global.exception.schedule.CountryNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -34,6 +36,7 @@ public class GptService { private final GptScheduleParser gptScheduleParser; + private final CountryRepository countryRepository; private final ScheduleService scheduleService; private final WebClient webClient; @@ -49,7 +52,6 @@ public GptScheduleResponse getResponse(HttpHeaders headers, GptRequest gptReques .retrieve() .bodyToMono(GptResponse.class) .block(); - List gptSchedules = gptScheduleParser.parseScheduleText(extractScheduleText(response)); return new GptScheduleResponse(gptSchedules); } @@ -61,9 +63,8 @@ private String extractScheduleText(GptResponse gptResponse) { @Async public CompletableFuture askQuestion(GptScheduleRequest questionRequest) { String question = formatQuestion(questionRequest); + Country country = countryRepository.findAirportCodeByCity(questionRequest.getDestination()).orElseThrow(CountryNotFoundException::new); String countryImage = country.getImageUrl(); List messages = Collections.singletonList(createMessage(question)); - Country country = scheduleService.validateCountry(questionRequest.getDestination()); - String countryImage = country.getImageUrl(); ExecutorService executorService = Executors.newFixedThreadPool(5); List> futures = Arrays.asList( @@ -103,7 +104,7 @@ private String formatQuestion(GptScheduleRequest questionRequest) { questionRequest.getExcludedActivities(), questionRequest.getDepartureDate(), questionRequest.getReturnDate(), - String.join(ParsingConstants.COMMA, questionRequest.getPurpose()) + String.join(ParsingConstants.COMMA.getStringValue(), questionRequest.getPurpose()) ); } diff --git a/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java b/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java index 60ebb2b0..1b671207 100644 --- a/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java +++ b/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java @@ -38,7 +38,10 @@ public enum ErrorCode { NOT_YOUR_FLIGHT(HttpStatus.UNAUTHORIZED, "F004", "사용자의 항공권이 아닙니다"), OPEN_WEATHER_SEARCH_FAILED(HttpStatus.NOT_FOUND,"F005", "날씨 정보 파싱에 실패하였습니다"), EXCHANGE_RATE_SEARCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"F006", "환율 요청을 가져오는 중 오류를 발생했습니다."), - EXCHANGE_RATE_IS_FAILED(HttpStatus.BAD_REQUEST,"F007", "환율 DB를 저장하던 중 오류가 발생하였습니다."); + EXCHANGE_RATE_IS_FAILED(HttpStatus.BAD_REQUEST,"F007", "환율 DB를 저장하던 중 오류가 발생하였습니다."), + + // Gpt + PARSING_IS_NOT_VALIDATED(HttpStatus.BAD_REQUEST, "G001", "GPT 응답을 파싱하던 중 오류가 발생했습니다."); private HttpStatus status; @@ -52,4 +55,4 @@ public enum ErrorCode { this.message = message; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/global/exception/gpt/NonValidatedParsingException.java b/backend/src/main/java/com/isp/backend/global/exception/gpt/NonValidatedParsingException.java new file mode 100644 index 00000000..8daf8379 --- /dev/null +++ b/backend/src/main/java/com/isp/backend/global/exception/gpt/NonValidatedParsingException.java @@ -0,0 +1,10 @@ +package com.isp.backend.global.exception.gpt; + +import com.isp.backend.global.exception.CustomException; +import com.isp.backend.global.exception.ErrorCode; + +public class NonValidatedParsingException extends CustomException { + public NonValidatedParsingException() { + super(ErrorCode.PARSING_IS_NOT_VALIDATED); + } +} \ No newline at end of file