Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ca79f4
[refactor]: tripPlan Dto 수정
Neo1228 Feb 18, 2025
f43e0f5
[refactor]: tripPlan Converter 수정
Neo1228 Feb 18, 2025
c693bdf
Merge pull request #151 from Yeogigallae/refactor/#150
Neo1228 Feb 18, 2025
febaf42
[refactor] 방 정보 조회시 본인 프로필 제외
GithubKangMin Feb 18, 2025
f4b4eb1
[refactor] aiCourse 프롬프트 수정
Gwanghyeon-k Feb 18, 2025
278d0dc
Merge pull request #155 from Yeogigallae/refactor/#154
Gwanghyeon-k Feb 18, 2025
d8ce8d9
[refactor]: 엑세스 토큰 유효기간 수정
Neo1228 Feb 18, 2025
2f2999b
[feature]: Vote 에러코드 및 컨트롤러 예외처리 추가
Neo1228 Feb 18, 2025
12ac357
[refactor]: HomeRepository 조회 메서드 수정
Neo1228 Feb 18, 2025
9c5db42
[refactor]: VoteRoom NullPointer 처리
Neo1228 Feb 18, 2025
5086937
[refactor]: COURSE 관련 로직 추가
Neo1228 Feb 18, 2025
ae87a41
[refactor]: HomeService, HomeConverter 로직 수정
Neo1228 Feb 18, 2025
9434278
Merge pull request #156 from Yeogigallae/refactor/#152
Neo1228 Feb 18, 2025
c18d9bb
[refactor] budgetType 추가
Gwanghyeon-k Feb 19, 2025
c6ddc58
[feat] budget 조회 응답값 수정
Gwanghyeon-k Feb 19, 2025
ec255f7
[refactor] budget controller, service 수정
Gwanghyeon-k Feb 19, 2025
0f03ee7
Merge pull request #158 from Yeogigallae/refactor/#157
Gwanghyeon-k Feb 19, 2025
46601a5
Merge pull request #153 from Yeogigallae/feat/#7
parkmineum Feb 19, 2025
efeddd9
변경 사항 반영
parkmineum Feb 19, 2025
1a21ff1
Merge remote-tracking branch 'origin/dev' into dev
parkmineum Feb 19, 2025
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 @@ -6,16 +6,21 @@
import com.umc.yeogi_gal_lae.api.aiCourse.domain.AICourse;
import com.umc.yeogi_gal_lae.api.aiCourse.repository.AICourseRepository;
import com.umc.yeogi_gal_lae.api.place.domain.Place;
import com.umc.yeogi_gal_lae.api.place.repository.PlaceRepository;
import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
Expand All @@ -24,17 +29,20 @@

