blog-garden-widget
Version:
GitHub style blog activity visualization widget with RSS feed support, deployed proxy server integration, and enhanced customization options for multiple users
152 lines (120 loc) • 4.94 kB
JavaScript
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');
const xml2js = require('xml2js');
const rateLimit = require('express-rate-limit');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3001;
// CORS 설정 - 모든 도메인 허용
app.use(cors({
origin: '*', // 프로덕션에서는 특정 도메인만 허용하는 것이 좋습니다
credentials: true
}));
// 정적 파일 서빙 (HTML, JS 파일들)
app.use(express.static('.'));
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 100개 요청
message: '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'
});
app.use('/analyze/rss', limiter);
app.use('/proxy/rss', limiter);
// RSS 프록시 엔드포인트
app.get('/proxy/rss', async (req, res) => {
try {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL parameter is required' });
}
console.log(`프록시 요청: ${url}`);
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const content = await response.text();
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(content);
console.log(`프록시 성공: ${url}`);
} catch (error) {
console.error('프록시 에러:', error.message);
res.status(500).json({
error: '프록시 요청 실패',
details: error.message
});
}
});
// RSS 분석 엔드포인트 (날짜별 게시물 수)
app.get('/analyze/rss', async (req, res) => {
try {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL parameter is required' });
}
console.log(`RSS 분석 요청: ${url}`);
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const xmlContent = await response.text();
// XML을 JSON으로 파싱
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(xmlContent);
// RSS 피드에서 아이템 추출
const items = result.rss?.channel?.[0]?.item || result.feed?.entry || [];
if (!items || items.length === 0) {
return res.json({});
}
// 날짜별 게시물 수 계산
const dateCounts = {};
items.forEach(item => {
let date;
// RSS 형식에 따라 날짜 필드 확인
if (item.pubDate && item.pubDate[0]) {
date = new Date(item.pubDate[0]);
} else if (item.published && item.published[0]) {
date = new Date(item.published[0]);
} else if (item.updated && item.updated[0]) {
date = new Date(item.updated[0]);
} else {
// 날짜가 없으면 현재 날짜 사용
date = new Date();
}
if (date && !isNaN(date.getTime())) {
const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD 형식
dateCounts[dateString] = (dateCounts[dateString] || 0) + 1;
}
});
console.log(`RSS 분석 완료: ${url} - ${Object.keys(dateCounts).length}개 날짜`);
res.json(dateCounts);
} catch (error) {
console.error('RSS 분석 에러:', error.message);
res.status(500).json({
error: 'RSS 분석 실패',
details: error.message
});
}
});
// 헬스체크
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 홈페이지 라우트 - cdn-test.html을 기본 페이지로 설정
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'cdn-test.html'));
});
// 환경 변수로 포트 설정
app.listen(PORT, () => {
console.log(`🚀 CORS 프록시 서버가 포트 ${PORT}에서 실행 중입니다`);
console.log(`📡 RSS 프록시: /proxy/rss?url=YOUR_RSS_URL`);
console.log(`📊 RSS 분석: /analyze/rss?url=YOUR_RSS_URL`);
console.log(`💚 헬스체크: /health`);
});