Skip to content

Commit 1126e9b

Browse files
committed
Refact : 전략 패턴에 맞추어 기존 코드 수정
1 parent a2fff11 commit 1126e9b

File tree

9 files changed

+96
-165
lines changed

9 files changed

+96
-165
lines changed

src/main/java/com/perfact/be/domain/alt/service/ArticleExtractionServiceImpl.java

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.perfact.be.domain.alt.service;
22

33
import com.perfact.be.domain.alt.dto.ArticleExtractionResult;
4-
54
import com.perfact.be.domain.news.dto.NewsArticleResponse;
6-
import com.perfact.be.domain.news.service.NewsService;
5+
import com.perfact.be.domain.news.extractor.factory.NewsExtractorFactory;
76
import lombok.RequiredArgsConstructor;
87
import lombok.extern.slf4j.Slf4j;
98
import org.springframework.stereotype.Service;
@@ -13,17 +12,14 @@
1312
@RequiredArgsConstructor
1413
public class ArticleExtractionServiceImpl implements ArticleExtractionService {
1514

16-
private final NewsService newsService;
15+
private final NewsExtractorFactory newsExtractorFactory;
1716

1817
@Override
1918
public String extractArticleContent(String url) {
2019
try {
21-
if (newsService.isNaverNewsDomain(url)) {
22-
NewsArticleResponse newsData = newsService.extractNaverNewsArticle(url);
23-
return newsData.getContent();
24-
} else {
25-
return newsService.extractNewsArticleContent(url);
26-
}
20+
// 모든 뉴스 사이트에 대해 동일한 방식으로 처리
21+
NewsArticleResponse newsData = newsExtractorFactory.extractNews(url);
22+
return newsData.getContent();
2723
} catch (Exception e) {
2824
log.error("기사 본문 추출 실패 - URL: {}, 에러: {}", url, e.getMessage(), e);
2925
throw new RuntimeException(e);
@@ -33,22 +29,13 @@ public String extractArticleContent(String url) {
3329
@Override
3430
public ArticleExtractionResult extractArticleWithMetadata(String url) {
3531
try {
36-
if (newsService.isNaverNewsDomain(url)) {
37-
NewsArticleResponse newsData = newsService.extractNaverNewsArticle(url);
38-
return ArticleExtractionResult.builder()
39-
.title(newsData.getTitle())
40-
.publicationDate(newsData.getDate())
41-
.content(newsData.getContent())
42-
.build();
43-
} else {
44-
String title = newsService.extractTitleFromOtherNewsSites(url);
45-
String content = newsService.extractNewsArticleContent(url);
46-
return ArticleExtractionResult.builder()
47-
.title(title)
48-
.publicationDate("날짜 정보 없음")
49-
.content(content)
50-
.build();
51-
}
32+
// 모든 뉴스 사이트에 대해 동일한 방식으로 처리
33+
NewsArticleResponse newsData = newsExtractorFactory.extractNews(url);
34+
return ArticleExtractionResult.builder()
35+
.title(newsData.getTitle())
36+
.publicationDate(newsData.getDate())
37+
.content(newsData.getContent())
38+
.build();
5239
} catch (Exception e) {
5340
log.error("기사 메타데이터 추출 실패 - URL: {}, 에러: {}", url, e.getMessage(), e);
5441
throw new RuntimeException(e);

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

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

33
import com.perfact.be.domain.news.dto.NewsArticleResponse;
4-
import com.perfact.be.domain.news.service.NewsService;
4+
import com.perfact.be.domain.news.extractor.factory.NewsExtractorFactory;
55
import com.perfact.be.global.apiPayload.ApiResponse;
66
import io.swagger.v3.oas.annotations.Operation;
77
import io.swagger.v3.oas.annotations.Parameter;
@@ -17,21 +17,20 @@
1717
@RequiredArgsConstructor
1818
public class NewsController {
1919

20-
private final NewsService newsService;
20+
private final NewsExtractorFactory newsExtractorFactory;
2121

22-
@Operation(summary = "뉴스 기사 내용 추출", description = "네이버 뉴스 URL을 입력받아 기사의 제목, 날짜, 내용을 추출합니다.")
22+
@Operation(summary = "뉴스 기사 내용 추출", description = "뉴스 URL을 입력받아 기사의 제목, 날짜, 내용을 추출합니다. 지원 사이트: 네이버뉴스, 연합뉴스, 뉴시스, 노컷뉴스")
2323
@GetMapping("/article-content")
2424
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) {
26-
NewsArticleResponse response = newsService.extractNaverNewsArticle(url);
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) {
26+
NewsArticleResponse response = newsExtractorFactory.extractNews(url);
2727
return ApiResponse.onSuccess(response);
2828
}
2929

3030
@Operation(summary = "네이버 뉴스 검색", description = "검색어를 입력받아 네이버 뉴스 검색 결과를 반환합니다.")
3131
@GetMapping("/search")
3232
public ApiResponse<String> searchNaverNews(
3333
@Parameter(description = "검색할 키워드", required = true, example = "AI 기술") @RequestParam String query) {
34-
String searchResult = newsService.searchNaverNews(query);
35-
return ApiResponse.onSuccess(searchResult);
34+
throw new UnsupportedOperationException("네이버 뉴스 검색 기능은 현재 지원되지 않습니다.");
3635
}
3736
}

src/main/java/com/perfact/be/domain/news/exception/status/NewsErrorStatus.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
public enum NewsErrorStatus implements BaseErrorCode {
1212
NOT_NAVER_NEWS(HttpStatus.BAD_REQUEST, "NEWS4001", "네이버 뉴스 도메인이 아닙니다. 네이버 뉴스를 통한 링크만 가능합니다."),
1313
UNSUPPORTED_NEWS_SITE(HttpStatus.BAD_REQUEST, "NEWS4002",
14-
"지원하지 않는 뉴스 사이트입니다. 현재 지원되는 사이트: 네이버뉴스, 연합뉴스, 뉴시스, 노컷뉴스, 오마이뉴스 (네이버 뉴스에 최적화되어 있습니다.)"),
14+
"지원하지 않는 뉴스 사이트입니다. 현재 지원되는 사이트: 네이버뉴스, 연합뉴스, 뉴시스, 노컷뉴스 (네이버 뉴스에 최적화되어 있습니다.)"),
1515
NEWS_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "NEWS4003", "뉴스 내용을 찾을 수 없습니다."),
1616
NEWS_TITLE_EXTRACTION_FAILED(HttpStatus.BAD_REQUEST, "NEWS4004", "뉴스 제목 추출에 실패했습니다."),
1717
NEWS_DATE_EXTRACTION_FAILED(HttpStatus.BAD_REQUEST, "NEWS4005", "뉴스 날짜 추출에 실패했습니다."),

src/main/java/com/perfact/be/domain/news/extractor/AbstractNewsExtractor.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected String extractTitle(Document doc, String[] titleSelectors) {
3232
}
3333
}
3434
log.warn("제목을 찾을 수 없습니다. 사용된 셀렉터: {}", String.join(", ", titleSelectors));
35-
return "제목을 찾을 수 없습니다";
35+
throw new NewsHandler(NewsErrorStatus.NEWS_TITLE_EXTRACTION_FAILED);
3636
}
3737

3838
// HTML 문서에서 내용 추출
@@ -48,7 +48,7 @@ protected String extractContent(Document doc, String[] contentSelectors) {
4848
}
4949
}
5050
log.warn("내용을 찾을 수 없습니다. 사용된 셀렉터: {}", String.join(", ", contentSelectors));
51-
return "내용을 찾을 수 없습니다";
51+
throw new NewsHandler(NewsErrorStatus.NEWS_CONTENT_NOT_FOUND);
5252
}
5353

