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 @@ -35,30 +35,40 @@ public interface AiControllerDocs {
""")
)
),
@ApiResponse(responseCode = "400", description = "해당 언론사는 요약 정보 제공이 불가능합니다.",
@ApiResponse(responseCode = "400", description = "크롤링 불가 (본문 서두 제공)",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"timestamp": "2025-04-01T13:37:56.6982049",
"isSuccess": false,
"message": "해당 언론사는 요약 정보 제공이 불가능합니다.",
"message": "해당 언론사는 요약 정보 제공이 불가능합니다. (크롤링 불가)",
"data": "NEWARK With a mastery of collaborative, often pretty basketball that belied both its youth and the volatile state of the college sport, Duke soared to the programs 18th Final Four on Saturday night, … [+5884 chars]"
}
""")
)
),
@ApiResponse(responseCode = "500", description = "서버 에러가 발생하였습니다.",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "서버 에러가 발생하였습니다.",
"data": null
}
""")
examples = {
@ExampleObject(name="서버 에러", value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "서버 에러가 발생하였습니다.",
"data": null
}
"""),
@ExampleObject(name = "gpt 에러", value = """
{
"timestamp": "2024-10-30T15:40:00.12345678",
"isSuccess": false,
"message": "GPT 요약 중 에러가 발생하였습니다.",
"data": null
}
""")
}
)
),
)
})
public ResponseEntity<?> summarizeArticle(
@Parameter(description = "뉴스기사 ID", example = "1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public enum NewsErrorStatus implements BaseResponse {

_EMPTY_NEWS_DATA(HttpStatus.BAD_REQUEST, "해당 기사의 정보가 없습니다,"),

_CRAWLER_ERROR(HttpStatus.BAD_REQUEST, "해당 언론사는 요약 정보 제공이 불가능합니다."),
_CRAWLER_ERROR(HttpStatus.BAD_REQUEST, "해당 언론사는 요약 정보 제공이 불가능합니다. (크롤링 불가)"),

_GPT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "GPT 요약 중 에러가 발생하였습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.globalTimes_be.domain.news.service;

import com.example.globalTimes_be.domain.news.exception.NewsErrorStatus;
import com.example.globalTimes_be.global.exception.BaseException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
Expand Down Expand Up @@ -43,6 +45,6 @@ private String extractContent(Map<String, Object> response) {
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return (String) message.get("content");
}
return "응답을 처리할 수 없습니다.";
throw new BaseException(NewsErrorStatus._GPT_ERROR.getResponse());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ public String getArticleCrawledContent(Long id) {
Article article = articleRepository.findById(id)
.orElseThrow(() -> new BaseException(NewsErrorStatus._EMPTY_NEWS_DATA.getResponse()));

System.out.println("newsEntity: " + article.getUrl());

String crawledContent = article.getCrawledContent();

//content가 null이면 크롤링 해오고 db에 저장
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.globalTimes_be.domain.trend.controller;

import com.example.globalTimes_be.domain.trend.exception.TrendSuccessStatus;
import com.example.globalTimes_be.domain.trend.service.TrendAiService;
import com.example.globalTimes_be.domain.trend.service.TrendCrawledService;
import com.example.globalTimes_be.global.apiPayload.code.ApiResponse;
import com.example.globalTimes_be.global.apiPayload.code.status.GlobalSuccessStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/trend")
public class TrendAiController implements TrendAiControllerDocs{
private final TrendCrawledService trendCrawledService;
private final TrendAiService trendAiService;

//기사의 url을 받으면 요약해주는 api 제공
@Override
@GetMapping("/summary")
public ResponseEntity<ApiResponse> getSummarizeTrendArticle (@RequestParam String url,
@RequestParam(defaultValue = "영어") String language) {
//본문 크롤링
String content = trendCrawledService.getArticleCrawledContent(url);
//크롤링한 본문 요약
String summary = trendAiService.summarizeTrendArticle(content, language);
return ApiResponse.success(GlobalSuccessStatus._OK.getResponse(), summary);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.globalTimes_be.domain.trend.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "실시간 검색어 페이지", description = "실시간 검색어 관련 API입니다.")
public interface TrendAiControllerDocs {

@Operation(summary = "기사 요약",
description = "해당 기사에 대한 요약을 한번에 반환합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "응답 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = com.example.globalTimes_be.global.apiPayload.code.ApiResponse.class),
examples = @ExampleObject(value = """
{
"timestamp": "2025-04-01T15:41:45.2180857",
"isSuccess": true,
"message": "응답에 성공했습니다.",
"data": "국가정보원이 최근 5년간 공공기관의 인공지능(AI) 정보화사업 실태를 조사하기 시작했으며, 이는 AI 기술의 안전한 운용과 보안 강화 방안을 모색하기 위한 차원이다. 이번 조사는 공공기관의 AI 활용 확대에 따른 보안 체계 정비를 목적으로 하고 있다."
}
""")
)
),
@ApiResponse(responseCode = "400", description = "크롤링 불가",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"timestamp": "2025-04-01T13:37:56.6982049",
"isSuccess": false,
"message": "해당 언론사는 요약 정보 제공이 불가능합니다. (크롤링 불가)"
}
""")
)
),
@ApiResponse(responseCode = "500", description = "서버 에러가 발생하였습니다.",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(name="서버 에러", value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "서버 에러가 발생하였습니다.",
"data": null
}
"""),
@ExampleObject(name = "gpt 에러", value = """
{
"timestamp": "2024-10-30T15:40:00.12345678",
"isSuccess": false,
"message": "GPT 요약 중 에러가 발생하였습니다.",
"data": null
}
""")
}
)
)
})
public ResponseEntity<?> getSummarizeTrendArticle(
@Parameter(description = "뉴스기사 url", example = "https://www.etnews.com/20...")
@NotNull(message = "뉴스기사 url은 비어있을 수 없습니다.")
@PathVariable String url,

@Parameter(description = "언어 설정 (기본값: 영어)", examples = {
@ExampleObject(name = "영어", value = "영어"),
@ExampleObject(name = "한국어", value = "한국어"),
@ExampleObject(name = "중국어", value = "중국어")})
@RequestParam String language);

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "실시간 검색어", description = "실시간 검색어 관련 API입니다.")
@Tag(name = "실시간 검색어 페이지", description = "실시간 검색어 관련 API입니다.")
public interface TrendControllerDocs {

@Operation(summary = "나라별 실시간 검색어 데이터 응답",
Expand Down Expand Up @@ -58,28 +58,26 @@ public interface TrendControllerDocs {
""")
)
),
@ApiResponse(responseCode = "400", description = "역직렬화에 실패하였습니다.",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "역직렬화에 실패하였습니다.",
"data": null
}
""")
)
),

