본 글은 WebCrawling- 1 에 이어서 작성하는 글입니다. 문제 풀이 과정을 기록하는 글이기에 완성된 코드가 없을 수 있고 부족한 부분이 있을 수 있다는 점을 미리 알립니다.
Spring Boot + MVC 패턴 활용하여 크롤링한 정보 화면에 띄우기
해당 글에서 작성한 내용을 바탕으로 Spring Boot와 MVC 패턴을 활용하여 프로젝트를 이어서 진행하였다.
https://minwook6457.tistory.com/22
[인실리코젠] Spring Boot + MVC 이론 정리
MVC (Model - View - Controller) 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴 모델 : 데이터와 비즈니스 로직 관리 뷰 : 레이아웃과 화면을 처리 컨트롤
minwook6457.tistory.com
기본적인 세팅에 대한 내용은 언급하지 않겠다. 세세한 내용을 적어가며 브리핑을 하기엔 많인 내용이 기입되어야 하고 기록하면 좋겠지만, 충분히 기억하고 찾아볼 수 있는 내용들이기 때문이다.
스프링 부트 어플리케이션 시작 클래스
package com.insilicogen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CrawlingApplication {
public static void main(String[] args) {
SpringApplication.run(CrawlingApplication.class, args);
}
}
- @SpringBootApplication 어노테이션
- 스프링 부트 어플리에키션을 나타내는 메타 어노테이션
- 어플리케이션을 설정하고 실행하는데 여러 가지 기능을 자동으로 활성화
- @EnableAutoConfiguration: 스프링 부트가 클래스패스 상의 설정을 기반으로 빈을 자동으로 구성
- @ComponentScan: 현재 패키지와 하위 패키지에서 @Component, @Service, @Repository 등의 어노테이션이 붙은 클래스들을 찾아 빈으로 등록
- @Configuration: 해당 클래스를 애플리케이션의 구성 클래스로 지정
- 기본적으로 8080 포트에 적용
컨트롤러
package com.insilicogen.crawl.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.insilicogen.crawl.dto.InfoDto;
import com.insilicogen.crawl.service.NewsService;
@Controller
public class NewsController {
@Autowired
private NewsService newsService;
@GetMapping("/news")
public String news() {
return "news"; // news.jsp로 포워딩
}
/*
@GetMapping("/crawling")
public String crawlingNews(Model model) {
List<InfoDto> newsList = newsService.crawlAndSaveNews();
model.addAttribute("newsList", newsList);
model.addAttribute("imagePath", NewsService.destinationFolder);
return "news";
}
*/
@GetMapping("/selectNewsList")
@ResponseBody
public Map<String, Object> selectNewsList(Model model,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize) {
List<InfoDto> newsList = newsService.getPage(page, pageSize);
int totalItems = newsService.getTotalNewsCount();
Map<String, Object> response = new HashMap<>();
response.put("newsList", newsList);
response.put("totalItems", totalItems);
return response;
}
@GetMapping("/initCrawling")
@ResponseBody
public ModelAndView initCrawling(Model model) {
try {
List<InfoDto> newsList = newsService.crawlAndSaveNews();
model.addAttribute("newsList", newsList);
model.addAttribute("imagePath", NewsService.destinationFolder);
return new ModelAndView("news", model.asMap());
} catch (Exception e) {
e.printStackTrace();
return new ModelAndView("errorPage", "error", "Error initiating crawling.");
}
}
}
- @Controller : 컨트롤러를 나타내는 어노테이션
- @Autowired : 필드, 생성자, Setter 메서드 등에 사용되어 해당 타입의 빈을 자동으로 주입
- NewsService 타입의 빈 주입
- @GetMapping("/news") : /news url로 get 요청 처리
- news
- news.jsp로 포워딩
- selectNewsList
- /selectNewsList 엔드포인트에 대한 get 요청 처리
- page 와 pageSize 를 parameter로 받으며 특정 범위의 뉴스 목록을 가져와 응답
- Map<String,Object> json 응답
- 파라미터 기본값으로 page : 1, pageSize 20을 요청
- initCrawling
- /initCrawling 엔드포인트에 대한 get 요청 처리
- 뉴스를 크롤링하여 데이터를 저장한 후 news.jsp로 포워딩하여 뉴스 목록을 화면에 표시
- ModelAndView (뷰 이름과 모델 데이터) 응답
ModelAndView 메소드
import org.springframework.web.servlet.ModelAndView;
public ModelAndView(String viewName, @Nullable Map<String, ?> model) {
this.view = viewName;
if (model != null) {
getModelMap().addAllAttributes(model);
}
}
DTO
크롤링한 데이터를 담을 DTO (수정)
package com.insilicogen.crawl.dto;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter // Getter 메서드 자동 주입
@Setter
@Table(name="info_dto")
@AllArgsConstructor
@Entity // 디비와 연결해주겠다. 라는 의미
// 외부 시스템과 데이터 통신을 할 경우 DTO로 정의
// DB에서 가져오는 Data 는 VO로 정의 후 사용
public class InfoDto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private String publisher;
private String upload;
// 1:1 관계 설정
@JsonIgnore
@OneToOne(mappedBy = "infoDto", cascade = CascadeType.ALL, orphanRemoval = true)
private ImageDto imageDto;
public InfoDto() {
// Default Constructor
}
public InfoDto(String title, String content, String publisher, String upload) {
this.title = title;
this.content = content;
this.publisher = publisher;
this.upload = upload;
}
}
이미지 정보를 담을 DTO
package com.insilicogen.crawl.dto;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class ImageDto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String imageUrl;
@JsonIgnore
@OneToOne
@JoinColumn(name = "info_id", unique = true) // info_id를 외래키로 가짐
private InfoDto infoDto;
public ImageDto() {
// Default Constructor
}
public ImageDto(String imageUrl, InfoDto infoDto) {
this.imageUrl = imageUrl;
this.infoDto = infoDto;
}
}
각 DTO를 살펴보면 @JsonIgnore 라는 어노테이션을 추가하였다. 해당 어노테이션은 Jackson 직렬화를 무시하겠다라는 의미로 해석할 수 있다. 해당 어노테이션을 추가한 이유는 Front 단에서 ajax 통신을 이용하여 데이터를 불러올 때 InfoDto 와 ImageDto가 1:1 관계로 매핑 되어 있어 무한 루프에 빠지게되어 오류를 발생하는 문제가 발생하여 이를 해결하기 위해 추가하였다.
Ajax 통신이란?
자바스크립트의 라이브러리 중 하나로 비동기적으로 서버와 웹 브라우저 간에 데이터를 교환하는 기술이고 페이지 전체를 리로드하지 않고도 웹 페이지의 일부를 업데이트한다.
Ajax 장점
- 페이지의 갱신없이 서버와 비동기 통신을 가능하게 해줌
- 화면이 새로 로딩되는 것이 아니기에 속도면에서 성능 향상
- 별도의 플러그인이 필요하지 않음
- HTTP 전송 중에도 클라이언트가 웹 어플리케이션과 상호작용
동기 vs 비동기
Ajax를 이해하려면 동기와 비동기 방식의 차이를 알아야 한다.
![]() |
![]() |
<동기식> <비동기식>
동기식은 요청 후 응답을 받아야 다음 동작이 이루어지고 비동기식에서는 요청을 보낸 후 응답과는 상관없이 동작한다.
이러한 비동기식을 사용하기 때문에 HTTP 전송 중에도 클라이언트가 웹 어플리케이션과 상호작용을 할 수 있는 것이다.
Ajax 기본 형태
$.ajax({
type : "get",
url : "/testUrl",
data : "",
async : true,
dataType : "json",
timeout : 10000,
success : function(res) {
console.log("success : " + res )
}
})
- type : HTTP 타입 (GET,POST,PUT,DELETE)
- url : 호출 url
- async : 디폴드 => true (false시에 동기 통신으로 변경되며 서버에 응답이 올 때 까지 기다림)
- data : url 호출 시 보낼 파라미터 데이터
- dataType : 서버에 반환되는 데이터 형식
- timeout : 제한시간 설정
Ajax 통신을 위해 news.jsp 코드를 아래와 같이 작성하였다.
function loadNews(page, pageSize) {
$.ajax({
type: "GET",
url: "/selectNewsList",
data: {
page: page,
pageSize: pageSize
},
dataType: "json",
success: function (response) {
newsList = response.newsList;
displayNews();
console.log(newsList);
},
error: function () {
alert("Error loading News");
}
})
}
page 와 pageSize를 /selectNewsList 파라미터로 받아와 json 형태로 응답한다. 응답된 결과는 아래와 같다.
데이터베이스에서 데이터를 가져오는 Repository 코드는 아래와 같다.
package com.insilicogen.crawl.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.insilicogen.crawl.dto.InfoDto;
@Repository
public interface NewsRepository extends JpaRepository<InfoDto, Long> {
@Query(value = "SELECT * FROM INFO_DTO info_dto LIMIT ?1, ?2", nativeQuery = true)
List<InfoDto> findNewsList(int offset , int pageSize);
}
필자는 JPA를 사용하였다. InfoDto를 엔터티로 설정하고 JpaRepository<InfoDto, Long>을 통해 CRUD 작업이 가능하다. 하지만 직접적으로 offset 과 limit의 설정이 필요하기 때문에 @Query 어노테이션을 사용하여 sql 쿼리를 작성하였다.
SELECT * FROM INFO_DTO info_dto LIMIT ?1, ?2
offset 부터 pageSize 까지의 범위의 데이터를 가져온다. 중요하게 볼 부분은 LIMIT ?1, ?2 부분이다.
?1 은 시작 인덱스이고 ?2는 가져올 행의 수 이다.
- ?1 : 시작인덱스로 몇 번째 행부터 결과를 가져올 지 지정한다.
- ?2 : 가져올 행의 수로 시작 인덱스부터 몇 개의 행을 가져올 지 지정한다.
만약 LIMIT 0,10 이라면 첫 번째 행부터 시작하여 10개의 행을 가져온다. 이 쿼리를 통해 앞단에서의 페이지네이션 알고리즘을 작성하지 않고도 INFO_DTO 테이블에서 특정 범위의 행을 선택하여 데이터를 가져올 수 있다.
{
"newsList": [
{
"id": 1,
"title": "SK하이닉스, 작년 4분기 흑자전환 성공…영업익 3460억원",
"content": "SK하이닉스가 지난해 4분기 3천460억 원의 영업이익을 기록하며 흑자 전환에 성공했다. 이로써 회사는 2022년 4분기부터 이 …",
"publisher": "지디넷코리아",
"upload": "2분전"
},
{
"id": 2,
"title": "'운명의 날' 제4통신사 선정 위한 주파수 경매 시작",
"content": "과학기술정보통신부는 25일 서울 송파구 아이티벤처타워에서 5G 28㎓ 대역 주파수 경매를 시작한다. 28㎓ 대역 주파수 할당을 …",
"publisher": "노컷뉴스",
"upload": "3분전"
},
{
"id": 3,
"title": "SK C&C, 하이브리드 클라우드 현대홈쇼핑 차세대 시스템 구축",
"content": "SK C&C는 2022년 5월 착수한 현대홈쇼핑의 차세대 시스템 구축을 완료했다고 25일 밝혔다. SK C&C는 20개월에 걸쳐 …",
"publisher": "연합뉴스",
"upload": "3분전"
},
{
"id": 4,
"title": "“뼈에 금갔다면 기브스 말고 이것 붙이세요” KAIST, ‘뼈 반창고’ 개발",
"content": "국내 연구진이 금이 간 뼈의 재생을 돕는 신소재 개발에 성공했다. 카이스트(KAIST)는 신소재공학과 홍승범 교수 연구팀이 전남 …",
"publisher": "헤럴드경제",
"upload": "6분전"
},
{
"id": 5,
"title": "'사랑의 호르몬' 옥시토신 역할…패배 후 자기방어도 관여",
"content": "'사랑의 호르몬'으로 불리는 옥시토신(Oxytocin)이 패배에 관한 기억에도 관여해 공격자를 피하도록 한다는 연구 결과가 나왔 …",
"publisher": "머니투데이",
"upload": "6분전"
},
{
"id": 6,
"title": "SK하이닉스 전년 4분기 흑자전환…영업이익 3460억원",
"content": "반도체 회복세에 SK하이닉스가 흑자전환에 성공했다. SK하이닉스는 지난해 4분기 매출 32조 7657억원, 영업이익 3460억원 …",
"publisher": "디지털데일리",
"upload": "9분전"
},
{
"id": 7,
"title": "DGIST 연구팀, 극한의 환경에도 작동하는 차세대 반도체 메모리 개발",
"content": "DGIST(총장 이건우) 는 전기전자컴퓨터공학과 권혁준 교수팀(제1저자 장봉호 박사과정생)이 저온에서도 고품질의 산화막 제작과 …",
"publisher": "한국경제",
"upload": "10분전"
},
{
"id": 8,
"title": "SK하이닉스 작년 4분기 영업이익 흑자전환...1년만에 적자 탈출",
"content": "메모리 반도체 업황 반등이 본격화된 가운데 SK하이닉스가 지난해 4분기 3460억 원의 영업이익을 기록하며 흑자 전환했다. 20 …",
"publisher": "조선일보",
"upload": "11분전"
},
{
"id": 9,
"title": "[미장브리핑] S&P500·나스닥 5거래일 연속 상승 마감",
"content": "◇ 24일 (현지시간) 미국 증시 ▲다우존스산업평균(다우)지수 전 거래일 대비 0.26% 하락한 37806.39. ▲스탠다드앤푸 …",
"publisher": "지디넷코리아",
"upload": "12분전"
},
{
"id": 10,
"title": "[속보] 작년 4분기 0.6% 성장…반도체 수출 회복 등 영향",
"content": "한국은행은 지난해 4분기 실질 국내총생산, GDP 성장률이 0.6%라고 밝혔습니다. 재작년 4분기 -0.3%였던 실질 GDP는 …",
"publisher": "연합뉴스TV",
"upload": "15분전"
},
{
"id": 11,
"title": "[1보] SK하이닉스, 지난해 영업손실 7조7303억...매출 26% 감소",
"content": "SK하이닉스는 지난해 연결기준 영업손실 7조7천303억 원으로 전년(68조941억 원) 대비 적자전환했다고 25일 공시했다. 매 …",
"publisher": "지디넷코리아",
"upload": "27분전"
},
{
"id": 12,
"title": "페북 모회사 메타 1.43% 상승, 시총 1조 달러 돌파",
"content": "페북의 모회사 메타가 시총 1조 달러를 돌파했다. 24일(현지시간) 뉴욕증시에서 메타는 전거래일보다 1.43% 상승한 390.7 …",
"publisher": "뉴스1",
"upload": "31분전"
},
{
"id": 13,
"title": "[단독]삼성전자 '2024년 하반기 언팩' 올림픽 열리는 파리에서 개최",
"content": "파리올림픽 전 개최 위해 전년보다 2주 앞당겨… AI 장착 갤Z플립6·폴드6 공개 예정 최근 미국 새너제이에서 세계 최초 인공지 …",
"publisher": "머니S",
"upload": "31분전"
},
{
"id": 14,
"title": "국정원 보안 실태 평가서 원안위·방통위·서울시 등 7곳 '미흡'",
"content": "지난해 하반기 국가정보원 정보보안 관리 실태 평가에서 '우수' 판정을 받은 중앙행정기관·광역지방자치단체는 기획재정부와 해양수산부 …",
"publisher": "서울경제",
"upload": "32분전"
},
{
"id": 15,
"title": "AI로 중심 이동한 메타, 2021년 이후 처음으로 시총 '1조 클럽' 재입성",
"content": "사업 중심을 메타버스로 급선회했다가 뚜렷한 성과를 내지 못하고 휘청거렸던 메타가 시가총액 1조 클럽에 재입성했다. 24일(현지 …",
"publisher": "조선일보",
"upload": "35분전"
},
{
"id": 16,
"title": "엔비디아 2.5%-AMD 6% 급등, 필라델피아반도체지수 1.54%↑",
"content": "생성형 인공지능(AI) 최대 수혜주 엔비디아가 2.5%, 제2의 엔비디아로 불리는 AMD가 6% 정도 급등함에 따라 반도체 모임 …",
"publisher": "뉴스1",
"upload": "41분전"
},
{
"id": 17,
"title": "5G 28㎓ 경매 시작… 최종 낙찰자는 누구",
"content": "5세대 이동통신(5G) 28기가헤르츠(㎓) 주파수 할당 신규 사업자를 선정하기 위한 경매가 25일 진행되는 가운데 최종 낙찰자는 …",
"publisher": "머니S",
"upload": "43분전"
},
{
"id": 18,
"title": "과기정통부, AI 산업 지원책 올해 1분기 안에 마련키로",
"content": "과학기술정보통신부가 올해 1분기 내에 온디바이스AI를 포함한 AI 종합 지원 대책을 마련한다. 24일 박윤규 과기정통부 2차관은 …",
"publisher": "아시아경제",
"upload": "44분전"
},
{
"id": 19,
"title": "MS 장중 시총 '3조 달러' 클럽 진입… 목표주가 줄상향",
"content": "마이크로소프트(MS) 시가총액이 장중 3조 달러(약 4000조 원)을 돌파했다. 잠시 동안이지만 애플에 이어 세계에서 두번째로 …",
"publisher": "서울경제",
"upload": "50분전"
},
{
"id": 20,
"title": "이상일 경상의대 교수, 42대 대한면역학회장 취임",
"content": "대한면역학회는 이상일 경상의대 류마티스내과 교수가 제42대 대한면역학회 회장으로 취임했다고 25일 밝혔다. 이 교수는 “대한면역 …",
"publisher": "아시아경제",
"upload": "52분전"
}
],
"totalItems": 20000
}
서비스
package com.insilicogen.crawl.service;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.insilicogen.crawl.dto.ImageDto;
import com.insilicogen.crawl.dto.InfoDto;
import com.insilicogen.crawl.model.Info;
import com.insilicogen.crawl.repository.NewsRepository;
@Service
public class NewsService {
public static String destinationFolder = "C:\\Users\\kih25\\OneDrive\\바탕 화면\\Test\\crawling\\image";
private final String newsUrl = "https://news.naver.com/main/list.naver?mode=LS2D&sid2=230&sid1=105&mid=shm&";
private final String newsTag = "#main_content > div.list_body.newsflash_body";
@Autowired
private NewsRepository newsRepository;
@Autowired
public NewsService(NewsRepository newsRepository) {
this.newsRepository = newsRepository;
}
// 이미지 다운로드 함수
public void downloadImage(String imageUrl, String destinationFolder, InfoDto i) {
try {
if (!imageUrl.equals("")) { // 수정된 부분
URL url = new URL(imageUrl);
try (InputStream in = url.openStream()) {
String fileName = i.getImageDto().getId() + ".jpg";
Path destinationPath = Paths.get(destinationFolder, fileName);
Files.copy(in, destinationPath, StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 크롤링 함수 => 우선 데이터 정보를 객체에 담아두고 DTO에 저장
private List<InfoDto> crawlingNewsInInfo(Elements elements) {
List<InfoDto> newsList = new ArrayList<>();
for (Element element : elements) {
String title = element.select("dt:not(.photo) > a").text().trim();
String content = element.select("dd > span.lede").text().trim();
String publisher = element.select("dd > span.writing").text().trim();
String upload1 = element.select("dd > span.date.is_new").text().trim(); // ~분전
String upload2 = element.select("dd > span.date.is_outdated").text().trim(); // ~시간전
String upload3 = element.select("dd > span.date.is_outdated").text().trim(); // ~6일전 까지는 is_outdated
// 그 이후로는 date 값이 온다.
String upload4 = element.select("dd > span.date").text().trim(); //
Element imageElement = element.selectFirst("dl > dt.photo img");
if (upload1.contains("분")) {
String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";
Info info = new Info(title, content, publisher, upload1, imageUrl);
newsList.add(saveData(info));
}
else if (upload2.contains("시")) {
String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";
Info info = new Info(title, content, publisher, upload2, imageUrl);
newsList.add(saveData(info));
}
else if (upload3.contains("일")) {
String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";
Info info = new Info(title, content, publisher, upload3, imageUrl);
newsList.add(saveData(info));
}else {
String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";
Info info = new Info(title, content, publisher, upload4, imageUrl);
newsList.add(saveData(info));
}
}
return newsList;
}
private InfoDto saveData(Info info) {
InfoDto infoDto = new InfoDto(info.getTitle(), info.getContent(), info.getPublisher(), info.getUpload());
ImageDto imageDto = new ImageDto(info.getUrl(), infoDto);
infoDto.setImageDto(imageDto);
return infoDto;
}
// 초기 크롤링 함수
public List<InfoDto> crawlAndSaveNews() {
System.out.println("크롤링 시작!!");
List<InfoDto> newsList = new ArrayList<>();
String page = "&page=1";
try {
for (int date = 0; date < 30; date++) {
Date currentDate = new Date();
currentDate = decrementDate(currentDate, date);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
String formattedDate = simpleDateFormat.format(currentDate);
String newUrl = newsUrl.concat("date=").concat(formattedDate).concat(page);
System.out.println(newUrl);
Document doc = Jsoup.connect(newUrl).get();
Element body = doc.selectFirst(newsTag);
// 헤드라인 기사와 일반기사 구문 하지 않음
Elements headlineElements = body.select("ul.type06_headline > li");
newsList.addAll(crawlingNewsInInfo(headlineElements));
Elements normalElements = body.select("ul.type06 > li");
newsList.addAll(crawlingNewsInInfo(normalElements));
// 이미지 다운로드 및 뉴스 저장
for (InfoDto info : newsList) {
if (StringUtil.isBlank(info.getImageDto().getImageUrl())) {
downloadImage(info.getImageDto().getImageUrl(), destinationFolder, info);
}
}
System.out.println("크롤링 완료");
}
} catch (IOException e) {
e.printStackTrace();
}
newsRepository.saveAll(newsList);
return newsList;
}
public List<InfoDto> getPage(int page, int pageSize) {
int offset = (page - 1) * pageSize;
return newsRepository.findNewsList(offset, pageSize);
}
public int getTotalNewsCount() {
return (int) newsRepository.count();
}
/* 날짜가 주어지면 하루 씩 줄어드는 메소드 작성 */
private static Date decrementDate(Date date, int minusDate) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
// 현재 일자에서 1일을 뺍니다.
calendar.add(Calendar.DATE, -minusDate);
return calendar.getTime();
}
}
서비스 부분은 크게 달라진 점은 없다.
새로 추가된 메소드와 수정된 메소드에 대해서 설명을 덧붙이도록 하겠다.
- crawlingNewsInInfo()
- 현재 날짜를 기준으로 하루 씩 감소하며 타겟 url에 해당되는 1 페이지의 기사를 크롤링한다.
- 우선 Info 객체에 데이터를 저장하고 이를 InfoDto로 변환하여 newsList에 추가한다.
- 코드를 수정하기 전 데이터가 하루 전 까지의 데이터 즉, 이틀 치만 데이터베이스에 저장이 되는 이슈가 있었다.
- 이유를 파악해보니 태그 url의 upload를 나타내는 selector 부분이 4가지로 나뉘었다.
- ~분전
- ~시간전
- ~일전
- date
- 이를 파악하기 전에는 upload1, upload2 로만 판단하여 데이터를 넣어 이슈가 발생하였고, 이를 해결하기 위해 위 코드 처럼 각 데이터에 맞는 태그를 맞춰주었다.
- 이유를 파악해보니 태그 url의 upload를 나타내는 selector 부분이 4가지로 나뉘었다.
- decrementDate()
- 주어진 날짜를 기준으로 하루를 뺀 새로운 날짜를 반환한다.
- crawlingNewsInInfo()에 호출되며 30번 반복 (한달)
첫 크롤링 후 데이터는 600개 (20개 * 30일)가 저장된다. postman을 통해 데이터가 잘 삽입되고 페이지 네이션이 이루어 지는지 확인해보자.
이제 Front 단을 구축해보자.
추가해야할 것은 테이블과 버튼 그리고 페이지네이션이다.
아래는 news.jsp 코드이다. 아직 미완성으로 ajax 통신이 이루어지는지 만 파악이 가능한 코드이다.
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<script src="resources/js/jquery/jquery-3.7.1.js"></script>
<script src="resources/js/bootstrap/bootstrap.bundle.js"></script>
<link href="resources/css/bootstrap/bootstrap.css" rel="stylesheet" />
<title>News Page</title>
<script>
var newsList = [];
var currentPage = 1;
function initCrawling() {
$.ajax({
type: "GET",
url: "/initCrawling",
data: {},
success: function (response) {
console.log(response)
},
error: function () {
alert("Error crawling.");
}
});
}
function loadNews(page, pageSize) {
$.ajax({
type: "GET",
url: "/selectNewsList",
data: {
page: page,
pageSize: pageSize
},
success: function (response) {
console.log(response);
newsList = response.newsList;
displayNews();
console.log(newsList);
},
error: function () {
alert("load News Error");
}
})
}
// 테이블 갱신 함수
function displayNews() {
// 기존에 표시된 내용 초기화
$("#newsTableBody").empty();
// newsList에 있는 데이터를 테이블에 추가
for (var i = 0; i < newsList.length; i++) {
var news = newsList[i];
var imageUrl = news.imageDto ? news.imageDto.imageUrl : ""; // 이미지가 있는 경우만 URL 가져오기
// 각 데이터를 테이블에 추가하는 로직
var row = "<tr>" +
"<td>" + news.title + "</td>" +
"<td>" + news.content + "</td>" +
"<td>" + news.publisher + "</td>" +
"<td>" + news.upload + "</td>" +
"<td><img src='" + imageUrl + "' alt='Image'></td>" +
"</tr>";
// 생성한 행을 테이블에 추가
$("#newsTableBody").append(row);
}
}
// 버튼 클릭 시 다음 페이지 호출
$("button").on("click", function () {
loadNews(currentPage++, 20);
});
// 초기 페이지 로드
$(document).ready(function () {
// 초기 페이지 로딩 시에도 displayNews 함수 호출
loadNews(currentPage, 20);
});
//버튼 클릭 시 params를 ajax 통신으로 보내도록 수정
$("button").on("click", function () {
var url = "/initCrawling/button";
var params = {
page: currentPage,
pageSize: 20
};
$.ajax({
type: "GET",
url: url,
data: params,
dataType: "json",
success: function (response) {
newsList = response.newsList;
displayNews();
console.log(newsList);
},
error: function () {
alert("btn error");
}
});
});
</script>
<div class="container pt-5">
<button class="btn btn-primary" type="submit" style="float: right; position: relative;">Button</button>
<h2 style="text-align: center;">News List</h2>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>Publisher</th>
<th>Upload Date</th>
<th>Image</th>
</tr>
</thead>
<tbody id="newsTableBody"></tbody>
</table>
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<li class="page-item"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</div>
이것 저것 시도하다보니 굉장히 난잡하다.
현재 상황은 /initCrawling 으로 크롤링이 완료되면 200 응답을 보내고 loadNews() 로 넘어간다. 하지만, 에러가 발생하여 alert로 지정해둔 localhost:8080 load News Error 문구가 뜬다. 다음 포스팅 전까지 이를 해결하여 테이블과 페이지 네이션을 완료하겠다.
현재 화면은 아래와 같다.
추가적인 코드는 아래 깃 허브에서 확인할 수 있다.
https://github.com/MinWook6457/WebCrolling
GitHub - MinWook6457/WebCrolling: Insilicogen : WebCrawling using Jsoup with Spring Boot
Insilicogen : WebCrawling using Jsoup with Spring Boot - GitHub - MinWook6457/WebCrolling: Insilicogen : WebCrawling using Jsoup with Spring Boot
github.com
'인실리코젠' 카테고리의 다른 글
[인실리코젠] Spring CRUD 프로젝트 요구 사항 분석 및 데이터베이스 설계 (1) | 2024.02.16 |
---|---|
[인실리코젠] Web Crawling - Finish (1) | 2024.02.02 |
[인실리코젠] Spring : 용어 정리(기본) (0) | 2024.01.19 |
[인실리코젠] Spring Boot + MVC 이론 정리 (0) | 2024.01.17 |
[인실리코젠] WebCrawling - 1 (0) | 2024.01.16 |