5454
// 내용 요소 처리
@@ -98,10 +98,14 @@ protected Document getDocument(String url) {
9898
// 날짜 추출
9999
protected String extractDate(String url) {
100100
try {
101-
return dateExtractorService.extractArticleDate(url);
101+
String date = dateExtractorService.extractArticleDate(url);
102+
if (date == null || date.equals("날짜 정보 없음")) {
103+
throw new NewsHandler(NewsErrorStatus.NEWS_DATE_EXTRACTION_FAILED);
104+
}
105+
return date;
102106
} catch (Exception e) {
103107
log.warn("날짜 추출 실패: {}", url, e);
104-
return "날짜 정보 없음";
108+
throw new NewsHandler(NewsErrorStatus.NEWS_DATE_EXTRACTION_FAILED);
105109
}
106110
}
107111

src/main/java/com/perfact/be/domain/news/extractor/impl/GenericNewsExtractor.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ public GenericNewsExtractor(com.perfact.be.domain.news.service.HtmlParserService
1818

1919
@Override
2020
public boolean canExtract(String url) {
21-
// 네이버 뉴스, 연합뉴스, 뉴시스, 노컷뉴스가 아닌 모든 URL을 처리
22-
return !url.contains("news.naver.com") && !url.contains("yna.co.kr") && !url.contains("newsis.com")
23-
&& !url.contains("nocutnews.co.kr") && !url.contains("ohmynews.com");
21+
// 지원하는 뉴스 사이트들만 처리하고, 나머지는 거부
22+
return url.contains("news.naver.com") || url.contains("yna.co.kr") || url.contains("newsis.com")
23+
|| url.contains("nocutnews.co.kr"); //|| url.contains("ohmynews.com");
2424
}
2525

2626
@Override
2727
public NewsArticleResponse extract(String url) {
28-
log.info("일반 뉴스 사이트 판별: {}", url);
28+
log.info("지원하는 뉴스 사이트 처리: {}", url);
2929

3030
try {
3131
Document doc = getDocument(url);
@@ -37,7 +37,7 @@ public NewsArticleResponse extract(String url) {
3737
return new NewsArticleResponse(title, date, content);
3838

3939
} catch (Exception e) {
40-
log.error("일반 뉴스 사이트 판별 실패: {}", url, e);
40+
log.error("뉴스 사이트 처리 실패: {}", url, e);
4141
throw e;
4242
}
4343
}

src/main/java/com/perfact/be/domain/news/extractor/impl/OhMyNewsExtractor.java

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -67,33 +67,30 @@ protected String[] getContentSelectors() {
6767
// 오마이뉴스 특화 날짜 추출
6868
private String extractDate(Document doc) {
6969
try {
70-
// 오마이뉴스 날짜 선택자
71-
Elements dateElements = doc.select("div.atc-sponsor span.date");
72-
73-
if (!dateElements.isEmpty()) {
74-
// 첫 번째 date span 사용
75-
Element firstDateElement = dateElements.first();
76-
String dateText = firstDateElement.text().trim();
77-
78-
log.debug("오마이뉴스 원본 날짜 텍스트: {}", dateText);
79-
80-
// "25.08.19 15:25" 형식을 "2025-08-19 15:25" 형식으로 변환
81-
String convertedDate = convertOhMyNewsDate(dateText);
82-
83-
if (convertedDate != null) {
84-
log.info("오마이뉴스 날짜 변환 성공: {} → {}", dateText, convertedDate);
85-
return convertedDate;
86-
}
87-
}
88-
89-
// fallback: 다른 날짜 선택자들 시도
90-
Elements fallbackElements = doc.select("span.date, .date, [class*='date']");
91-
for (Element element : fallbackElements) {
92-
String text = element.text().trim();
93-
String convertedDate = convertOhMyNewsDate(text);
94-
if (convertedDate != null) {
95-
log.info("fallback으로 오마이뉴스 날짜 추출 성공: {} → {}", text, convertedDate);
96-
return convertedDate;
70+
// 오마이뉴스 날짜 선택자들 (우선순위 순)
71+
String[] dateSelectors = {
72+
"div.atc-sponsor span.date", // 기존 셀렉터
73+
"span.date", // 직접 span.date
74+
".date", // 클래스로만
75+
"[class*='date']" // 클래스에 date 포함
76+
};
77+
78+
for (String selector : dateSelectors) {
79+
Elements dateElements = doc.select(selector);
80+
81+
if (!dateElements.isEmpty()) {
82+
Element firstDateElement = dateElements.first();
83+
String dateText = firstDateElement.text().trim();
84+
85+
log.debug("오마이뉴스 원본 날짜 텍스트: {}", dateText);
86+
87+
// "25.08.19 15:25" 또는 "25.08.19 19:00" 형식을 "2025-08-19 15:25" 형식으로 변환
88+
String convertedDate = convertOhMyNewsDate(dateText);
89+
90+
if (convertedDate != null) {
91+
log.info("오마이뉴스 날짜 변환 성공: {} → {}", dateText, convertedDate);
92+
return convertedDate;
93+
}
9794
}
9895
}
9996

@@ -108,20 +105,24 @@ private String extractDate(Document doc) {
108105
// 오마이뉴스 날짜 형식 변환
109106
private String convertOhMyNewsDate(String dateText) {
110107
try {
111-
// "25.08.19 15:25" 형식 매칭
112-
Pattern pattern = Pattern.compile("(\\d{2})\\.(\\d{2})\\.(\\d{2})\\s+(\\d{2}:\\d{2})");
108+
// "25.08.19 15:25" 또는 "25.08.19 19:00" 형식 매칭 (시간이 1자리 또는 2자리)
109+
Pattern pattern = Pattern.compile("(\\d{2})\\.(\\d{2})\\.(\\d{2})\\s+(\\d{1,2}):(\\d{2})");
113110
Matcher matcher = pattern.matcher(dateText);
114111

115112
if (matcher.find()) {
116113
String year = matcher.group(1);
117114
String month = matcher.group(2);
118115
String day = matcher.group(3);
119-
String time = matcher.group(4);
116+
int hour = Integer.parseInt(matcher.group(4));
117+
String minute = matcher.group(5);
120118

121119
// 20xx년으로 변환 (25 → 2025)
122120
String fullYear = "20" + year;
123121

124-
return String.format("%s-%s-%s %s", fullYear, month, day, time);
122+
// 시간을 2자리로 포맷팅
123+
String formattedHour = String.format("%02d", hour);
124+
125+
return String.format("%s-%s-%s %s:%s", fullYear, month, day, formattedHour, minute);
125126
}
126127

127128
return null;
@@ -150,8 +151,19 @@ private void removeOhMyNewsSpecificElements(Element contentElement) {
150151
contentElement.select("button.zoom-btn, button.rhksfus").remove();
151152
contentElement.select("figure.omn-photo").remove();
152153

154+
// 이미지 관련 요소들 제거
155+
contentElement.select("figure, .pho-center, .pho-caption").remove();
156+
contentElement.select("img[src*='ohmynews.com']").remove();
157+
153158
// 기타 불필요한 요소들
154159
contentElement.select("div[id*='google'], div[id*='Google']").remove();
155160
contentElement.select("div[class*='ad'], div[class*='Ad']").remove();
161+
162+
// HTML 주석 제거
163+
contentElement.select("*").forEach(element -> {
164+
if (element.nodeName().equals("#comment")) {
165+
element.remove();
166+
}
167+
});
156168
}
157169
}

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ public interface NewsService {
77
// URL에서 HTML 가져오기
88
org.jsoup.nodes.Document getHtmlFromUrl(String url);
99

10-
// 네이버 뉴스 도메인인지 확인
11-
boolean isNaverNewsDomain(String url);
12-
13-
// 네이버 뉴스의 제목과 내용 추출
14-
NewsArticleResponse extractNaverNewsArticle(String url);
15-
1610
// 뉴스 기사 내용 추출
1711
String extractNewsArticleContent(String url);
1812

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,6 @@ public String extractNewsArticleContent(String url) {
3131
return newsExtractorService.extractNewsArticleContent(url);
3232
}
3333

34-
@Override
35-
public boolean isNaverNewsDomain(String url) {
36-
return url.contains("news.naver.com");
37-
}
38-
39-
@Override
40-
public NewsArticleResponse extractNaverNewsArticle(String url) {
41-
try {
42-
log.info("네이버 뉴스 기사 추출 시작: {}", url);
43-
44-
// 새로운 팩토리 패턴 사용
45-
NewsArticleResponse newsData = newsExtractorFactory.extractNews(url);
46-
47-
log.info("네이버 뉴스 기사 추출 완료 - 제목: {}, 날짜: {}, 내용 길이: {}",
48-
newsData.getTitle(), newsData.getDate(), newsData.getContent().length());
49-
50-
return newsData;
51-
52-
} catch (Exception e) {
53-
log.error("네이버 뉴스 기사 추출 실패: {}", url, e);
54-
return null;
55-
}
56-
}
57-
5834
@Override
5935
public String extractTitleFromOtherNewsSites(String url) {
6036
return newsExtractorService.extractTitleFromOtherNewsSites(url);

0 commit comments

Comments
 (0)