Skip to content

Commit 1c44df3

Browse files
authored
Merge pull request #10 from 508PERFACT/refact/9-newsreport-refactor
Refact : 리포트, 뉴스 도메인 리팩토링(Converter 분리, 예외처리, 문서화 등)
2 parents a9f7f06 + 7e91afc commit 1c44df3

30 files changed

+1050
-260
lines changed

src/main/java/com/perfact/be/domain/news/config/SelectorConfig.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,30 +47,22 @@ public class SelectorConfig {
4747
".article-time" // 기사 시간 클래스
4848
);
4949

50-
/**
51-
* 제목 셀렉터 배열을 반환합니다.
52-
*/
50+
// 제목 셀렉터 배열 밴환
5351
public String[] getTitleSelectors() {
5452
return TITLE_SELECTORS.toArray(new String[0]);
5553
}
5654

57-
/**
58-
* 다른 뉴스 사이트 제목 셀렉터 배열을 반환합니다.
59-
*/
55+
// 다른 뉴스 사이트 제목 셀렉터 배열 반환
6056
public String[] getOtherNewsTitleSelectors() {
6157
return OTHER_NEWS_TITLE_SELECTORS.toArray(new String[0]);
6258
}
6359

64-
/**
65-
* 내용 셀렉터 배열을 반환합니다.
66-
*/
60+
// 내용 셀렉터 배열 반환
6761
public String[] getContentSelectors() {
6862
return CONTENT_SELECTORS.toArray(new String[0]);
6963
}
7064

71-
/**
72-
* 날짜 셀렉터 배열을 반환합니다.
73-
*/
65+
// 날짜 셀렉터 배열 반환
7466
public String[] getDateSelectors() {
7567
return DATE_SELECTORS.toArray(new String[0]);
7668
}

src/main/java/com/perfact/be/domain/news/controller/NewsController.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import com.perfact.be.domain.news.dto.NewsArticleResponse;
44
import com.perfact.be.domain.news.service.NewsService;
55
import com.perfact.be.global.apiPayload.ApiResponse;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
69
import lombok.RequiredArgsConstructor;
710
import lombok.extern.slf4j.Slf4j;
811
import org.springframework.web.bind.annotation.*;
912

