Skip to content

Commit b486e5d

Browse files
committed
Merge remote-tracking branch 'origin/develop' into OPS-319-BE-refactor-아카이브-로그인-연동
2 parents eb6d9b3 + 657365b commit b486e5d

File tree

9 files changed

+230
-33
lines changed

9 files changed

+230
-33
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ ext {
2828
springAiVersion = "1.0.0"
2929
}
3030

31+
test {
32+
useJUnitPlatform()
33+
}
34+
3135
dependencies {
3236
// Spring Data JPA
3337
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

src/main/java/org/tuna/zoopzoop/backend/BackendApplication.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
@SpringBootApplication
77
public class BackendApplication {
8-
98
public static void main(String[] args) {
109
SpringApplication.run(BackendApplication.class, args);
1110
}

src/main/java/org/tuna/zoopzoop/backend/domain/auth/handler/OAuth2SuccessHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
7070

7171
// 확장 프로그램에서 로그인 했을 경우.
7272
if(isExtension){
73-
String redirectUrl = redirect_domain + "/extension/callback "
73+
String redirectUrl = redirect_domain + "/extension/callback"
7474
+ "?success=true"
7575
+ "&accessToken=" + URLEncoder.encode(accessToken, "UTF-8")
7676
+ "&refreshToken=" + URLEncoder.encode(refreshToken, "UTF-8");
@@ -80,7 +80,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
8080

8181
if ("http://localhost:3000".equals(redirect_domain)) {
8282
// server 환경일 때: URL 파라미터로 토큰 전달
83-
String redirectUrl = redirect_domain + "/auth/callback"
83+
String redirectUrl = redirect_domain + "/api/auth/callback"
8484
+ "?success=true"
8585
+ "&accessToken=" + URLEncoder.encode(accessToken, "UTF-8")
8686
+ "&refreshToken=" + URLEncoder.encode(refreshToken, "UTF-8");
@@ -115,7 +115,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
115115

116116
// 로그인 성공 후 리다이렉트.
117117
// 배포 시에 프론트엔드와 조율이 필요한 부분일 듯 함.
118-
response.sendRedirect(redirect_domain + "/auth/callback");
118+
response.sendRedirect(redirect_domain + "/api/auth/callback");
119119
}
120120
// 보안을 좀 더 강화하고자 한다면 CSRF 토큰 같은 걸 생각해볼 수 있겠으나,
121121
// 일단은 구현하지 않음.(개발 과정 중에 번거로워질 수 있을 듯 함.)

src/main/java/org/tuna/zoopzoop/backend/domain/home/controller/HomeController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import lombok.SneakyThrows;
77
import org.springframework.web.bind.annotation.GetMapping;
88
import org.springframework.web.bind.annotation.RestController;
9-
import org.tuna.zoopzoop.backend.domain.news.service.NewsSearchService;
9+
import org.tuna.zoopzoop.backend.domain.news.service.NewsAPIService;
1010

1111
import java.net.InetAddress;
1212

@@ -22,7 +22,7 @@ public class HomeController {
2222
//
2323
// @Value("${kakao.redirect_uri}")
2424
// private String kakaoRedirectUri;
25-
private final NewsSearchService newsSearchService;
25+
private final NewsAPIService newsSearchService;
2626

2727
@SneakyThrows
2828
@GetMapping(produces = TEXT_HTML_VALUE)

src/main/java/org/tuna/zoopzoop/backend/domain/news/controller/ApiV1NewsController.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.http.HttpStatus;
77
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
89
import org.springframework.web.bind.annotation.*;
10+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
911
import org.tuna.zoopzoop.backend.domain.news.dto.req.ReqBodyForKeyword;
1012
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
11-
import org.tuna.zoopzoop.backend.domain.news.service.NewsSearchService;
13+
import org.tuna.zoopzoop.backend.domain.news.service.NewsAPIService;
14+
import org.tuna.zoopzoop.backend.domain.news.service.NewsService;
1215
import org.tuna.zoopzoop.backend.global.rsData.RsData;
16+
import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails;
1317
import reactor.core.publisher.Mono;
1418

19+
import java.util.List;
20+
1521
@RestController
1622
@RequiredArgsConstructor
1723
@RequestMapping("/api/v1/news")
1824
@Tag(name = "ApiV1NewsController", description = "뉴스 API 기반 검색 컨트롤러")
1925
public class ApiV1NewsController {
20-
private final NewsSearchService newsSearchService;
26+
private final NewsAPIService newsSearchService;
27+
private final NewsService newsService;
2128

2229
/**
2330
* 최신 뉴스 목록을 조회하는 API
@@ -47,7 +54,7 @@ public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchRecentNews(
4754
* @param dto 키워드를 받아오는 reqDto
4855
*/
4956
@PostMapping("/keywords")
50-
@Operation(summary = "최신 뉴스 목록 조회")
57+
@Operation(summary = "키워드 기반 뉴스 조회")
5158
public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByKeywords(
5259
@RequestBody ReqBodyForKeyword dto
5360
) {
@@ -64,4 +71,31 @@ public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByKeywords(
6471
response
6572
)));
6673
}
74+
75+
/**
76+
* 개인 아카이브의 폴더 내부의 자료를 기반으로 키워드를 추천해서 검색하는 뉴스 API
77+
* HTTP METHOD: GET
78+
* 한번에 100개를 조회 합니다.
79+
* @param userDetails 로그인한 사용자
80+
* @param folderId 대상 폴더 id
81+
*/
82+
@GetMapping("/recommends/personal/{folderId}")
83+
@Operation(summary = "개인 아카이브 뉴스 추천")
84+
public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByRecommends(
85+
@AuthenticationPrincipal CustomUserDetails userDetails,
86+
@PathVariable Integer folderId
87+
) {
88+
Member member = userDetails.getMember();
89+
List<String> frequency = newsService.getTagFrequencyFromFiles(member.getId(), folderId);
90+
String query = String.join(" ", frequency);
91+
92+
return newsSearchService.searchNews(query, "sim")
93+
.map(response -> ResponseEntity
94+
.status(HttpStatus.OK)
95+
.body(new RsData<>(
96+
"200",
97+
"키워드 기반 뉴스 목록을 조회했습니다.",
98+
response
99+
)));
100+
}
67101
}

src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsSearchService.java renamed to src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsAPIService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import java.util.List;
1111

1212
@Service
13-
public class NewsSearchService {
13+
public class NewsAPIService {
1414
private final WebClient webClient;
1515

1616
@Value("${naver.client_id}")
@@ -19,7 +19,7 @@ public class NewsSearchService {
1919
@Value("${naver.client_secret}")
2020
private String client_secret;
2121

22-
public NewsSearchService(WebClient.Builder webClientBuilder) {
22+
public NewsAPIService(WebClient.Builder webClientBuilder) {
2323
this.webClient = webClientBuilder.baseUrl("https://openapi.naver.com").build();
2424
}
2525

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.tuna.zoopzoop.backend.domain.news.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.stereotype.Service;
5+
import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService;
6+
import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary;
7+
import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto;
8+
9+
import java.util.Comparator;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.stream.Collectors;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class NewsService {
17+
private final FolderService folderService;
18+
19+
public List<String> getTagFrequencyFromFiles(Integer memberId, Integer folderId) {
20+
FolderFilesDto folderFilesDto = folderService.getFilesInFolderForPersonal(memberId, folderId);
21+
22+
List<FileSummary> files = folderFilesDto.files();
23+
24+
Map<String, Long> tags = files.stream()
25+
.flatMap(file -> file.tags().stream())
26+
.map(tag -> tag.getTagName())
27+
.collect(Collectors.groupingBy(
28+
tagName -> tagName,
29+
Collectors.counting()
30+
));
31+
32+
List<String> frequency = tags.entrySet().stream()
33+
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
34+
.limit(3)
35+
.map(Map.Entry::getKey)
36+
.toList();
37+
38+
return frequency;
39+
}
40+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.tuna.zoopzoop.backend.domain.news.service;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.test.context.ActiveProfiles;
8+
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
9+
import reactor.core.publisher.Mono;
10+
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
13+
@SpringBootTest
14+
@ActiveProfiles("test")
15+
class NewsAPIServiceTest {
16+
@Autowired
17+
private NewsAPIService newsSearchService;
18+
19+
@Test
20+
@DisplayName("뉴스 서비스 테스트 - 정상적인 JSON 구조 반환 여부 확인")
21+
void newsJsonStructureTest() {
22+
Mono<ResBodyForNaverNews> result = newsSearchService.searchNews("뉴스", "sim");
23+
24+
// JSON 구조 확인
25+
result.doOnNext(res -> {
26+
assertNotNull(res.total());
27+
assertNotNull(res.items());
28+
29+
res.items().forEach(item -> {
30+
assertNotNull(item.title());
31+
assertNotNull(item.link());
32+
assertNotNull(item.description());
33+
assertNotNull(item.pubDate());
34+
});
35+
}).block(); // Mono 블로킹.
36+
}
37+
}
Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,121 @@
11
package org.tuna.zoopzoop.backend.domain.news.service;
22

3+
import jakarta.transaction.Transactional;
4+
import org.junit.jupiter.api.AfterEach;
5+
import org.junit.jupiter.api.BeforeEach;
36
import org.junit.jupiter.api.DisplayName;
47
import org.junit.jupiter.api.Test;
58
import org.springframework.beans.factory.annotation.Autowired;
69
import org.springframework.boot.test.context.SpringBootTest;
710
import org.springframework.test.context.ActiveProfiles;
8-
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
9-
import reactor.core.publisher.Mono;
11+
import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse;
12+
import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder;
13+
import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository;
14+
import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService;
15+
import org.tuna.zoopzoop.backend.domain.datasource.entity.Category;
16+
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
17+
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
18+
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository;
19+
import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService;
20+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
21+
import org.tuna.zoopzoop.backend.domain.member.enums.Provider;
22+
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
23+
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
1024

11-
import static org.junit.jupiter.api.Assertions.assertNotNull;
25+
import java.time.LocalDate;
26+
import java.util.List;
27+
import java.util.Map;
1228

29+
import static org.junit.jupiter.api.Assertions.assertEquals;
1330

1431
@SpringBootTest
1532
@ActiveProfiles("test")
16-
class NewsServiceTest {
33+
@Transactional
34+
public class NewsServiceTest {
1735
@Autowired
18-
private NewsSearchService newsSearchService;
36+
private NewsService newsService;
37+
38+
@Autowired
39+
private NewsAPIService newsAPIService;
40+
41+
@Autowired
42+
private MemberService memberService;
43+
44+
@Autowired
45+
private MemberRepository memberRepository;
46+
47+
@Autowired
48+
private FolderService folderService;
49+
50+
@Autowired
51+
private FolderRepository folderRepository;
52+
53+
@Autowired
54+
private DataSourceService dataSourceService;
55+
56+
@Autowired
57+
private DataSourceRepository dataSourceRepository;
58+
59+
private final Map<Integer, List<Tag>> tags = Map.ofEntries(
60+
Map.entry(1, List.of(new Tag("A"), new Tag("B"), new Tag("E"))),
61+
Map.entry(2, List.of(new Tag("B"), new Tag("E"), new Tag("F"))),
62+
Map.entry(3, List.of(new Tag("E"), new Tag("F"))),
63+
Map.entry(4, List.of(new Tag("A"), new Tag("D"), new Tag("E"))),
64+
Map.entry(5, List.of(new Tag("B"), new Tag("F"))),
65+
Map.entry(6, List.of(new Tag("E"), new Tag("F"), new Tag("B"))),
66+
Map.entry(7, List.of(new Tag("D"), new Tag("E"))),
67+
Map.entry(8, List.of(new Tag("B"), new Tag("E"))),
68+
Map.entry(9, List.of(new Tag("F"), new Tag("E"))),
69+
Map.entry(10, List.of(new Tag("C"), new Tag("E")))
70+
);
71+
// A = 2회, B = 5회, C = 1회, D = 2회, E = 9회, F = 5회
72+
73+
private DataSource buildDataSource(String title, Folder folder, String sourceUrl, List<Tag> tags) {
74+
DataSource ds = new DataSource();
75+
ds.setFolder(folder);
76+
ds.setSourceUrl(sourceUrl);
77+
ds.setTitle(title);
78+
ds.setSource("www.examplesource.com");
79+
ds.setSummary("설명");
80+
ds.setImageUrl("www.example.com/img");
81+
ds.setDataCreatedDate(LocalDate.now());
82+
ds.setTags(tags);
83+
ds.setCategory(Category.ENVIRONMENT);
84+
ds.setActive(true);
85+
return dataSourceRepository.save(ds);
86+
}
87+
88+
@AfterEach
89+
void cleanUp() {
90+
memberRepository.deleteAll();
91+
}
92+
93+
@BeforeEach
94+
public void setUp() {
95+
Member member = memberService.createMember(
96+
"newsServiceTestMember",
97+
"url",
98+
"newsServiceTestKey",
99+
Provider.KAKAO
100+
);
101+
102+
FolderResponse folderResponse = folderService.createFolderForPersonal(member.getId(), "newServiceTestFolder");
103+
Folder folder = folderRepository.findById(folderResponse.folderId()).orElse(null);
104+
105+
for(int i = 1; i <= 10; i++) {
106+
buildDataSource(String.valueOf(i), folder, String.valueOf(i), tags.get(i));
107+
}
108+
}
19109

20110
@Test
21-
@DisplayName("뉴스 서비스 테스트 - 정상적인 JSON 구조 반환 여부 확인")
22-
void newsJsonStructureTest() {
23-
Mono<ResBodyForNaverNews> result = newsSearchService.searchNews("뉴스", "sim");
24-
25-
// JSON 구조 확인
26-
result.doOnNext(res -> {
27-
assertNotNull(res.total());
28-
assertNotNull(res.items());
29-
30-
res.items().forEach(item -> {
31-
assertNotNull(item.title());
32-
assertNotNull(item.link());
33-
assertNotNull(item.description());
34-
assertNotNull(item.pubDate());
35-
});
36-
}).block(); // Mono 블로킹.
111+
@DisplayName("태그 빈도 수 추출 테스트")
112+
void DataSourceExtractTagsTest(){
113+
Member member = memberService.findByProviderKey("newsServiceTestKey");
114+
List<FolderResponse> folderResponses = folderService.getFoldersForPersonal(member.getId());
115+
List<String> frequency = newsService.getTagFrequencyFromFiles(member.getId(), folderResponses.get(0).folderId());
116+
117+
assertEquals("E", frequency.get(0));
118+
assertEquals("B", frequency.get(1));
119+
assertEquals("F", frequency.get(2));
37120
}
38-
}
121+
}

0 commit comments

Comments
 (0)