Skip to content
Open
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
105 changes: 65 additions & 40 deletions app/jobs/fetch_posts_job.rb
Original file line number Diff line number Diff line change
@@ -1,49 +1,59 @@
class FetchPostsJob < ApplicationJob
require 'faraday'
require 'nokogiri'
require "faraday"
require "nokogiri"

queue_as :default

# 사이버캠퍼스 상수
BASE_URL = 'https://cyber.wku.ac.kr/Cyber/ComBoard_V005/Content'.freeze
GID = '1115983888724'.freeze
BID = '1115985252888'.freeze
BASE_URL = "https://cyber.wku.ac.kr/Cyber/ComBoard_V005/Content".freeze
GID = "1115983888724".freeze
BID = "1115985252888".freeze

# 게시글 목록 페이지를 크롤링하고 게시글 레코드를 업서트합니다
# @param page [Integer] 크롤링할 페이지 번호, 기본값은 1
def perform(page = 1)
Rails.logger.info "페이지 #{page}에서 게시글 크롤링 시작"

# 페이지 URL 구성
url = "#{BASE_URL}/list.jsp?gid=#{GID}&bid=#{BID}"
url += "&lpage=#{page}" if page > 1

begin
# 페이지 가져오기
response = Faraday.get(url)

if response.status == 200
# HTML 파싱
doc = Nokogiri::HTML(response.body)

# 처리된 게시글 수 추적
posts_count = 0


# 현재 페이지의 공지글 CID 목록 저장
current_notice_cids = []

# 공지사항 처리
doc.css('tr.notice').each do |row|
doc.css("tr.notice").each do |row|
cid = extract_cid(row.css("td:nth-child(3) a").attr("href")&.to_s)
current_notice_cids << cid if cid.present?
process_notice_post(row)
posts_count += 1
end

# 일반 게시글 처리
doc.css('table.table tbody tr').each do |row|
doc.css("table.table tbody tr").each do |row|
# 헤더 행과 공지 게시글은 건너뜀
next if row.css('th').any? || row['class']&.include?('notice')
next if row.css("th").any? || row["class"]&.include?("notice")

process_regular_post(row)
posts_count += 1
end


# 기존 공지글 중 현재 페이지에 나타나지 않은 게시글의 공지 상태 업데이트
if page == 1
update_removed_notices(current_notice_cids)
end

Rails.logger.info "페이지 #{page}에서 #{posts_count}개의 게시글을 성공적으로 처리했습니다"
else
Rails.logger.error "페이지 가져오기 실패. 상태 코드: #{response.status}"
Expand All @@ -52,85 +62,100 @@ def perform(page = 1)
Rails.logger.error "게시글 크롤링 중 오류 발생: #{e.message}\n#{e.backtrace.join("\n")}"
end
end

private

# 공지사항 게시글을 처리하고 데이터베이스에 업서트합니다
# @param row [Nokogiri::XML::Element] 게시글의 테이블 행 요소
def process_notice_post(row)
cid = extract_cid(row.css('td:nth-child(3) a').attr('href')&.to_s)
cid = extract_cid(row.css("td:nth-child(3) a").attr("href")&.to_s)
return if cid.blank?

# 원본 URL 생성
source_url = "#{BASE_URL}/print.jsp?gid=#{GID}&bid=#{BID}&cid=#{cid}"

# 게시글 속성 구성 (view에서만 확인 가능한 정보는 제외)
post_attributes = {
title: row.css('td:nth-child(3) a').text.strip,
author_name: row.css('td:nth-child(2)').text.strip,
view_count: row.css('td:nth-child(5)').text.strip.to_i,
title: row.css("td:nth-child(3) a").text.strip,
author_name: row.css("td:nth-child(2)").text.strip,
view_count: row.css("td:nth-child(5)").text.strip.to_i,
is_notice: true,
cid: cid,
gid: GID,
bid: BID,
source_url: source_url,
scraped_at: Time.current
}

# 게시글 업서트
upsert_post(post_attributes)
end

# 일반 게시글을 처리하고 데이터베이스에 업서트합니다
# @param row [Nokogiri::XML::Element] 게시글의 테이블 행 요소
def process_regular_post(row)
cid = extract_cid(row.css('td:nth-child(3) a').attr('href')&.to_s)
cid = extract_cid(row.css("td:nth-child(3) a").attr("href")&.to_s)
return if cid.blank?

# 원본 URL 생성
source_url = "#{BASE_URL}/print.jsp?gid=#{GID}&bid=#{BID}&cid=#{cid}"

