RSS 피드를 수집하여 콘텐츠를 자동 큐레이션하고, 출처 표기, 자동 요약, 스케줄 발행을 구현합니다.
RSS 피드 수집 엔진 전체 구현
데이터베이스 테이블 설계
RSS 큐레이션 시스템은 두 개의 커스텀 테이블이 필요합니다. 피드 소스 테이블은 구독 중인 RSS 피드의 URL, 이름, 카테고리, 수집 주기를 저장합니다. 아이템 테이블은 수집된 개별 기사의 제목, 링크, 설명, AI 요약, 상태(대기/승인/거절/사용됨) 등을 저장합니다.
function map_rss_create_tables() {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$sql1 = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}map_rss_feeds (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
url VARCHAR(500) NOT NULL,
category VARCHAR(100) DEFAULT '',
max_items INT DEFAULT 10,
fetch_interval VARCHAR(20) DEFAULT 'hourly',
is_active TINYINT(1) DEFAULT 1,
last_fetched DATETIME DEFAULT NULL,
last_error TEXT DEFAULT NULL,
total_items_fetched INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_active (is_active),
UNIQUE KEY uk_url (url(255))
) {$charset};";
$sql2 = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}map_rss_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
feed_id BIGINT UNSIGNED NOT NULL,
guid VARCHAR(64) NOT NULL,
title VARCHAR(500) NOT NULL,
link VARCHAR(1000) NOT NULL,
description TEXT,
ai_summary TEXT DEFAULT NULL,
ai_category VARCHAR(100) DEFAULT NULL,
ai_relevance_score DECIMAL(5,2) DEFAULT NULL,
source_name VARCHAR(200),
author VARCHAR(200) DEFAULT '',
pub_date DATETIME,
category VARCHAR(100) DEFAULT '',
status ENUM('pending','approved','rejected','used','spam') DEFAULT 'pending',
post_id BIGINT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_guid (guid),
INDEX idx_status (status),
INDEX idx_feed_id (feed_id),
INDEX idx_pub_date (pub_date)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql1);
dbDelta($sql2);
}
register_activation_hook(__FILE__, 'map_rss_create_tables');
class MAP_RSS_Curator {
private $items_table;
private $feeds_table;
private $spam_filter;
public function __construct() {
global $wpdb;
$this->items_table = $wpdb->prefix . 'map_rss_items';
$this->feeds_table = $wpdb->prefix . 'map_rss_feeds';
$this->spam_filter = new MAP_RSS_Spam_Filter();
add_action('map_fetch_rss_feeds', [$this, 'fetch_all_feeds']);
if (!wp_next_scheduled('map_fetch_rss_feeds')) {
wp_schedule_event(time(), 'hourly', 'map_fetch_rss_feeds');
}
}
public function add_feed($name, $url, $category = '', $max_items = 10) {
global $wpdb;
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return new WP_Error('invalid_url', '유효하지 않은 URL입니다.');
}
include_once ABSPATH . WPINC . '/feed.php';
$test = fetch_feed($url);
if (is_wp_error($test)) {
return new WP_Error('invalid_feed', 'RSS 피드를 파싱할 수 없습니다: ' . $test->get_error_message());
}
return $wpdb->insert($this->feeds_table, [
'name' => sanitize_text_field($name),
'url' => esc_url_raw($url),
'category' => sanitize_text_field($category),
'max_items'=> absint($max_items),
]);
}
public function fetch_all_feeds() {
global $wpdb;
$feeds = $wpdb->get_results(
"SELECT * FROM {$this->feeds_table} WHERE is_active = 1"
);
$total_new = 0;
foreach ($feeds as $feed) {
$count = $this->fetch_feed($feed);
$total_new += $count;
}
error_log(sprintf(
'[RSS Curator] %d개 피드에서 %d개 새 아이템 수집',
count($feeds), $total_new
));
return $total_new;
}
private function fetch_feed($feed) {
global $wpdb;
include_once ABSPATH . WPINC . '/feed.php';
$rss = fetch_feed($feed->url);
if (is_wp_error($rss)) {
$wpdb->update($this->feeds_table, [
'last_error' => $rss->get_error_message(),
], ['id' => $feed->id]);
error_log("[RSS] 피드 오류 [{$feed->name}]: " . $rss->get_error_message());
return 0;
}
$items = $rss->get_items(0, $feed->max_items);
$new_count = 0;
foreach ($items as $item) {
$guid = md5($item->get_permalink());
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->items_table} WHERE guid = %s",
$guid
));
if ($exists) continue;
$description = wp_trim_words(
strip_tags($item->get_description()), 100
);
$status = $this->spam_filter->check($item->get_title(), $description)
? 'spam' : 'pending';
$wpdb->insert($this->items_table, [
'feed_id' => $feed->id,
'guid' => $guid,
'title' => sanitize_text_field($item->get_title()),
'link' => esc_url_raw($item->get_permalink()),
'description' => $description,
'author' => sanitize_text_field($item->get_author() ? $item->get_author()->get_name() : ''),
'source_name' => $feed->name,
'pub_date' => $item->get_date('Y-m-d H:i:s') ?: current_time('mysql'),
'category' => $feed->category,
'status' => $status,
]);
$new_count++;
}
$wpdb->update($this->feeds_table, [
'last_fetched' => current_time('mysql'),
'last_error' => null,
'total_items_fetched' => $feed->total_items_fetched + $new_count,
], ['id' => $feed->id]);
return $new_count;
}
}