@ApiResponse(responseCode = "500", description = "서버 에러가 발생하였습니다.",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "서버 에러가 발생하였습니다.",
"data": null
}
""")
examples = {@ExampleObject(name="서버에러", value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "서버 에러가 발생하였습니다.",
"data": null
}
"""),
@ExampleObject(name="역직렬화 실패", value = """
{
"timestamp": "2024-10-30T15:38:12.43483271",
"isSuccess": false,
"message": "역직렬화에 실패하였습니다.",
"data": null
}
""")
}
)
),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ public enum TrendErrorStatus implements BaseResponse {
_CUSTOM_ERROR(HttpStatus.BAD_REQUEST, "에러테스트 요청입니다."),

_INVALID_COUNTRY_CODE(HttpStatus.BAD_REQUEST, "잘못된 국가 코드입니다."),
_FAIL_SERIALIZATION(HttpStatus.BAD_REQUEST, "직렬화에 실패하였습니다."),
_FAIL_DESERIALIZATION(HttpStatus.BAD_REQUEST, "역직렬화에 실패하였습니다."),
_FAIL_SERIALIZATION(HttpStatus.INTERNAL_SERVER_ERROR, "직렬화에 실패하였습니다."),
_FAIL_DESERIALIZATION(HttpStatus.INTERNAL_SERVER_ERROR, "역직렬화에 실패하였습니다."),

_CRAWLER_ERROR(HttpStatus.BAD_REQUEST, "해당 언론사는 요약 정보 제공이 불가능합니다. (크롤링 불가)"),

_GPT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "GPT 요약 중 에러가 발생하였습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.example.globalTimes_be.domain.trend.service;

import com.example.globalTimes_be.domain.trend.exception.TrendErrorStatus;
import com.example.globalTimes_be.global.exception.BaseException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.List;
import java.util.Map;

@RequiredArgsConstructor
@Service
public class TrendAiService {
private final WebClient openAiWebClient;

public String summarizeTrendArticle (String content, String language) {
String summary = openAiWebClient.post()
.uri("/chat/completions")
.bodyValue(createRequestBody(content, language)) // 요청 본문
.retrieve()
.bodyToMono(Map.class) // 전체 응답을 한 번에 받음
.map(response -> extractContent(response)) // 응답 본문에서 요약 내용 추출
.block();
return summary;
}

// OpenAI 요청 본문 생성 (기사 요약)
private Map<String, Object> createRequestBody(String content, String language) {
return Map.of(
"model", "gpt-4o-mini",
"messages", List.of(
Map.of("role", "system", "content", "이 기사를 " + language + "로 2줄 요약해줘."),
Map.of("role", "user", "content", content)
),
"stream", false // 🔹 스트리밍 비활성화
);
}

// OpenAI 응답에서 'content' 추출
private String extractContent(Map<String, Object> response) {
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return (String) message.get("content");
}
throw new BaseException(TrendErrorStatus._GPT_ERROR.getResponse());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.globalTimes_be.domain.trend.service;

import com.example.globalTimes_be.domain.trend.exception.TrendErrorStatus;
import com.example.globalTimes_be.global.exception.BaseException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
@Service
public class TrendCrawledService {

//url 본문 기사 크롤링
public String getArticleCrawledContent(String url){
try {
// URL에서 HTML 문서 가져오기
Document doc = Jsoup.connect(url).get();

// 모든 <p> 태그 가져오기
Elements paragraphs = doc.select("p");

// 텍스트만 추출해서 하나의 문자열로 반환 (줄바꿈 포함)
return paragraphs.stream()
.map(Element::text)
.reduce((p1, p2) -> p1 + "\n" + p2) // 문장마다 줄바꿈 추가
.orElse(null);

} catch (IOException e) {
log.error("크롤링에 실패했습니다. \n{}", e.getMessage());
throw new BaseException(TrendErrorStatus._CRAWLER_ERROR.getResponse());
}
}
}