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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ ext {
springAiVersion = "1.0.0"
}

test {
useJUnitPlatform()
}

dependencies {
// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

@SpringBootApplication
public class BackendApplication {

public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo

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

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

// 로그인 성공 후 리다이렉트.
// 배포 시에 프론트엔드와 조율이 필요한 부분일 듯 함.
response.sendRedirect(redirect_domain + "/auth/callback");
response.sendRedirect(redirect_domain + "/api/auth/callback");
}
// 보안을 좀 더 강화하고자 한다면 CSRF 토큰 같은 걸 생각해볼 수 있겠으나,
// 일단은 구현하지 않음.(개발 과정 중에 번거로워질 수 있을 듯 함.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.tuna.zoopzoop.backend.domain.news.service.NewsSearchService;
import org.tuna.zoopzoop.backend.domain.news.service.NewsAPIService;

import java.net.InetAddress;

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

@SneakyThrows
@GetMapping(produces = TEXT_HTML_VALUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
import org.tuna.zoopzoop.backend.domain.news.dto.req.ReqBodyForKeyword;
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
import org.tuna.zoopzoop.backend.domain.news.service.NewsSearchService;
import org.tuna.zoopzoop.backend.domain.news.service.NewsAPIService;
import org.tuna.zoopzoop.backend.domain.news.service.NewsService;
import org.tuna.zoopzoop.backend.global.rsData.RsData;
import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails;
import reactor.core.publisher.Mono;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/news")
@Tag(name = "ApiV1NewsController", description = "뉴스 API 기반 검색 컨트롤러")
public class ApiV1NewsController {
private final NewsSearchService newsSearchService;
private final NewsAPIService newsSearchService;
private final NewsService newsService;

/**
* 최신 뉴스 목록을 조회하는 API
Expand Down Expand Up @@ -47,7 +54,7 @@ public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchRecentNews(
* @param dto 키워드를 받아오는 reqDto
*/
@PostMapping("/keywords")
@Operation(summary = "최신 뉴스 목록 조회")
@Operation(summary = "키워드 기반 뉴스 조회")
public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByKeywords(
@RequestBody ReqBodyForKeyword dto
) {
Expand All @@ -64,4 +71,31 @@ public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByKeywords(
response
)));
}

/**
* 개인 아카이브의 폴더 내부의 자료를 기반으로 키워드를 추천해서 검색하는 뉴스 API
* HTTP METHOD: GET
* 한번에 100개를 조회 합니다.
* @param userDetails 로그인한 사용자
* @param folderId 대상 폴더 id
*/
@GetMapping("/recommends/personal/{folderId}")
@Operation(summary = "개인 아카이브 뉴스 추천")
public Mono<ResponseEntity<RsData<ResBodyForNaverNews>>> searchNewsByRecommends(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Integer folderId
) {
Member member = userDetails.getMember();
List<String> frequency = newsService.getTagFrequencyFromFiles(member.getId(), folderId);
String query = String.join(" ", frequency);

return newsSearchService.searchNews(query, "sim")
.map(response -> ResponseEntity
.status(HttpStatus.OK)
.body(new RsData<>(
"200",
"키워드 기반 뉴스 목록을 조회했습니다.",
response
)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.util.List;

@Service
public class NewsSearchService {
public class NewsAPIService {
private final WebClient webClient;

@Value("${naver.client_id}")
Expand All @@ -19,7 +19,7 @@ public class NewsSearchService {
@Value("${naver.client_secret}")
private String client_secret;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.tuna.zoopzoop.backend.domain.news.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService;
import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary;
import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class NewsService {
private final FolderService folderService;

public List<String> getTagFrequencyFromFiles(Integer memberId, Integer folderId) {
FolderFilesDto folderFilesDto = folderService.getFilesInFolderForPersonal(memberId, folderId);

List<FileSummary> files = folderFilesDto.files();

Map<String, Long> tags = files.stream()
.flatMap(file -> file.tags().stream())
.map(tag -> tag.getTagName())
.collect(Collectors.groupingBy(
tagName -> tagName,
Collectors.counting()
));

List<String> frequency = tags.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(3)
.map(Map.Entry::getKey)
.toList();

return frequency;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.tuna.zoopzoop.backend.domain.news.service;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
import reactor.core.publisher.Mono;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
@ActiveProfiles("test")
class NewsAPIServiceTest {
@Autowired
private NewsAPIService newsSearchService;

@Test
@DisplayName("뉴스 서비스 테스트 - 정상적인 JSON 구조 반환 여부 확인")
void newsJsonStructureTest() {
Mono<ResBodyForNaverNews> result = newsSearchService.searchNews("뉴스", "sim");

// JSON 구조 확인
result.doOnNext(res -> {
assertNotNull(res.total());
assertNotNull(res.items());

res.items().forEach(item -> {
assertNotNull(item.title());
assertNotNull(item.link());
assertNotNull(item.description());
assertNotNull(item.pubDate());
});
}).block(); // Mono 블로킹.
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,121 @@
package org.tuna.zoopzoop.backend.domain.news.service;

import jakarta.transaction.Transactional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.tuna.zoopzoop.backend.domain.news.dto.res.ResBodyForNaverNews;
import reactor.core.publisher.Mono;
import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse;
import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder;
import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository;
import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService;
import org.tuna.zoopzoop.backend.domain.datasource.entity.Category;
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository;
import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService;
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
import org.tuna.zoopzoop.backend.domain.member.enums.Provider;
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@ActiveProfiles("test")
class NewsServiceTest {
@Transactional
public class NewsServiceTest {
@Autowired
private NewsSearchService newsSearchService;
private NewsService newsService;

@Autowired
private NewsAPIService newsAPIService;

@Autowired
private MemberService memberService;

@Autowired
private MemberRepository memberRepository;

@Autowired
private FolderService folderService;

@Autowired
private FolderRepository folderRepository;

@Autowired
private DataSourceService dataSourceService;

@Autowired
private DataSourceRepository dataSourceRepository;

private final Map<Integer, List<Tag>> tags = Map.ofEntries(
Map.entry(1, List.of(new Tag("A"), new Tag("B"), new Tag("E"))),
Map.entry(2, List.of(new Tag("B"), new Tag("E"), new Tag("F"))),
Map.entry(3, List.of(new Tag("E"), new Tag("F"))),
Map.entry(4, List.of(new Tag("A"), new Tag("D"), new Tag("E"))),
Map.entry(5, List.of(new Tag("B"), new Tag("F"))),
Map.entry(6, List.of(new Tag("E"), new Tag("F"), new Tag("B"))),
Map.entry(7, List.of(new Tag("D"), new Tag("E"))),
Map.entry(8, List.of(new Tag("B"), new Tag("E"))),
Map.entry(9, List.of(new Tag("F"), new Tag("E"))),
Map.entry(10, List.of(new Tag("C"), new Tag("E")))
);
// A = 2회, B = 5회, C = 1회, D = 2회, E = 9회, F = 5회

private DataSource buildDataSource(String title, Folder folder, String sourceUrl, List<Tag> tags) {
DataSource ds = new DataSource();
ds.setFolder(folder);
ds.setSourceUrl(sourceUrl);
ds.setTitle(title);
ds.setSource("www.examplesource.com");
ds.setSummary("설명");
ds.setImageUrl("www.example.com/img");
ds.setDataCreatedDate(LocalDate.now());
ds.setTags(tags);
ds.setCategory(Category.ENVIRONMENT);
ds.setActive(true);
return dataSourceRepository.save(ds);
}

@AfterEach
void cleanUp() {
memberRepository.deleteAll();
}

@BeforeEach
public void setUp() {
Member member = memberService.createMember(
"newsServiceTestMember",
"url",
"newsServiceTestKey",
Provider.KAKAO
);

FolderResponse folderResponse = folderService.createFolderForPersonal(member.getId(), "newServiceTestFolder");
Folder folder = folderRepository.findById(folderResponse.folderId()).orElse(null);

for(int i = 1; i <= 10; i++) {
buildDataSource(String.valueOf(i), folder, String.valueOf(i), tags.get(i));
}
}

@Test
@DisplayName("뉴스 서비스 테스트 - 정상적인 JSON 구조 반환 여부 확인")
void newsJsonStructureTest() {
Mono<ResBodyForNaverNews> result = newsSearchService.searchNews("뉴스", "sim");

// JSON 구조 확인
result.doOnNext(res -> {
assertNotNull(res.total());
assertNotNull(res.items());

res.items().forEach(item -> {
assertNotNull(item.title());
assertNotNull(item.link());
assertNotNull(item.description());
assertNotNull(item.pubDate());
});
}).block(); // Mono 블로킹.
@DisplayName("태그 빈도 수 추출 테스트")
void DataSourceExtractTagsTest(){
Member member = memberService.findByProviderKey("newsServiceTestKey");
List<FolderResponse> folderResponses = folderService.getFoldersForPersonal(member.getId());
List<String> frequency = newsService.getTagFrequencyFromFiles(member.getId(), folderResponses.get(0).folderId());

assertEquals("E", frequency.get(0));
assertEquals("B", frequency.get(1));
assertEquals("F", frequency.get(2));
}
}
}