-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/ISSUE-53' into staging
- Loading branch information
Showing
12 changed files
with
330 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
src/main/java/gyeongdan/article/recommend/controller/RecommendController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package gyeongdan.article.recommend.controller; | ||
|
||
import gyeongdan.article.article.dto.ArticleAllResponse; | ||
import gyeongdan.article.recommend.service.RecommendService; | ||
import gyeongdan.util.CommonResponse; | ||
import gyeongdan.util.JwtUtil; | ||
import jakarta.annotation.Nullable; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
@RestController | ||
@RequestMapping("/api/article") | ||
@RequiredArgsConstructor | ||
public class RecommendController { | ||
private final JwtUtil jwtUtil; | ||
private final RecommendService recommendService; | ||
|
||
@GetMapping("/recommend") | ||
public ResponseEntity<?> getUserTypeArticles(@RequestHeader @Nullable String accessToken) { // id : 기사id, access token : 유저의 접근 권한 | ||
Optional<Long> userId = Optional.empty(); | ||
if (accessToken != null && !accessToken.isEmpty()) { | ||
userId = jwtUtil.getUserId(jwtUtil.resolveToken(accessToken)); | ||
} | ||
|
||
// 추천 알고리즘 | ||
List<ArticleAllResponse> articles = recommendService.recommendArticleById(userId); | ||
|
||
return ResponseEntity.ok(new CommonResponse<>(articles, "추천 기사 10개 가져오기 성공", true)); | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
src/main/java/gyeongdan/article/recommend/domain/Recommends.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package gyeongdan.article.recommend.domain; | ||
|
||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.persistence.*; | ||
import lombok.*; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
@Entity | ||
@Table(name = "recommends", schema = "gyeongdan") | ||
@Getter | ||
@Setter | ||
@Builder | ||
@AllArgsConstructor | ||
@NoArgsConstructor | ||
public class Recommends { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long classificationId; | ||
|
||
@Column(name = "recommend_article_ids", columnDefinition = "jsonb") | ||
private String recommendArticleIdsJson; | ||
|
||
@Transient | ||
private List<Long> recommendArticleIds; | ||
|
||
// 활용 메서드들 | ||
private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
@PostLoad | ||
private void onLoad() { | ||
this.recommendArticleIds = convertJsonToList(this.recommendArticleIdsJson); | ||
} | ||
|
||
@PrePersist | ||
@PreUpdate | ||
private void onPersist() { | ||
this.recommendArticleIdsJson = convertListToJson(this.recommendArticleIds); | ||
} | ||
|
||
private List<Long> convertJsonToList(String json) { | ||
try { | ||
return objectMapper.readValue(json, new TypeReference<List<Long>>() {}); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to convert JSON to List<Long>", e); | ||
} | ||
} | ||
|
||
private String convertListToJson(List<Long> list) { | ||
try { | ||
return objectMapper.writeValueAsString(list); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to convert List<Long> to JSON", e); | ||
} | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
src/main/java/gyeongdan/article/recommend/domain/UserType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package gyeongdan.article.recommend.domain; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.Setter; | ||
|
||
|
||
@Entity | ||
@Table(name = "user_type", schema = "gyeongdan") | ||
@Getter | ||
@Setter | ||
@Builder | ||
@AllArgsConstructor | ||
@RequiredArgsConstructor | ||
public class UserType { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long userId; | ||
|
||
private Long userTypeIssueFinder; | ||
private Long userTypeLifestyleConsumer; | ||
private Long userTypeEntertainer; | ||
private Long userTypeTechSpecialist; | ||
private Long userTypeProfessionals; | ||
private Long userType; | ||
} | ||
|
9 changes: 9 additions & 0 deletions
9
src/main/java/gyeongdan/article/recommend/repository/RecommendJpaRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package gyeongdan.article.recommend.repository; | ||
|
||
import gyeongdan.article.recommend.domain.Recommends; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
public interface RecommendJpaRepository extends JpaRepository<Recommends, Long> { | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/gyeongdan/article/recommend/repository/UserTypeJpaRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package gyeongdan.article.recommend.repository; | ||
|
||
import gyeongdan.article.recommend.domain.UserType; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.stereotype.Repository; | ||
|
||
import java.util.Optional; | ||
|
||
@Repository | ||
public interface UserTypeJpaRepository extends JpaRepository<UserType, Long> { | ||
Optional<UserType> findByuserId(Long user_id); | ||
} |
105 changes: 105 additions & 0 deletions
105
src/main/java/gyeongdan/article/recommend/service/RecommendService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package gyeongdan.article.recommend.service; | ||
|
||
import gyeongdan.article.article.dto.ArticleAllResponse; | ||
import gyeongdan.article.article.repository.ArticleJpaRepository; | ||
import gyeongdan.article.recommend.domain.Recommends; | ||
import gyeongdan.article.recommend.domain.UserType; | ||
import gyeongdan.article.recommend.repository.RecommendJpaRepository; | ||
import gyeongdan.article.recommend.repository.UserTypeJpaRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.util.*; | ||
import java.util.stream.Collectors; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class RecommendService { | ||
private final RecommendJpaRepository recommendJpaRepository; | ||
private final UserTypeJpaRepository userTypeJpaRepository; | ||
private final ArticleJpaRepository articleJpaRepository; | ||
|
||
public List<ArticleAllResponse> recommendArticleById(Optional<Long> userId) { | ||
if (userId.isEmpty()) { | ||
throw new IllegalArgumentException( "아직 유형검사를 하지 않은 유저입니다."); | ||
} | ||
|
||
// (1) 사용자 id에 해당하는 UserType을 가져옴 | ||
UserType userType = userTypeJpaRepository.findByuserId(userId.get()) | ||
.orElseThrow(() -> new IllegalArgumentException( "아직 유형검사를 하지 않은 유저입니다.")); | ||
|
||
// (2) UserType에서 가장 값이 높은 3개의 타입값을 추출하기 | ||
Map<String, Long> userTypeValues = new HashMap<>(); | ||
userTypeValues.put("ISSUE_FINDER", userType.getUserTypeIssueFinder()); | ||
userTypeValues.put("LIFESTYLE_CONSUMER", userType.getUserTypeLifestyleConsumer()); | ||
userTypeValues.put("ENTERTAINER", userType.getUserTypeEntertainer()); | ||
userTypeValues.put("TECH_SPECIALIST", userType.getUserTypeTechSpecialist()); | ||
userTypeValues.put("PROFESSIONALS", userType.getUserTypeProfessionals()); | ||
|
||
List<String> top3UserTypes = userTypeValues.entrySet().stream() | ||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed()) | ||
.limit(3) | ||
.map(Map.Entry::getKey) | ||
.collect(Collectors.toList()); | ||
|
||
// (3) top3UserTypes를 기반으로 classification_id를 결정 | ||
int classificationId = determineClassificationId(top3UserTypes); | ||
|
||
// (4) classification_id에 해당하는 추천 기사 ID들을 가져옴 | ||
Recommends recommends = recommendJpaRepository.findById((long) classificationId) | ||
.orElseThrow(() -> new IllegalArgumentException("Invalid classification ID")); | ||
|
||
// 추천 기사 ID들에 해당하는 ArticleAllResponse 리스트를 반환 | ||
return fetchArticlesByIds(recommends.getRecommendArticleIds()); | ||
} | ||
|
||
private int determineClassificationId(List<String> top3UserTypes) { | ||
// 데이터 매핑 (파이썬의 데이터 프레임에 해당) | ||
Map<Integer, Map<String, Integer>> data = new HashMap<>(); | ||
data.put(1, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 1, "TECH_SPECIALIST", 0, "PROFESSIONALS", 0)); | ||
data.put(2, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 0, "TECH_SPECIALIST", 1, "PROFESSIONALS", 0)); | ||
data.put(3, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 0, "TECH_SPECIALIST", 0, "PROFESSIONALS", 1)); | ||
data.put(4, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 0, "ENTERTAINER", 1, "TECH_SPECIALIST", 1, "PROFESSIONALS", 0)); | ||
data.put(5, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 0, "ENTERTAINER", 1, "TECH_SPECIALIST", 0, "PROFESSIONALS", 1)); | ||
data.put(6, Map.of("ISSUE_FINDER", 1, "LIFESTYLE_CONSUMER", 0, "ENTERTAINER", 0, "TECH_SPECIALIST", 1, "PROFESSIONALS", 1)); | ||
data.put(7, Map.of("ISSUE_FINDER", 0, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 1, "TECH_SPECIALIST", 1, "PROFESSIONALS", 0)); | ||
data.put(8, Map.of("ISSUE_FINDER", 0, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 1, "TECH_SPECIALIST", 0, "PROFESSIONALS", 1)); | ||
data.put(9, Map.of("ISSUE_FINDER", 0, "LIFESTYLE_CONSUMER", 1, "ENTERTAINER", 0, "TECH_SPECIALIST", 1, "PROFESSIONALS", 1)); | ||
data.put(10, Map.of("ISSUE_FINDER", 0, "LIFESTYLE_CONSUMER", 0, "ENTERTAINER", 1, "TECH_SPECIALIST", 1, "PROFESSIONALS", 1)); | ||
|
||
// (3) top3UserTypes를 기반으로 classification_id를 결정 | ||
for (Map.Entry<Integer, Map<String, Integer>> entry : data.entrySet()) { | ||
Map<String, Integer> userTypeMap = entry.getValue(); | ||
boolean matches = true; | ||
for (String userType : top3UserTypes) { | ||
if (userTypeMap.getOrDefault(userType, 0) != 1) { | ||
matches = false; | ||
break; | ||
} | ||
} | ||
if (matches) { | ||
return entry.getKey(); | ||
} | ||
} | ||
|
||
throw new IllegalArgumentException("User type 3개를 기반으로 classification_id를 결정할 수 없습니다!"); | ||
} | ||
|
||
// (4) ArticleAllResponse 리스트를 반환하는 메서드 | ||
private List<ArticleAllResponse> fetchArticlesByIds(List<Long> articleIds) { | ||
return articleIds.stream() | ||
.map(articleJpaRepository::findById) | ||
.filter(Optional::isPresent) | ||
.map(Optional::get) | ||
.map(article -> new ArticleAllResponse( | ||
article.getId(), | ||
article.getSimpleTitle(), | ||
article.getSimpleContent(), | ||
article.getViewCount(), | ||
article.getCategory(), | ||
Optional.ofNullable(article.getImageUrl()), | ||
article.getPublishedAt() | ||
)) | ||
.collect(Collectors.toList()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.