Skip to content

Commit c34c4d9

Browse files
authored
Merge pull request #97 from TU-NEBULA/feature/SCRUM-239
SCRUM-239: 추천 시스템 API 구현
2 parents acc71d4 + a46c1f5 commit c34c4d9

13 files changed

Lines changed: 754 additions & 0 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.team_nebula.nebula.domain.recommendation.api;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.PostMapping;
5+
import org.springframework.web.bind.annotation.RequestBody;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.team_nebula.nebula.domain.recommendation.dto.request.RecommendationFeedbackRequestDTO;
11+
import com.team_nebula.nebula.domain.recommendation.dto.request.SearchBasedRecommendationRequestDTO;
12+
import com.team_nebula.nebula.domain.recommendation.dto.response.ClusterTrendsResponseDTO;
13+
import com.team_nebula.nebula.domain.recommendation.dto.response.GeneralRecommendationResponseDTO;
14+
import com.team_nebula.nebula.domain.recommendation.dto.response.RecommendationFeedbackResponseDTO;
15+
import com.team_nebula.nebula.domain.recommendation.dto.response.SearchBasedRecommendationResponseDTO;
16+
import com.team_nebula.nebula.domain.recommendation.service.RecommendationService;
17+
import com.team_nebula.nebula.global.annotation.AuthUser;
18+
import com.team_nebula.nebula.global.apipayload.ApiResponse;
19+
20+
import io.swagger.v3.oas.annotations.Operation;
21+
import io.swagger.v3.oas.annotations.tags.Tag;
22+
import lombok.RequiredArgsConstructor;
23+
24+
@RestController
25+
@RequestMapping("/api/v2/recommendations")
26+
@RequiredArgsConstructor
27+
@Tag(name = "[추천]")
28+
public class RecommendationController {
29+
30+
private final RecommendationService recommendationService;
31+
32+
@Operation(summary = "전체적인 콘텐츠 추천", description = "사용자 프로파일과 클러스터 분석을 기반으로 전체적인 콘텐츠를 추천하는 API")
33+
@GetMapping("/general")
34+
public ApiResponse<GeneralRecommendationResponseDTO> generalRecommendation(
35+
@AuthUser Long userId,
36+
@RequestParam(defaultValue = "10") int limit,
37+
@RequestParam(required = false) String category,
38+
@RequestParam(defaultValue = "true") boolean excludeViewed,
39+
@RequestParam(defaultValue = "true") boolean diversify,
40+
@RequestParam(defaultValue = "week") String timeRange) {
41+
return ApiResponse.onSuccess(
42+
recommendationService.generalRecommendation(userId, limit, category, excludeViewed, diversify, timeRange));
43+
}
44+
45+
@Operation(summary = "검색 기반 실시간 추천", description = "사용자의 검색어를 분석하여 실시간으로 관련 콘텐츠를 추천하는 API")
46+
@PostMapping("/search-based")
47+
public ApiResponse<SearchBasedRecommendationResponseDTO> searchBasedRecommendation(
48+
@AuthUser Long userId,
49+
@RequestBody SearchBasedRecommendationRequestDTO request) {
50+
return ApiResponse.onSuccess(recommendationService.searchBasedRecommendation(userId, request));
51+
}
52+
53+
@Operation(summary = "클러스터 트렌드 분석", description = "사용자 클러스터별 트렌딩 키워드와 관심사를 분석하는 API")
54+
@GetMapping("/cluster-trends")
55+
public ApiResponse<ClusterTrendsResponseDTO> clusterTrends(
56+
@RequestParam(required = false) Integer clusterId,
57+
@RequestParam(defaultValue = "week") String timePeriod,
58+
@RequestParam(defaultValue = "true") boolean includeGlobal) {
59+
return ApiResponse.onSuccess(recommendationService.clusterTrends(clusterId, timePeriod, includeGlobal));
60+
}
61+
62+
@Operation(summary = "추천 피드백 수집", description = "사용자의 추천 콘텐츠 상호작용을 수집하는 API")
63+
@PostMapping("/feedback")
64+
public ApiResponse<RecommendationFeedbackResponseDTO> feedback(
65+
@AuthUser Long userId,
66+
@RequestBody RecommendationFeedbackRequestDTO request) {
67+
return ApiResponse.onSuccess(recommendationService.feedback(userId, request));
68+
}
69+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.request;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class RecommendationFeedbackRequestDTO {
15+
private String recommendationId;
16+
private String bookmarkId;
17+
private String actionType;
18+
private LocalDateTime shownAt;
19+
private LocalDateTime actionAt;
20+
private String sessionId;
21+
private String pageContext;
22+
private Integer recommendationPosition;
23+
private Integer explicitRating;
24+
private Integer engagementTime;
25+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.request;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class SearchBasedRecommendationRequestDTO {
15+
private String query;
16+
17+
@Builder.Default
18+
private Integer limit = 10;
19+
20+
@Builder.Default
21+
private Boolean includeSimilarQueries = true;
22+
23+
@Builder.Default
24+
private Boolean boostUserPreferences = true;
25+
26+
private String sessionId;
27+
28+
private List<String> previousQueries;
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class ClusterTrendDTO {
15+
private Integer clusterId;
16+
17+
private String clusterName;
18+
19+
private Integer memberCount;
20+
21+
private List<TrendingKeywordDTO> trendingKeywords;
22+
23+
private List<String> popularDomains;
24+
25+
private List<String> activityPeakHours;
26+
27+
private List<String> primaryInterests;
28+
29+
private List<String> emergingTopics;
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class ClusterTrendsResponseDTO {
15+
private List<ClusterTrendDTO> clusterTrends;
16+
17+
private Object globalTrends;
18+
19+
private String generatedAt;
20+
21+
private String analysisPeriod;
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class GeneralRecommendationResponseDTO {
15+
private List<RecommendationDTO> recommendations;
16+
17+
private Long userClusterId;
18+
19+
private String clusterDescription;
20+
21+
private Integer totalRecommendations;
22+
23+
private String generatedAt;
24+
25+
private String algorithmVersion;
26+
27+
private Double personalizationScore;
28+
29+
private Double diversityScore;
30+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class RecommendationDTO {
15+
private String bookmarkId;
16+
17+
private String title;
18+
19+
private String url;
20+
21+
private Double score;
22+
23+
private String reasonType;
24+
25+
private ReasonDetailsDTO reasonDetails;
26+
27+
private String domain;
28+
29+
private List<String> keywords;
30+
31+
private String category;
32+
33+
private String publishedAt;
34+
35+
private String summary;
36+
37+
@Getter
38+
@Builder
39+
@AllArgsConstructor
40+
@NoArgsConstructor
41+
public static class ReasonDetailsDTO {
42+
private String algorithm;
43+
44+
private Double similarityScore;
45+
46+
private Double clusterScore;
47+
48+
private Double popularityScore;
49+
}
50+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@Builder
10+
@AllArgsConstructor
11+
@NoArgsConstructor
12+
public class RecommendationFeedbackResponseDTO {
13+
private String feedbackId;
14+
private Boolean processed;
15+
private Double impactScore;
16+
private Boolean modelUpdated;
17+
private String message;
18+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@Builder
14+
@AllArgsConstructor
15+
@NoArgsConstructor
16+
public class SearchBasedRecommendationDTO {
17+
@JsonProperty("bookmark_id")
18+
private String bookmarkId;
19+
20+
private String title;
21+
22+
private String url;
23+
24+
private Double score;
25+
26+
@JsonProperty("reason_type")
27+
private String reasonType;
28+
29+
@JsonProperty("reason_details")
30+
private SearchBasedReasonDetailsDTO reasonDetails;
31+
32+
private String domain;
33+
34+
private List<String> keywords;
35+
36+
private String category;
37+
38+
@JsonProperty("published_at")
39+
private String publishedAt;
40+
41+
private String summary;
42+
43+
@Getter
44+
@Builder
45+
@AllArgsConstructor
46+
@NoArgsConstructor
47+
public static class SearchBasedReasonDetailsDTO {
48+
private String type;
49+
50+
@JsonProperty("search_query")
51+
private String searchQuery;
52+
53+
@JsonProperty("expanded_keywords")
54+
private List<String> expandedKeywords;
55+
56+
private List<RecommendationFactorDTO> factors;
57+
58+
private Double confidence;
59+
}
60+
61+
@Getter
62+
@Builder
63+
@AllArgsConstructor
64+
@NoArgsConstructor
65+
public static class RecommendationFactorDTO {
66+
private String factor;
67+
68+
private String description;
69+
70+
private Double weight;
71+
}
72+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.team_nebula.nebula.domain.recommendation.dto.response;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class SearchBasedRecommendationResponseDTO {
15+
private List<SearchBasedRecommendationDTO> recommendations;
16+
17+
private List<String> queryKeywords;
18+
19+
private List<String> expandedConcepts;
20+
21+
private String searchIntent;
22+
23+
private Integer totalRecommendations;
24+
25+
private Long processingTimeMs;
26+
27+
private Double similarityThresholdUsed;
28+
}

0 commit comments

Comments
 (0)