From 9f4db731d5da9280e56a6e8e41724f256dbd980f Mon Sep 17 00:00:00 2001 From: SeongHoon Ryu <4997174+ryush00@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:43:39 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=EA=B8=80=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공지에서 제외된 게시글의 is_notice 상태 값을 자동으로 갱신하는 기능 추가. 게시판 첫 페이지 크롤링 시 현재 공지로 표시되지 않은 게시글의 공지 상태를 해제하도록 구현. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/jobs/fetch_posts_job.rb | 105 ++++++++++++++++++------------ spec/jobs/fetch_posts_job_spec.rb | 93 +++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 42 deletions(-) diff --git a/app/jobs/fetch_posts_job.rb b/app/jobs/fetch_posts_job.rb index 1b128f4..da7e522 100644 --- a/app/jobs/fetch_posts_job.rb +++ b/app/jobs/fetch_posts_job.rb @@ -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}" @@ -52,23 +62,23 @@ 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, @@ -76,25 +86,25 @@ def process_notice_post(row) 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, @@ -102,30 +112,30 @@ def process_regular_post(row) 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})" @@ -133,4 +143,19 @@ def upsert_post(attributes) Rails.logger.error "게시글 업서트 실패 (CID: #{attributes[:cid]}): #{post.errors.full_messages.join(', ')}" end end + + # 페이지에 더 이상 공지로 표시되지 않는 게시글들의 공지 상태를 업데이트합니다 + # @param current_notice_cids [Array] 현재 페이지에서 공지로 표시된 게시글의 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 diff --git a/spec/jobs/fetch_posts_job_spec.rb b/spec/jobs/fetch_posts_job_spec.rb index e889ab6..2ec652f 100644 --- a/spec/jobs/fetch_posts_job_spec.rb +++ b/spec/jobs/fetch_posts_job_spec.rb @@ -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 + + + Test + + + + + + + + + + + + + + + + + + + + +
공지관리자공지 게시글2025-04-0810
사용자일반 게시글2025-04-075
+ + + 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