# 게시글 속성 구성 (view에서만 확인 가능한 정보는 제외)
post_attributes = {
title: row.css('td:nth-child(3) a').text.strip,
author_name: row.css('td:nth-child(2)').text.strip,
view_count: row.css('td:nth-child(5)').text.strip.to_i,
title: row.css("td:nth-child(3) a").text.strip,
author_name: row.css("td:nth-child(2)").text.strip,
view_count: row.css("td:nth-child(5)").text.strip.to_i,
is_notice: false,
cid: cid,
gid: GID,
bid: BID,
source_url: source_url,
scraped_at: Time.current
}

# 게시글 업서트
upsert_post(post_attributes)
end

# viewGo JavaScript 함수에서 게시글 CID를 추출합니다
# @param href [String] href 속성 내용
# @return [String, nil] 추출된 CID 또는 찾지 못한 경우 nil
def extract_cid(href)
href&.match(/viewGo\(\"([^\"]+)\"/)&.captures&.first
end

# 데이터베이스에 게시글 레코드를 업서트합니다
# @param attributes [Hash] 게시글 속성
def upsert_post(attributes)
# CID로 기존 게시글을 찾거나 새 게시글을 초기화
post = Post.find_or_initialize_by(cid: attributes[:cid])

# 게시글 속성 업데이트
post.assign_attributes(attributes)

# 만약 이미 존재하는 레코드인 경우 last_updated_at 설정
post.last_updated_at = Time.current unless post.new_record?

# 게시글 저장
if post.save
Rails.logger.info "게시글 업서트 완료: #{post.title} (CID: #{post.cid})"
else
Rails.logger.error "게시글 업서트 실패 (CID: #{attributes[:cid]}): #{post.errors.full_messages.join(', ')}"
end
end

# 페이지에 더 이상 공지로 표시되지 않는 게시글들의 공지 상태를 업데이트합니다
# @param current_notice_cids [Array<String>] 현재 페이지에서 공지로 표시된 게시글의 CID 목록
def update_removed_notices(current_notice_cids)
# 데이터베이스에서 현재 공지로 표시된 모든 게시글 찾기
notice_posts = Post.where(is_notice: true)

# 현재 페이지에 없는 공지글의 공지 상태 해제
notice_posts.each do |post|
unless current_notice_cids.include?(post.cid)
post.update(is_notice: false, last_updated_at: Time.current)
Rails.logger.info "공지에서 제외된 게시글 업데이트: #{post.title} (CID: #{post.cid})"
end
end
end
end
93 changes: 91 additions & 2 deletions spec/jobs/fetch_posts_job_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
require 'rails_helper'
require "rails_helper"

RSpec.describe FetchPostsJob, type: :job do
pending "add some examples to (or delete) #{__FILE__}"
describe "#perform" do
let(:html_content) do
<<~HTML
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<table class="table">
<tbody>
<tr class="notice notice-view">
<td><span class="label">공지</span></td>
<td>관리자</td>
<td><a href="javascript:viewGo(&#34;12345&#34;, 1);">공지 게시글</a></td>
<td>2025-04-08</td>
<td>10</td>
<td></td>
</tr>
<tr>
<td></td>
<td>사용자</td>
<td><a href="javascript:viewGo(&#34;67890&#34;, 1);">일반 게시글</a></td>
<td>2025-04-07</td>
<td>5</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
HTML
end

before do
# 테스트 데이터 초기화
Post.destroy_all

# Faraday 응답 모의 처리
response_double = instance_double(Faraday::Response, status: 200, body: html_content)
allow(Faraday).to receive(:get).and_return(response_double)
end

context "정상적인 응답을 받을 때" do
it "게시글을 적절히 처리한다" do
# 테스트 데이터 초기화 확인
expect(Post.count).to eq(0)

# 작업 실행
subject.perform

# 총 게시글 수 확인
expect(Post.count).to eq(2)

# 공지 게시글 확인
notice_post = Post.find_by(cid: "12345")
expect(notice_post).to be_present
expect(notice_post.is_notice).to be true
expect(notice_post.title).to eq("공지 게시글")

# 일반 게시글 확인
regular_post = Post.find_by(cid: "67890")
expect(regular_post).to be_present
expect(regular_post.is_notice).to be false
expect(regular_post.title).to eq("일반 게시글")
end
end

context "공지에서 제외된 게시글이 있을 때" do
before do
# 이전에 공지로 표시된 게시글 생성
Post.create!(
title: "예전 공지",
cid: "99999",
gid: FetchPostsJob::GID,
bid: FetchPostsJob::BID,
is_notice: true,
author_name: "관리자",
view_count: 20,
source_url: "https://example.com",
scraped_at: Time.current
)
end

it "현재 공지 목록에 없는 게시글의 공지 상태를 해제한다" do
subject.perform
old_post = Post.find_by(cid: "99999")
expect(old_post).to be_present
expect(old_post.is_notice).to be false
end
end
end
end
Loading