81f52afd57
- 도메인 매핑 저장 시 소문자 정규화 - 도메인 조회 시 case-insensitive 비교 (LOWER) - robots.txt, sitemap.xml 자동 생성 - SEO 메타 태그 (OG, Twitter Card, JSON-LD 구조화 데이터)
242 lines
7.1 KiB
JavaScript
242 lines
7.1 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../db');
|
|
const { crawlSite } = require('../services/crawler');
|
|
const { updateSchedule, getActiveJobs } = require('../services/scheduler');
|
|
|
|
// ===================== 사이트 CRUD =====================
|
|
|
|
// 목록
|
|
router.get('/sites', async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query(`
|
|
SELECT s.*, ac.name AS adsense_name,
|
|
(SELECT COUNT(*) FROM crawl_results WHERE site_id = s.id) AS crawl_count
|
|
FROM sites s
|
|
LEFT JOIN adsense_configs ac ON ac.id = s.adsense_config_id
|
|
ORDER BY s.id
|
|
`);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 단건 조회
|
|
router.get('/sites/:id', async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query('SELECT * FROM sites WHERE id = $1', [req.params.id]);
|
|
if (rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 생성
|
|
router.post('/sites', async (req, res) => {
|
|
try {
|
|
const { name, url, description, parse_rules, slug, template, adsense_config_id } = req.body;
|
|
const { rows } = await db.query(
|
|
`INSERT INTO sites (name, url, description, parse_rules, slug, template, adsense_config_id)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
[name, url, description || '', parse_rules || {}, slug || null, template || 'default', adsense_config_id || null]
|
|
);
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 수정
|
|
router.put('/sites/:id', async (req, res) => {
|
|
try {
|
|
const { name, url, description, parse_rules, slug, template, adsense_config_id } = req.body;
|
|
const { rows } = await db.query(
|
|
`UPDATE sites SET name=$1, url=$2, description=$3, parse_rules=$4,
|
|
slug=$5, template=$6, adsense_config_id=$7, updated_at=NOW()
|
|
WHERE id=$8 RETURNING *`,
|
|
[name, url, description, parse_rules, slug || null, template, adsense_config_id || null, req.params.id]
|
|
);
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 삭제
|
|
router.delete('/sites/:id', async (req, res) => {
|
|
try {
|
|
await db.query('DELETE FROM sites WHERE id = $1', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ===================== 크롤링 =====================
|
|
|
|
// 즉시 크롤링 실행
|
|
router.post('/sites/:id/crawl', async (req, res) => {
|
|
try {
|
|
const result = await crawlSite(parseInt(req.params.id));
|
|
res.json(result);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 크롤링 결과 목록
|
|
router.get('/sites/:id/results', async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 20;
|
|
const { rows } = await db.query(
|
|
`SELECT id, site_id, status, error_message, crawled_at,
|
|
jsonb_array_length(COALESCE(parsed_data->'items', '[]'::jsonb)) AS item_count
|
|
FROM crawl_results WHERE site_id = $1 ORDER BY crawled_at DESC LIMIT $2`,
|
|
[req.params.id, limit]
|
|
);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 특정 크롤링 결과 상세 (파싱 데이터)
|
|
router.get('/results/:id', async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query(
|
|
'SELECT * FROM crawl_results WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
if (rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ===================== 스케줄 =====================
|
|
|
|
// 스케줄 업데이트
|
|
router.put('/sites/:id/schedule', async (req, res) => {
|
|
try {
|
|
const { cron_schedule, schedule_active } = req.body;
|
|
await updateSchedule(parseInt(req.params.id), cron_schedule, schedule_active);
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 활성 스케줄 목록
|
|
router.get('/schedules/active', async (req, res) => {
|
|
res.json({ active_site_ids: getActiveJobs() });
|
|
});
|
|
|
|
// ===================== AdSense =====================
|
|
|
|
// 목록
|
|
router.get('/adsense', async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query('SELECT * FROM adsense_configs ORDER BY id');
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 생성
|
|
router.post('/adsense', async (req, res) => {
|
|
try {
|
|
const { name, client_id, slots } = req.body;
|
|
const { rows } = await db.query(
|
|
`INSERT INTO adsense_configs (name, client_id, slots) VALUES ($1, $2, $3) RETURNING *`,
|
|
[name, client_id, slots || {}]
|
|
);
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 수정
|
|
router.put('/adsense/:id', async (req, res) => {
|
|
try {
|
|
const { name, client_id, slots, is_active } = req.body;
|
|
const { rows } = await db.query(
|
|
`UPDATE adsense_configs SET name=$1, client_id=$2, slots=$3, is_active=$4, updated_at=NOW()
|
|
WHERE id=$5 RETURNING *`,
|
|
[name, client_id, slots, is_active, req.params.id]
|
|
);
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 삭제
|
|
router.delete('/adsense/:id', async (req, res) => {
|
|
try {
|
|
await db.query('DELETE FROM adsense_configs WHERE id = $1', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ===================== 도메인 매핑 =====================
|
|
|
|
router.get('/domains', async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query(`
|
|
SELECT d.*, s.name AS site_name FROM domain_mappings d
|
|
LEFT JOIN sites s ON s.id = d.site_id ORDER BY d.id
|
|
`);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/domains', async (req, res) => {
|
|
try {
|
|
const { domain, site_id, adsense_config_id } = req.body;
|
|
const normalizedDomain = domain ? domain.trim().toLowerCase() : domain;
|
|
const { rows } = await db.query(
|
|
`INSERT INTO domain_mappings (domain, site_id, adsense_config_id) VALUES ($1, $2, $3) RETURNING *`,
|
|
[normalizedDomain, site_id, adsense_config_id || null]
|
|
);
|
|
res.json(rows[0]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/domains/:id', async (req, res) => {
|
|
try {
|
|
await db.query('DELETE FROM domain_mappings WHERE id = $1', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ===================== 로그 =====================
|
|
|
|
router.get('/logs', async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const { rows } = await db.query(`
|
|
SELECT l.*, s.name AS site_name FROM crawl_logs l
|
|
LEFT JOIN sites s ON s.id = l.site_id
|
|
ORDER BY l.created_at DESC LIMIT $1
|
|
`, [limit]);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|