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 @@ -3,6 +3,7 @@
import com.daytodo.domain.course.dto.CourseRequest;
import com.daytodo.domain.course.dto.CourseResponse;
import com.daytodo.domain.course.service.CourseService;
import com.daytodo.domain.course.service.CourseAiRecommendationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand All @@ -21,6 +22,7 @@
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
private final CourseAiRecommendationService courseAiRecommendationService;

@Operation(summary = "홈 및 생성한 코스 목록 통합 조회")
@GetMapping
Expand Down Expand Up @@ -53,6 +55,15 @@ public CourseResponse.Created createCourse(
return courseService.createCourse(userId, request);
}

@Operation(summary = "AI 추천 코스 2종 조회")
@PostMapping("/ai-recommendations")
public CourseResponse.AiRecommendations getAiRecommendations(
@AuthenticationPrincipal Long userId,
@Valid @RequestBody CourseRequest.AiRecommendation request
) {
return courseAiRecommendationService.recommend(request);
}

@Operation(summary = "초대코드로 코스 참가")
@PostMapping("/join")
public CourseResponse.Joined joinCourse(
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/daytodo/domain/course/dto/CourseRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ public record Join(
) {
}

public record AiRecommendation(
@NotNull(message = "지역은 필수입니다.") Long regionId,
@NotNull(message = "최소 금액은 필수입니다.")
@PositiveOrZero(message = "최소 금액은 0 이상이어야 합니다.") Integer minPrice,
@NotNull(message = "최대 금액은 필수입니다.")
@PositiveOrZero(message = "최대 금액은 0 이상이어야 합니다.") Integer maxPrice
) {
@AssertTrue(message = "최대 금액은 최소 금액보다 작을 수 없습니다.")
public boolean isValidPriceRange() {
return minPrice == null || maxPrice == null || maxPrice >= minPrice;
}
}

// ==============================================================================

public record Setting(
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/com/daytodo/domain/course/dto/CourseResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,47 @@ public record Created(Long courseId, String inviteCode, LocalDateTime inviteCode
public record Joined(Long courseId, String courseName) {
}

public record AiRecommendations(
boolean success,
String code,
String message,
List<AiRecommendationCourse> result
) {
public AiRecommendations {
result = List.copyOf(result);
}
}

public record AiRecommendationCourse(
String courseName,
int estimatedTotalMinPrice,
int estimatedTotalMaxPrice,
List<AiRecommendationPlace> places
) {
public AiRecommendationCourse {
places = List.copyOf(places);
}
}

public record AiRecommendationPlace(
int recommendationOrder,
Long placeId,
String naverPlaceId,
String placeName,
String category,
String address,
String roadAddress,
double latitude,
double longitude,
String description,
String imageUrl,
int minPrice,
int maxPrice,
double priceConfidence,
String priceReason
) {
}

// ==============================================================================

public record Setting(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public enum CourseErrorCode implements BaseErrorCode {
RECOMMENDATION_ALREADY_LIKED(HttpStatus.CONFLICT, "RECOMMENDATION_ALREADY_LIKED", "이미 좋아요를 누른 추천입니다."),
DUPLICATE_PLACE(HttpStatus.CONFLICT, "DUPLICATE_PLACE", "이미 코스에 추가된 장소입니다."),
PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE_NOT_FOUND", "장소를 찾을 수 없습니다."),
DUPLICATE_RECOMMENDATION(HttpStatus.CONFLICT, "DUPLICATE_RECOMMENDATION", "이미 추천된 장소입니다.");
DUPLICATE_RECOMMENDATION(HttpStatus.CONFLICT, "DUPLICATE_RECOMMENDATION", "이미 추천된 장소입니다."),
COURSE_RECOMMENDATION_FAILED(HttpStatus.CONFLICT, "COURSE_RECOMMENDATION_FAILED", "추천 코스를 생성할 수 없습니다.");

private final HttpStatus status;
private final String code;
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/daytodo/domain/place/entity/Place.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,20 @@ public class Place extends BaseEntity {

@Column (name = "image_url", length = 500)
private String imageUrl;

public Place(Region region, String naverPlaceId, String placeName, String category,
String address, String roadAddress, double latitude, double longitude,
String phone, String description, String imageUrl) {
this.region = region;
this.naverPlaceId = naverPlaceId;
this.placeName = placeName;
this.category = category;
this.address = address;
this.roadAddress = roadAddress;
this.latitude = latitude;
this.longitude = longitude;
this.phone = phone;
this.description = description;
this.imageUrl = imageUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,15 @@ public class PlacePriceEstimate {
// 추론 근거
@Column (name = "reason", length = 500)
private String reason;

public PlacePriceEstimate(Place place, int minPrice, int maxPrice, double confidence, String reason) {
if (maxPrice < minPrice) {
throw new IllegalArgumentException("최대 가격은 최소 가격보다 작을 수 없습니다.");
}
this.place = place;
this.minPrice = minPrice;
this.maxPrice = maxPrice;
this.confidence = confidence;
this.reason = reason;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.daytodo.domain.place.entity.Place;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

/**
* 이미 존재한다면 이 파일은 만들지 않아도 됩니다 (findAllById는 JpaRepository 기본 제공 메서드).
*/
public interface PlaceRepository extends JpaRepository<Place, Long> {
}
Optional<Place> findByNaverPlaceId(String naverPlaceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import com.daytodo.domain.user.entity.User;
import com.daytodo.domain.user.enums.LoginType;
import com.daytodo.domain.user.service.UserService;
import com.daytodo.domain.place.repository.PlaceRecommendationRepository;
import com.daytodo.domain.place.repository.RecommendationLikeRepository;
import com.daytodo.domain.place.repository.PlaceRepository;
import com.daytodo.domain.place.repository.RecommendationCommentRepository;
import com.daytodo.global.apiPayload.exception.ProjectException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -62,6 +66,10 @@ class CourseServiceTest {
@Mock InviteCodeRepository inviteCodeRepository;
@Mock RegionRepository regionRepository;
@Mock UserService userService;
@Mock PlaceRecommendationRepository placeRecommendationRepository;
@Mock RecommendationLikeRepository recommendationLikeRepository;
@Mock PlaceRepository placeRepository;
@Mock RecommendationCommentRepository recommendationCommentRepository;

CourseService courseService;
User user;
Expand All @@ -76,6 +84,10 @@ void setUp() {
inviteCodeRepository,
regionRepository,
userService,
placeRecommendationRepository,
recommendationLikeRepository,
placeRepository,
recommendationCommentRepository,
CLOCK
);
user = new User("user@example.com", "password", "user", null, LoginType.LOCAL);
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.daytodo.domain.course.controller.CourseController;
import com.daytodo.domain.course.dto.CourseResponse;
import com.daytodo.domain.course.service.CourseService;
import com.daytodo.domain.course.service.CourseAiRecommendationService;
import com.daytodo.domain.user.controller.UserController;
import com.daytodo.domain.user.dto.UserResponse;
import com.daytodo.domain.user.service.FeedbackService;
Expand Down Expand Up @@ -56,6 +57,7 @@ class TargetApiSecurityTest {
@MockitoBean FeedbackService feedbackService;
@MockitoBean PolicyService policyService;
@MockitoBean CourseService courseService;
@MockitoBean CourseAiRecommendationService courseAiRecommendationService;
@MockitoBean JpaMetamodelMappingContext jpaMetamodelMappingContext;

@Test
Expand Down Expand Up @@ -87,6 +89,15 @@ void targetCourseApiRejectsRequestWithoutJwt() throws Exception {
.andExpect(status().isUnauthorized());
}

@Test
void aiRecommendationApiRejectsRequestWithoutJwt() throws Exception {
mockMvc.perform(post("/courses/ai-recommendations")
.contentType("application/json")
.content("{\"regionId\":1,\"minPrice\":10000,\"maxPrice\":30000}"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value("UNAUTHORIZED"));
}

@Test
void calendarUsesJwtPrincipal() throws Exception {
when(courseService.getCalendar(1L, 2026, 7))
Expand Down
Loading