Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
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.notification.domain.NotificationType;
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;
Expand All @@ -21,11 +22,13 @@
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;
import com.umc.yeogi_gal_lae.api.notification.service.NotificationService;

@Service
public class AICourseService {
Expand All @@ -35,16 +38,21 @@ public class AICourseService {
private final PlaceRepository placeRepository;
private final WebClient webClient;
private final ObjectMapper objectMapper;
private final NotificationService notificationService;

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

public AICourseService(AICourseRepository aiCourseRepository, PlaceRepository placeRepository,
WebClient.Builder webClientBuilder) {
public AICourseService(AICourseRepository aiCourseRepository,
PlaceRepository placeRepository,
WebClient.Builder webClientBuilder,
ObjectMapper objectMapper,
NotificationService notificationService) {
this.aiCourseRepository = aiCourseRepository;
this.placeRepository = placeRepository;
this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build();
this.objectMapper = new ObjectMapper();
this.objectMapper = objectMapper;
this.notificationService = notificationService;
}

/**
Expand All @@ -55,6 +63,15 @@ public AICourseService(AICourseRepository aiCourseRepository, PlaceRepository pl
*/
@Transactional
public AICourse generateAndStoreAICourse(TripPlan tripPlan) {
// 1. "코스 짜기 시작" 알림
notificationService.createStartNotification(
tripPlan.getRoom().getName(), // 방 이름
tripPlan.getUser().getUsername(), // 사용자 이름
tripPlan.getUser().getEmail(), // 사용자 이메일
NotificationType.COURSE_START, // 알림 타입
tripPlan.getId(),
tripPlan.getTripPlanType()
);
// TripPlan에 직접 연결된 Place들을 DB에서 명시적으로 조회
List<Place> places = placeRepository.findAllByTripPlanId(tripPlan.getId());

Expand Down Expand Up @@ -112,7 +129,16 @@ public AICourse generateAndStoreAICourse(TripPlan tripPlan) {
.tripPlan(tripPlan)
.courseJson(courseJson)
.build();
// "코스 짜기 완료" 알림 추가
notificationService.createEndNotification(
tripPlan.getRoom().getName(), // 방 이름
tripPlan.getUser().getEmail(), // 사용자 이메일
NotificationType.COURSE_COMPLETE, // 알림 타입
tripPlan.getId(),
tripPlan.getTripPlanType()
);
return aiCourseRepository.save(aiCourse);

} catch (Exception e) {
e.printStackTrace();
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.umc.yeogi_gal_lae.api.notification.domain.NotificationType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;
import com.umc.yeogi_gal_lae.api.notification.service.NotificationService;

@Service
public class BudgetService {
Expand All @@ -27,19 +30,23 @@ public class BudgetService {
private final AICourseRepository aiCourseRepository;
private final WebClient webClient;
private final ObjectMapper objectMapper;
private final NotificationService notificationService;


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

public BudgetService(BudgetRepository budgetRepository,
AICourseRepository aiCourseRepository,
WebClient.Builder webClientBuilder) {
WebClient.Builder webClientBuilder,
ObjectMapper objectMapper,
NotificationService notificationService) {
this.budgetRepository = budgetRepository;
this.aiCourseRepository = aiCourseRepository;
this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build();
this.objectMapper = new ObjectMapper();
this.objectMapper = objectMapper;
this.notificationService = notificationService;
}

/**
* 저장된 AICourse의 스케줄 데이터를 기반으로 GPT API를 호출하여, 각 일차별 각 장소에 대해 예산 추천(예: budgetType 및 추천 금액)을 산출하고, 그 결과를 Budget 엔티티에
* 저장합니다.
Expand All @@ -54,6 +61,15 @@ public Budget generateAndStoreBudget(Long aiCourseId) {
return null;
}
AICourse aiCourse = aiCourseOpt.get();
// "예산 정하기 시작" 알림 추가
notificationService.createStartNotification(
aiCourse.getTripPlan().getRoom().getName(), // 방 이름
aiCourse.getTripPlan().getUser().getUsername(), // 사용자 이름
aiCourse.getTripPlan().getUser().getEmail(), // 사용자 이메일
NotificationType.BUDGET_START, // 알림 타입 (추가 필요)
aiCourse.getTripPlan().getId(),
aiCourse.getTripPlan().getTripPlanType()
);
// aiCourse에 저장된 courseJson은 이미 일정(일차별 장소 이름 목록)을 포함하고 있음
String scheduleJson = aiCourse.getCourseJson();
// 프롬프트 구성: GPT에게 스케줄 정보를 바탕으로 각 장소의 예산 추천을 요청
Expand All @@ -62,13 +78,22 @@ public Budget generateAndStoreBudget(Long aiCourseId) {
String gptApiResponse = callGptApi(prompt);
// 응답 파싱: 예시 결과 JSON은 Map<String, List<BudgetAssignment>>
Map<String, List<BudgetAssignment>> budgetMap = parseBudgetGptResponse(gptApiResponse);
// "예산 정하기 완료" 알림 추가
notificationService.createEndNotification(
aiCourse.getTripPlan().getRoom().getName(), // 방 이름
aiCourse.getTripPlan().getUser().getEmail(), // 사용자 이메일
NotificationType.BUDGET_COMPLETE, // 알림 타입 (추가 필요)
aiCourse.getTripPlan().getId(),
aiCourse.getTripPlan().getTripPlanType()
);
try {
String budgetJson = objectMapper.writeValueAsString(budgetMap);
Budget budget = Budget.builder()
.aiCourse(aiCourse)
.budgetJson(budgetJson)
.build();
return budgetRepository.save(budget);

} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ public ResponseEntity<Response<Void>> createEndNotification(
* 최신 알림 조회 API
*/
@GetMapping
public ResponseEntity<Response<List<NotificationDto>>> getAllNotifications() {
public ResponseEntity<Response<List<NotificationDto>>> getUserNotifications() {

//로그인 없을시 에러냄
// 현재 로그인한 사용자의 이메일 가져오기
String userEmail = AuthenticatedUserUtils.getAuthenticatedUserEmail();

List<NotificationDto> notifications = notificationService.getAllNotifications();
List<NotificationDto> notifications = notificationService.getUserNotifications(userEmail);

return ResponseEntity.ok(Response.of(SuccessCode.NOTIFICATION_FETCH_OK, notifications));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.umc.yeogi_gal_lae.api.notification.domain;

import com.umc.yeogi_gal_lae.api.tripPlan.types.TripPlanType;
import com.umc.yeogi_gal_lae.api.user.domain.User;
import com.umc.yeogi_gal_lae.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.Getter;
Expand All @@ -16,6 +17,10 @@ public class Notification extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY) // 특정 유저의 알림
@JoinColumn(name = "user_id", nullable = false)
private User user;

private String roomName; // 방 이름
private String userName; // 사용자 이름 (시작 알림에만 필요)
private String userEmail; // 사용자 이메일 추가 (중복 문제 해결)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

import com.umc.yeogi_gal_lae.api.notification.domain.Notification;
import java.util.Optional;

import com.umc.yeogi_gal_lae.api.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.umc.yeogi_gal_lae.api.notification.domain.NotificationType;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
// 최신순 정렬
List<Notification> findAllByOrderByCreatedAtDesc();
// 특정 유저의 모든 알림 조회 (최신순)
List<Notification> findByUserOrderByCreatedAtDesc(User user);

Optional<Notification> findById(Long id);

// 특정 유저의 알림만 가져오기
@Query("SELECT n FROM Notification n WHERE n.user.id = :userId ORDER BY n.createdAt DESC")
List<Notification> findByUserId(@Param("userId") Long userId);

@Query("SELECT COUNT(n) FROM Notification n WHERE n.isRead = false AND n.userEmail = :userEmail")
long countUnreadNotificationsByUser(@Param("userEmail") String userEmail);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.umc.yeogi_gal_lae.api.notification.domain.NotificationType;
import com.umc.yeogi_gal_lae.api.notification.repository.NotificationRepository;
import com.umc.yeogi_gal_lae.api.tripPlan.types.TripPlanType;
import com.umc.yeogi_gal_lae.api.user.domain.User;
import com.umc.yeogi_gal_lae.global.error.BusinessException;
import com.umc.yeogi_gal_lae.global.error.ErrorCode;
import com.umc.yeogi_gal_lae.api.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -21,6 +23,7 @@
public class NotificationService {

private final NotificationRepository notificationRepository;
private final UserRepository userRepository;

/**
* 시작 알림 생성 (TripPlan 관련 정보 추가)
Expand All @@ -47,6 +50,8 @@ private void saveNotification(String roomName, String userName, String userEmail
notification.setRoomName(roomName);
notification.setUserName(userName);
notification.setUserEmail(userEmail); // 이메일 저장
notification.setUser(userRepository.findByEmail(userEmail)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)));
notification.setType(type);
notification.setContent(generateCaption(roomName, userName, type, isStart));
notification.setTripPlanId(tripPlanId);
Expand All @@ -56,24 +61,30 @@ private void saveNotification(String roomName, String userName, String userEmail
}

/**
* 최신순으로 정렬된 알림 리스트 조회 (TripPlan 정보 포함)
* 특정 유저의 최신 알림 리스트 조회 (타입 필터링 지원)
*/
@Transactional(readOnly = true)
public List<NotificationDto> getAllNotifications() {
List<Notification> notifications = notificationRepository.findAllByOrderByCreatedAtDesc();
// 레포지토리가 비어있으면 목업 데이터 반환
public List<NotificationDto> getUserNotifications(String userEmail) {
// userEmail을 기반으로 User 객체 조회
User user = userRepository.findByEmail(userEmail)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));

// 알림 조회 (필터링이 없으면 모든 알림 조회)
List<Notification> notifications;
notifications = notificationRepository.findByUserOrderByCreatedAtDesc(user);

// 알림이 없으면 목업 데이터 반환
if (notifications.isEmpty()) {
log.warn("알림 데이터 없음, 기본 목업 데이터 반환");
return mockNotifications();
}

return notifications.stream()
.map(notification -> new NotificationDto(
notification.getId(),
notification.getType().getTitle(),
generateCaption(notification.getRoomName(), notification.getUserName(), notification.getType(),
notification.getType() == NotificationType.VOTE_START ||
notification.getType() == NotificationType.COURSE_START ||
notification.getType() == NotificationType.BUDGET_START),
isStartNotification(notification.getType())),
notification.getType().name(),
notification.getTripPlanId(),
notification.getTripPlanType()
Expand All @@ -82,16 +93,25 @@ public List<NotificationDto> getAllNotifications() {
}

/**
* 목업 알림 데이터 생성
* 목업 데이터 반환 메서드
*/
private List<NotificationDto> mockNotifications() {
return List.of(
new NotificationDto(1L, "투표 시작", "mock에서 투표가 시작되었습니다!", "VOTE_START", 100L, TripPlanType.COURSE),
new NotificationDto(2L, "예산 설정 시작", "mock에서 예산 설정이 시작되었습니다!", "BUDGET_START", 101L, TripPlanType.SCHEDULE),
new NotificationDto(3L, "코스 선택 시작", "mock에서 코스 선택이 시작되었습니다!", "COURSE_START", 102L, TripPlanType.SCHEDULE)
new NotificationDto(1L, "투표 시작", "Mock 투표가 시작되었습니다!", "VOTE_START", 100L, TripPlanType.COURSE),
new NotificationDto(2L, "예산 설정 시작", "Mock 예산 설정이 시작되었습니다!", "BUDGET_START", 101L, TripPlanType.SCHEDULE),
new NotificationDto(3L, "코스 선택 시작", "Mock 코스 선택이 시작되었습니다!", "COURSE_START", 102L, TripPlanType.SCHEDULE)
);
}

/**
* 특정 알림 타입이 "시작" 알림인지 체크
*/
private boolean isStartNotification(NotificationType type) {
return type == NotificationType.VOTE_START ||
type == NotificationType.COURSE_START ||
type == NotificationType.BUDGET_START;
}

/**
* 알림에 대한 캡션 생성 (시작 / 종료 통합)
*/
Expand Down