13+
@Tag(name = "News", description = "뉴스 관련 API")
1014
@Slf4j
1115
@RestController
1216
@RequestMapping("/api/news")
@@ -15,14 +19,18 @@ public class NewsController {
1519

1620
private final NewsService newsService;
1721

22+
@Operation(summary = "뉴스 기사 내용 추출", description = "네이버 뉴스 URL을 입력받아 기사의 제목, 날짜, 내용을 추출합니다.")
1823
@GetMapping("/article-content")
19-
public ApiResponse<NewsArticleResponse> getNewsArticleContent(@RequestParam String url) {
24+
public ApiResponse<NewsArticleResponse> getNewsArticleContent(
25+
@Parameter(description = "네이버 뉴스 URL", required = true, example = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=100&oid=001&aid=0012345678") @RequestParam String url) {
2026
NewsArticleResponse response = newsService.extractNaverNewsArticle(url);
2127
return ApiResponse.onSuccess(response);
2228
}
2329

30+
@Operation(summary = "네이버 뉴스 검색", description = "검색어를 입력받아 네이버 뉴스 검색 결과를 반환합니다.")
2431
@GetMapping("/search")
25-
public ApiResponse<String> searchNaverNews(@RequestParam String query) {
32+
public ApiResponse<String> searchNaverNews(
33+
@Parameter(description = "검색할 키워드", required = true, example = "AI 기술") @RequestParam String query) {
2634
String searchResult = newsService.searchNaverNews(query);
2735
return ApiResponse.onSuccess(searchResult);
2836
}

src/main/java/com/perfact/be/domain/news/converter/NewsConverter.java

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,16 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.springframework.stereotype.Component;
66

7-
/**
8-
* News 도메인의 데이터 변환을 담당하는 Converter
9-
*/
107
@Slf4j
118
@Component
129
public class NewsConverter {
1310

14-
/**
15-
* 뉴스 데이터를 NewsArticleResponse로 변환
16-
*/
1711
public NewsArticleResponse toNewsArticleResponse(String title, String date, String content) {
1812
try {
1913
return new NewsArticleResponse(title, date, content);
2014
} catch (Exception e) {
15+
log.error("NewsArticleResponse 변환 실패: {}", e.getMessage(), e);
2116
throw new RuntimeException("NewsArticleResponse 변환 실패", e);
2217
}
2318
}
24-
25-
/**
26-
* 뉴스 데이터를 JSON 형태로 변환
27-
*/
28-
public String toJsonString(NewsArticleResponse newsData) {
29-
try {
30-
return String.format(
31-
"{\"title\": \"%s\", \"date\": \"%s\", \"content\": \"%s\"}",
32-
escapeJsonString(newsData.getTitle()),
33-
escapeJsonString(newsData.getDate()),
34-
escapeJsonString(newsData.getContent()));
35-
} catch (Exception e) {
36-
throw new RuntimeException("JSON 변환 실패", e);
37-
}
38-
}
39-
40-
/**
41-
* JSON 문자열에서 특수문자 이스케이프 처리
42-
*/
43-
private String escapeJsonString(String input) {
44-
if (input == null) {
45-
return "";
46-
}
47-
return input.replace("\\", "\\\\")
48-
.replace("\"", "\\\"")
49-
.replace("\n", "\\n")
50-
.replace("\r", "\\r")
51-
.replace("\t", "\\t");
52-
}
5319
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.perfact.be.domain.news.dto;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import lombok.AllArgsConstructor;
45
import lombok.Getter;
56
import lombok.NoArgsConstructor;
67

78
@Getter
89
@NoArgsConstructor
910
@AllArgsConstructor
11+
@Schema(description = "뉴스 기사 응답 DTO")
1012
public class NewsArticleResponse {
13+
14+
@Schema(description = "뉴스 기사 제목", example = "AI 기술 발전으로 인한 산업 변화")
1115
private String title;
16+
17+
@Schema(description = "뉴스 발행일", example = "2025-08-06")
1218
private String date;
19+
20+
@Schema(description = "뉴스 기사 본문 내용", example = "최근 AI 기술의 급속한 발전으로 인해...")
1321
private String content;
1422
}

src/main/java/com/perfact/be/domain/news/exception/NewsExceptionHandler.java

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,47 @@
11
package com.perfact.be.domain.news.exception;
22

33
import com.perfact.be.domain.news.exception.status.NewsErrorStatus;
4-
import com.perfact.be.global.exception.GeneralException;
54
import lombok.extern.slf4j.Slf4j;
65
import org.springframework.stereotype.Component;
76

87
@Slf4j
98
@Component
109
public class NewsExceptionHandler {
1110

12-
/**
13-
* 뉴스 파싱 실패 시 예외를 처리합니다.
14-
*/
11+
// 뉴스 파싱 실패 예외 처리
1512
public void handleParsingFailure(String url, String operation, Exception e) {
1613
log.error("Failed to {} for URL: {}", operation, url, e);
17-
throw new GeneralException(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
14+
throw new NewsHandler(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
1815
}
1916

20-
/**
21-
* 뉴스 내용을 찾을 수 없을 때 예외를 처리합니다.
22-
*/
17+
// 뉴스 내용 찾을 수 없을 때 예외 처리
2318
public void handleContentNotFound(String url, String operation) {
2419
log.warn("Content not found during {} for URL: {}", operation, url);
25-
throw new GeneralException(NewsErrorStatus.NEWS_CONTENT_NOT_FOUND);
20+
throw new NewsHandler(NewsErrorStatus.NEWS_CONTENT_NOT_FOUND);
2621
}
2722

28-
/**
29-
* 뉴스 제목 추출 실패 시 예외를 처리합니다.
30-
*/
23+
// 뉴스 제목 추출 실패 예외 처리
3124
public void handleTitleExtractionFailure(String url, String operation, Exception e) {
3225
log.error("Failed to extract title during {} for URL: {}", operation, url, e);
33-
throw new GeneralException(NewsErrorStatus.NEWS_TITLE_EXTRACTION_FAILED);
26+
throw new NewsHandler(NewsErrorStatus.NEWS_TITLE_EXTRACTION_FAILED);
3427
}
3528

36-
/**
37-
* 네이버 API 호출 실패 시 예외를 처리합니다.
38-
*/
29+
// 네이버 API 호출 실패 예외 처리
3930
public void handleNaverApiFailure(String query, Exception e) {
4031
log.error("Failed to call Naver API for query: {}", query, e);
41-
throw new GeneralException(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
32+
throw new NewsHandler(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
4233
}
4334

44-
/**
45-
* 안전한 텍스트 추출을 수행합니다. 실패 시 null을 반환합니다.
46-
*/
35+
// 안전한 텍스트 추출 수행 실패 시 null 반환
4736
public String safeExtractText(String url, String operation, TextExtractor extractor) {
4837
try {
4938
return extractor.extract();
5039
} catch (Exception e) {
40+
log.warn("Text extraction failed during {} for URL: {}", operation, url, e);
5141
return null;
5242
}
5343
}
5444

55-
/**
56-
* 텍스트 추출을 위한 함수형 인터페이스
57-
*/
5845
@FunctionalInterface
5946
public interface TextExtractor {
6047
String extract() throws Exception;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.perfact.be.domain.news.exception;
2+
3+
import com.perfact.be.global.apiPayload.code.BaseErrorCode;
4+
import com.perfact.be.global.exception.GeneralException;
5+
6+
public class NewsHandler extends GeneralException {
7+
public NewsHandler(BaseErrorCode code) {
8+
super(code);
9+
}
10+
}

src/main/java/com/perfact/be/domain/news/service/DateExtractorServiceImpl.java

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package com.perfact.be.domain.news.service;
22

3+
import com.perfact.be.domain.news.config.SelectorConfig;
34
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
46
import org.jsoup.nodes.Document;
57
import org.jsoup.nodes.Element;
68
import org.springframework.stereotype.Service;
79

810
import java.util.regex.Matcher;
911
import java.util.regex.Pattern;
1012

13+
@Slf4j
1114
@Service
1215
@RequiredArgsConstructor
1316
public class DateExtractorServiceImpl implements DateExtractorService {
1417

1518
private final HtmlParserService htmlParserService;
16-
private final com.perfact.be.domain.news.config.SelectorConfig selectorConfig;
19+
private final SelectorConfig selectorConfig;
1720

1821
@Override
1922
public String extractArticleDate(String url) {
@@ -42,36 +45,38 @@ public String extractArticleDate(String url) {
4245
}
4346
}
4447

48+
log.warn("날짜 정보를 찾을 수 없습니다 - URL: {}", url);
4549
return "날짜 정보 없음";
4650
}
4751

4852
return extractDateFromElement(dateElement);
4953

5054
} catch (Exception e) {
55+
log.error("날짜 추출 중 예외 발생 - URL: {}, 에러: {}", url, e.getMessage(), e);
5156
return "날짜 정보 없음";
5257
}
5358
}
5459

5560
// 날짜 파싱
5661
private String extractDateFromElement(Element dateElement) {
57-
String dataDateTime = dateElement.attr("data-date-time");
58-
if (!dataDateTime.isEmpty()) {
59-
String[] parts = dataDateTime.split(" ");
60-
if (parts.length > 0) {
61-
String datePart = parts[0];
62-
return datePart.replace("-", ".");
63-
}
64-
}
62+
String dateText = dateElement.text().trim();
6563

66-
String text = dateElement.text();
67-
if (!text.isEmpty()) {
68-
Pattern pattern = Pattern.compile("(\\d{4}\\.\\d{2}\\.\\d{2})");
69-
Matcher matcher = pattern.matcher(text);
70-
if (matcher.find()) {
71-
return matcher.group(1);
72-
}
64+
// 정규식을 사용하여 날짜 패턴 찾기
65+
Pattern pattern = Pattern.compile("(\\d{4})[.-](\\d{1,2})[.-](\\d{1,2})");
66+
Matcher matcher = pattern.matcher(dateText);
67+
68+
if (matcher.find()) {
69+
String year = matcher.group(1);
70+
String month = matcher.group(2);
71+
String day = matcher.group(3);
72+
73+
// 월과 일이 한 자리인 경우 앞에 0 추가
74+
month = month.length() == 1 ? "0" + month : month;
75+
day = day.length() == 1 ? "0" + day : day;
76+
77+
return year + "-" + month + "-" + day;
7378
}
7479

75-
return "날짜 정보 없음";
80+
return dateText;
7681
}
7782
}

src/main/java/com/perfact/be/domain/news/service/HtmlParserServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.perfact.be.domain.news.service;
22

3+
import com.perfact.be.domain.news.exception.NewsHandler;
34
import com.perfact.be.domain.news.exception.status.NewsErrorStatus;
4-
import com.perfact.be.global.exception.GeneralException;
55
import org.jsoup.Jsoup;
66
import org.jsoup.nodes.Document;
77
import org.jsoup.nodes.Element;
@@ -25,7 +25,7 @@ public Document getHtmlFromUrl(String url) {
2525
return doc;
2626

2727
} catch (IOException e) {
28-
throw new GeneralException(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
28+
throw new NewsHandler(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
2929
}
3030
}
3131

@@ -42,7 +42,7 @@ public Element extractElementBySelector(String url, String cssSelector) {
4242
return element;
4343

4444
} catch (Exception e) {
45-
throw new GeneralException(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
45+
throw new NewsHandler(NewsErrorStatus.NEWS_ARTICLE_PARSING_FAILED);
4646
}
4747
}
4848

src/main/java/com/perfact/be/domain/news/service/NaverApiServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.perfact.be.domain.news.service;
22

3+
import com.perfact.be.domain.news.exception.NewsHandler;
34
import com.perfact.be.domain.news.exception.status.NewsErrorStatus;
4-
import com.perfact.be.global.exception.GeneralException;
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.beans.factory.annotation.Value;
77
import org.springframework.http.HttpEntity;
@@ -46,11 +46,11 @@ public String searchNaverNews(String query) {
4646
if (response.getStatusCode() == HttpStatus.OK) {
4747
return response.getBody();
4848
} else {
49-
throw new GeneralException(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
49+
throw new NewsHandler(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
5050
}
5151

5252
} catch (Exception e) {
53-
throw new GeneralException(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
53+
throw new NewsHandler(NewsErrorStatus.NEWS_NAVER_API_CALL_FAILED);
5454
}
5555
}
5656

src/main/java/com/perfact/be/domain/news/service/NewsExtractorServiceImpl.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.perfact.be.domain.news.service;
22

3+
import com.perfact.be.domain.news.config.SelectorConfig;
4+
import com.perfact.be.domain.news.exception.NewsHandler;
35
import com.perfact.be.domain.news.exception.status.NewsErrorStatus;
4-
import com.perfact.be.global.exception.GeneralException;
56
import lombok.RequiredArgsConstructor;
67
import org.jsoup.nodes.Document;
78
import org.jsoup.nodes.Element;
@@ -13,7 +14,7 @@
1314
public class NewsExtractorServiceImpl implements NewsExtractorService {
1415

1516
private final HtmlParserService htmlParserService;
16-
private final com.perfact.be.domain.news.config.SelectorConfig selectorConfig;
17+
private final SelectorConfig selectorConfig;
1718

1819
// 뉴스 기사 내용 추출
1920
@Override
@@ -35,7 +36,7 @@ public String extractNewsArticleContent(String url) {
3536
return content.toString();
3637

3738
} catch (Exception e) {
38-
throw new GeneralException(NewsErrorStatus.NEWS_CONTENT_NOT_FOUND);
39+
throw new NewsHandler(NewsErrorStatus.NEWS_CONTENT_NOT_FOUND);
3940
}
4041
}
4142

0 commit comments

Comments
 (0)