@Service
public class AICourseService {
private static final Logger logger = LoggerFactory.getLogger(AICourseService.class);

private final AICourseRepository aiCourseRepository;
private final PlaceRepository placeRepository;
private final WebClient webClient;
private final ObjectMapper objectMapper;

@Value("${openai.api.key}")
private String apiKey;

public AICourseService(AICourseRepository aiCourseRepository,
public AICourseService(AICourseRepository aiCourseRepository, PlaceRepository placeRepository,
WebClient.Builder webClientBuilder) {
this.aiCourseRepository = aiCourseRepository;
this.placeRepository = placeRepository;
this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build();
this.objectMapper = new ObjectMapper();
}
Expand All @@ -47,8 +55,17 @@ public AICourseService(AICourseRepository aiCourseRepository,
*/
@Transactional
public AICourse generateAndStoreAICourse(TripPlan tripPlan) {
// TripPlan에 직접 연결된 Place들을 사용
List<Place> places = tripPlan.getPlaces(); // TripPlan에 places 컬렉션이 있다고 가정
// TripPlan에 직접 연결된 Place들을 DB에서 명시적으로 조회
List<Place> places = placeRepository.findAllByTripPlanId(tripPlan.getId());

if (places.isEmpty()) {
logger.warn("TripPlan id {}에 등록된 장소가 없습니다.", tripPlan.getId());
} else {
List<String> placeNames = places.stream()
.map(Place::getPlaceName)
.collect(Collectors.toList());
logger.info("TripPlan id {}에 등록된 장소 목록: {}", tripPlan.getId(), placeNames);
}
if (places.isEmpty()) {
return null;
}
Expand All @@ -60,13 +77,25 @@ public AICourse generateAndStoreAICourse(TripPlan tripPlan) {
Map<String, List<String>> courseByDay = parseGptResponse(gptApiResponse);
// GPT 결과를 실제 Place 객체와 매핑
Map<String, List<Place>> course = new LinkedHashMap<>();
Set<Long> usedPlaceIds = new HashSet<>();

for (Map.Entry<String, List<String>> entry : courseByDay.entrySet()) {
String dayLabel = entry.getKey();
List<Place> dayPlaces = entry.getValue().stream()
// 해당 일차의 추천 장소들을 실제 Place 객체로 매핑
List<Place> recommendedPlaces = entry.getValue().stream()
.map(name -> findPlaceByName(places, name))
.filter(Objects::nonNull)
.collect(Collectors.toList());
course.put(dayLabel, dayPlaces);

// 이미 사용된 장소를 제외하여 유니크한 장소만 할당 (글로벌 중복 제거)
List<Place> uniqueForDay = recommendedPlaces.stream()
.filter(place -> !usedPlaceIds.contains(place.getId()))
.collect(Collectors.toList());

// 선택된 장소들을 전역 사용 목록에 추가
uniqueForDay.forEach(place -> usedPlaceIds.add(place.getId()));

course.put(dayLabel, uniqueForDay);
}
// 저장할 데이터를 위해 각 일차별 Place 이름 목록으로 변환
Map<String, List<String>> courseByName = new LinkedHashMap<>();
Expand Down Expand Up @@ -132,7 +161,8 @@ private String buildPrompt(TripPlan tripPlan, List<Place> places) {
.append("여행 종료일: ").append(tripPlan.getEndDate()).append("\n")
.append("총 여행 일수: ").append(totalDays).append("일\n")
.append("여행 지역: ").append(tripPlan.getLocation()).append("\n\n")
.append("다음은 방문 가능한 장소 목록 (이름, 주소, 위도, 경도)입니다:\n");
.append("다음은 해당 여행 계획에 등록된 방문 가능한 장소 목록입니다.\n")
.append("※ 일정 생성 시 반드시 아래 목록에 있는 장소 이름만 사용하며, 동일한 장소가 중복되지 않도록 해 주세요.\n");
for (Place p : places) {
promptBuilder.append("- ").append(p.getPlaceName())
.append(" (주소: ").append(p.getAddress())
Expand All @@ -143,27 +173,30 @@ private String buildPrompt(TripPlan tripPlan, List<Place> places) {
.append("위 정보를 바탕으로, 총 ").append(totalDays)
.append("일의 여행 일정(각 일차에 방문할 장소 추천)을 생성해줘.\n")
.append("일정은 반드시 '1일차', '2일차', ... '").append(totalDays)
.append("일차' 형식의 키를 가지며, 각 키의 값은 해당 일차에 추천할 장소들의 이름 목록이어야 합니다.\n")
.append("일차' 형식의 키를 가지며, 각 키의 값은 위 목록에 있는 장소들의 이름만 포함해야 합니다.\n")
.append("예시:\n")
.append("{\"1일차\": [\"장소 A\", \"장소 B\"], \"2일차\": [\"장소 C\", \"장소 D\"], ...}");
return promptBuilder.toString();
}


private String callGptApi(String prompt) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4o-mini");
Map<String, String> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
requestBody.put("messages", List.of(message));
return webClient.post()
String response = webClient.post()
.uri("/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(String.class)
.block();
logger.info("GPT API 응답: {}", response);
return response;
}

private Map<String, List<String>> parseGptResponse(String gptResponse) {
Expand All @@ -183,8 +216,9 @@ private Map<String, List<String>> parseGptResponse(String gptResponse) {
}

private Place findPlaceByName(List<Place> places, String name) {
String normalizedName = name.trim().toLowerCase();
return places.stream()
.filter(p -> p.getPlaceName().equalsIgnoreCase(name))
.filter(p -> p.getPlaceName().trim().toLowerCase().equals(normalizedName))
.findFirst()
.orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import com.umc.yeogi_gal_lae.api.budget.domain.Budget;
import com.umc.yeogi_gal_lae.api.budget.dto.AICourseBudgetResponse;
import com.umc.yeogi_gal_lae.api.budget.dto.BudgetAssignment;
import com.umc.yeogi_gal_lae.api.budget.dto.BudgetDetailResponse;
import com.umc.yeogi_gal_lae.api.budget.dto.BudgetResponse;
import com.umc.yeogi_gal_lae.api.budget.dto.DailyBudgetAssignmentResponse;
import com.umc.yeogi_gal_lae.api.budget.repository.BudgetRepository;
import com.umc.yeogi_gal_lae.api.budget.service.BudgetService;
import com.umc.yeogi_gal_lae.global.common.response.Response;
import com.umc.yeogi_gal_lae.global.error.ErrorCode;
import com.umc.yeogi_gal_lae.global.success.SuccessCode;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -47,17 +49,32 @@ public Response<BudgetResponse> generateAndStoreBudget(@PathVariable Long aiCour
* GET /api/budget/{id} Budget 엔티티의 id로 저장된 예산 추천 데이터를 조회하고, 이를 DailyBudgetAssignmentResponse DTO 리스트로 변환하여 반환합니다.
*/
@GetMapping("/{budgetId}")
public Response<List<DailyBudgetAssignmentResponse>> getBudget(@PathVariable Long budgetId) {
public Response<BudgetDetailResponse> getBudget(@PathVariable Long budgetId) {
Optional<Budget> budgetOpt = budgetService.getBudgetById(budgetId);
if (!budgetOpt.isPresent()) {
return Response.of(ErrorCode.NOT_FOUND, null);
}
Budget budget = budgetOpt.get();
Map<String, List<BudgetAssignment>> budgetMap = budgetService.getBudgetMapById(budgetId);
List<DailyBudgetAssignmentResponse> responseList = BudgetConverter.toDailyBudgetAssignmentResponseList(
List<DailyBudgetAssignmentResponse> dailyAssignments = BudgetConverter.toDailyBudgetAssignmentResponseList(
budgetMap);
return Response.of(SuccessCode.OK, responseList);

// AICourse를 통해 TripPlan 정보를 조회 (TripPlan 클래스가 startDate와 endDate를 LocalDate 타입으로 제공한다고 가정)
String imageUrl = budget.getAiCourse().getTripPlan().getImageUrl();
LocalDate startDate = budget.getAiCourse().getTripPlan().getStartDate();
LocalDate endDate = budget.getAiCourse().getTripPlan().getEndDate();

BudgetDetailResponse detailResponse = BudgetDetailResponse.builder()
.dailyAssignments(dailyAssignments)
.imageUrl(imageUrl)
.startDate(startDate)
.endDate(endDate)
.build();

return Response.of(SuccessCode.OK, detailResponse);
}


@GetMapping("/{aiCourseId}/budgetIds")
public Response<List<AICourseBudgetResponse>> getBudgetIdsByAiCourseId(@PathVariable Long aiCourseId) {
List<com.umc.yeogi_gal_lae.api.budget.domain.Budget> budgets =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.umc.yeogi_gal_lae.api.budget.domain;

public enum BudgetType {
MEAL,
ACTIVITY,
SHOPPING,
TRANSPORT
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.umc.yeogi_gal_lae.api.budget.dto;

import com.umc.yeogi_gal_lae.api.budget.domain.BudgetType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -11,6 +12,6 @@
@Builder
public class BudgetAssignment {
private String placeName;
private String budgetType;
private BudgetType budgetType;
private Double recommendedAmount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.umc.yeogi_gal_lae.api.budget.dto;

import java.time.LocalDate;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BudgetDetailResponse {
private String imageUrl;
private LocalDate startDate;
private LocalDate endDate;
private List<DailyBudgetAssignmentResponse> dailyAssignments;

}
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,29 @@ private String buildBudgetPrompt(String scheduleJson) {
prompt.append("Given the following travel schedule in JSON format: ")
.append(scheduleJson)
.append(", generate budget recommendations for each day. ");
prompt.append("For each place, assign exactly one budget type and a recommended amount. ");
prompt.append(
"For each place, assign exactly one budget type (one of MEAL, ACTIVITY, SHOPPING, TRANSPORT) and a recommended amount. ");
prompt.append(
"Output the result as a JSON object where each key is the day (e.g., '1일차') and each value is an array of objects with the fields: 'placeName', 'budgetType', and 'recommendedAmount'. ");
prompt.append("Example output:\n");
prompt.append("{\n");
prompt.append(" \"1일차\": [\n");
prompt.append(
" {\"placeName\": \"장소 예시\", \"budgetType\": \"activityBudget\", \"recommendedAmount\": 20000},\n");
" {\"placeName\": \"Example Place\", \"budgetType\": \"ACTIVITY\", \"recommendedAmount\": 20000},\n");
prompt.append(
" {\"placeName\": \"음식점 예시\", \"budgetType\": \"mealBudget\", \"recommendedAmount\": 15000}\n");
" {\"placeName\": \"Example Restaurant\", \"budgetType\": \"MEAL\", \"recommendedAmount\": 15000}\n");
prompt.append(" ],\n");
prompt.append(" \"2일차\": [\n");
prompt.append(
" {\"placeName\": \"장소 예시\", \"budgetType\": \"activityBudget\", \"recommendedAmount\": 25000}\n");
" {\"placeName\": \"Another Place\", \"budgetType\": \"ACTIVITY\", \"recommendedAmount\": 25000}\n");
prompt.append(" ]\n");
prompt.append("}");
return prompt.toString();
}

private String callGptApi(String prompt) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4");
requestBody.put("model", "gpt-4o-mini");
Map<String, String> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,27 @@ public class HomeConverter {

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MM-dd");

public static HomeResponse.OngoingVoteRoom toOngoingVoteRoom(VoteRoom voteRoom) {

List<String> profileImageUrls = voteRoom.getTripPlan().getRoom().getRoomMembers().stream()
.map(member -> member.getUser().getProfileImage())
.filter(Objects::nonNull)
.collect(Collectors.toList());
public static HomeResponse.OngoingVoteRoom toOngoingVoteRoom(TripPlan tripPlan) {
List<String> profileImageUrls = tripPlan.getRoom().getRoomMembers().stream()
.map(member -> member.getUser().getProfileImage())
.filter(Objects::nonNull)
.collect(Collectors.toList());

return new HomeResponse.OngoingVoteRoom(
voteRoom.getTripPlan().getId(),
voteRoom.getTripPlan().getRoom().getId(),
voteRoom.getTripPlan().getRoom().getMaster().getId(),
voteRoom.getTripPlan().getRoom().getName(),
voteRoom.getTripPlan().getLocation(),
voteRoom.getTripPlan().getRoom().getRoomMembers().size(),
voteRoom.getTripPlan().getVoteLimitTime(),
voteRoom.getTripPlan().getRoom().getRoomMembers().stream().filter(m -> m.getUser().getVote() != null).count(),
tripPlan.getId(),
tripPlan.getRoom().getId(),
tripPlan.getRoom().getMaster().getId(),
tripPlan.getRoom().getName(),
tripPlan.getLocation(),
tripPlan.getRoom().getRoomMembers().size(),
tripPlan.getVoteLimitTime(),
tripPlan.getRoom().getRoomMembers().stream().filter(m -> m.getUser().getVote() != null).count(),
profileImageUrls,
voteRoom.getCreatedAt(),
voteRoom.getTripPlan().getTripPlanType(),
voteRoom.getTripPlan().getLatitude(),
voteRoom.getTripPlan().getLongitude()
);
tripPlan.getCreatedAt(),
tripPlan.getTripPlanType(),
tripPlan.getLatitude(),
tripPlan.getLongitude()
);
}

public static HomeResponse.CompletedVoteRoom toCompletedVoteRoom(TripPlan tripPlan, Long aiCourseId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
public interface HomeRepository extends JpaRepository<TripPlan, Long> {
List<TripPlan> findByStatus(Status status);

@Query("SELECT vr FROM VoteRoom vr JOIN vr.tripPlan tp WHERE tp.status = 'ONGOING'")
List<VoteRoom> findAllOngoingVoteRooms();
@Query("SELECT tp FROM TripPlan tp WHERE tp.status = 'ONGOING'")
List<TripPlan> findAllOngoingTripPlans();

// 완료된 투표방과 연관된 종료 날짜가 현재 또는 미래인 여행 계획 조회
@Query("SELECT tp FROM TripPlan tp JOIN tp.voteRoom vr WHERE vr.tripPlan.status = 'COMPLETED' AND tp.endDate >= CURRENT_DATE")
List<TripPlan> findCompletedVoteRoomsWithEndDateAfterNow();
@Query("SELECT tp FROM TripPlan tp WHERE tp.status = 'ONGOING' OR (tp.status = 'COMPLETED' AND tp.tripPlanType = 'COURSE')")
List<TripPlan> findAllOngoingAndCompletedCourseTripPlans();
}
Loading