diff --git a/Back-end/src/main/java/com/example/backend/schedule/service/AiService.java b/Back-end/src/main/java/com/example/backend/schedule/service/AiService.java index 6ade5d5..e100835 100644 --- a/Back-end/src/main/java/com/example/backend/schedule/service/AiService.java +++ b/Back-end/src/main/java/com/example/backend/schedule/service/AiService.java @@ -13,6 +13,7 @@ import java.time.LocalDate; import java.time.LocalTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.UUID; @@ -118,22 +119,61 @@ static String createDailyPlanPrompt(UUID scheduleId, LocalDate startDate, LocalD throw new RuntimeException("JSON 직렬화에 실패했습니다.", e); } - return String.format(""" - 너는 여행 일정 계획 전문가 AI다. 너의 임무는 주어진 장소 목록을 핵심 규칙에 따라 각 여행일에 논리적으로 배정하고, 지정된 JSON 형식으로 반환하는 것이다. + long travelDays = ChronoUnit.DAYS.between(startDate, endDate) + 1; + if (travelDays <= 0) { + travelDays = 1; + } + + int totalItemCount = items.size(); + long baseTotalCount = totalItemCount / travelDays; + long remainderTotal = totalItemCount % travelDays; + long[] totalItemsPerDay = new long[(int) travelDays]; + for (int i = 0; i < travelDays; i++) { + totalItemsPerDay[i] = baseTotalCount + (i < remainderTotal ? 1 : 0); + } + + long accommodationsCount = items.stream().filter(item -> "ACCOMMODATION".equals(item.category())).count(); + long restaurantsCount = items.stream().filter(item -> "RESTAURANT".equals(item.category())).count(); + long avgRestaurantsPerDay = (restaurantsCount > 0 && travelDays > 0) ? (long) Math.ceil((double) restaurantsCount / travelDays) : 2; + + StringBuilder distributionInstruction = new StringBuilder(); + for (int i = 0; i < travelDays; i++) { + long fixedItemsThisDay = 0; + if (travelDays == 1) { + fixedItemsThisDay += Math.min(1, accommodationsCount); + } else { + if (i == 0 || i == travelDays - 1) { + fixedItemsThisDay += (accommodationsCount > 0) ? 1 : 0; + } else { + fixedItemsThisDay += (accommodationsCount > 1) ? 2 : 0; + } + } + fixedItemsThisDay += avgRestaurantsPerDay; - ### **[1] 최종 목표** - 모든 규칙을 준수하여, 각 장소를 방문할 날짜(`dayNumber`)만 결정하여 그룹화한다. (방문 순서는 이 단계에서 결정하지 않는다.) + long otherItemsToAdd = totalItemsPerDay[i] - fixedItemsThisDay; + otherItemsToAdd = Math.max(0, otherItemsToAdd); - ### **[2] 핵심 작업 규칙** - [A] 숙소 배정 규칙 (최우선 순위!): - * `category`가 `ACCOMMODATION`인 장소를 식별한다. - * 1일차: 첫 번째 숙소를 1일차 그룹에 포함시킨다. 이 숙소는 그날의 마지막 장소가 될 것이다. - * 중간일 (2일차 ~ 마지막 전날): 이전 날의 숙소를 해당일 그룹의 첫 장소로, 다음 순서의 숙소를 해당일 그룹의 마지막 장소로 포함시킨다. - * 마지막 날: 이전 날의 숙소를 마지막 날 그룹의 첫 장소로 포함시킨다. + distributionInstruction.append(String.format("* %d일차: '기타 장소' %d개 추가 (최종 목표: 총 %d개)%n", i + 1, otherItemsToAdd, totalItemsPerDay[i])); + } - [B] 카테고리별 일정 계획 규칙: - * 식당(`RESTAURANT`): 각 날짜별로 점심, 저녁에 방문하도록 하루 2개씩 배정하는 것을 기본으로 한다. - * 기타 장소(`TOURIST_SPOT`, `LEISURE`, `HEALING`): 숙소와 식당 배정 후, 남은 장소들을 전체 여행 기간에 걸쳐 균등하게 분배한다. + return String.format(""" + 너는 여행 일정 계획 전문가 AI다. 너의 임무는 주어진 장소 목록을 핵심 규칙에 따라 각 여행일에 논리적으로 배정하여, **일자별 총 장소 개수 목표**를 정확히 맞추는 것이다. + + ### **[1] 핵심 목표** + 각 날짜의 `items` 배열에 포함될 장소의 **총개수**가 아래 **[일자별 최종 목표]** 표에 명시된 '최종 목표' 숫자와 정확히 일치하도록 만들어야 한다. + + ### **[2] 작업 규칙** + 장소들은 숙소, 식당, 기타 장소 세 종류로 나뉜다. + 1. **숙소(`ACCOMMODATION`)와 식당(`RESTAURANT`)을 규칙에 따라 먼저 마음속으로 배정한다.** + * 숙소 규칙: 1일차와 마지막 날은 1개, 중간 날은 2개(전날 숙소, 당일 숙소)가 기본이다. + * 식당 규칙: 하루 2개 배정을 목표로 한다. + 2. **그 다음, 아래 [일자별 최종 목표] 표를 확인한다.** + 3. 표에 적힌 **'기타 장소 추가 개수'만큼** `TOURIST_SPOT`, `LEISURE`, `HEALING` 카테고리에서 장소를 골라 추가한다. + 4. 이렇게 조합하여 최종적으로 그날의 **'최종 목표' 총개수를 정확히 맞춘다.** 지리적 근접성을 고려하여 장소를 선택하면 가장 좋다. + + ### **[일자별 최종 목표]** + **아래 지시에 따라 '기타 장소'를 추가하여, 일자별 '최종 목표' 총개수를 반드시 맞춰야 한다.** + %s ### **[3] 출력 데이터 형식 (매우 중요!)** 너의 응답은 반드시 아래 JSON 구조를 따라야 한다. 다른 설명 없이, 순수한 JSON 객체 하나만 출력해야 한다. @@ -159,6 +199,7 @@ static String createDailyPlanPrompt(UUID scheduleId, LocalDate startDate, LocalD 이제 위의 규칙에 따라 일자별 계획 JSON을 생성해줘. """, + distributionInstruction.toString(), startDate, endDate, scheduleId, itemsJson); } }