Files
crawlmanager/views/admin/domains.ejs
T
chpark c61f10560f init: 크롤링 관리 솔루션 초기 구성
- Express.js 기반 관리자 페이지 (사이트/크롤링/AdSense/도메인 관리)
- PostgreSQL 16 + Docker Compose (Traefik 연동)
- 크롤러: axios + cheerio 기반 HTML 파싱
- 스케줄러: node-cron 기반 자동 크롤링
- 공개 사이트: slug/도메인 기반 DB에서 렌더링 HTML 서빙
- 도메인: admin.startover.co.kr

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:44:19 +09:00

83 lines
3.4 KiB
Plaintext

<%- include('layout', { page: 'domains', pageTitle: '도메인 매핑', body: `
<div class="card">
<div class="card-header">
<h2>도메인 매핑</h2>
<button class="btn btn-primary" onclick="openModal()">+ 도메인 추가</button>
</div>
<p class="text-muted" style="margin-bottom:1rem;font-size:.82rem">
도메인을 특정 사이트에 연결하면, 해당 도메인으로 접속 시 크롤링 결과가 자동으로 표시됩니다.<br>
슬러그 기반 접근도 가능합니다: <code>/s/{slug}</code>
</p>
<table>
<thead><tr><th>도메인</th><th>연결 사이트</th><th>상태</th><th>등록일</th><th>액션</th></tr></thead>
<tbody id="dom-tbody"></tbody>
</table>
</div>
<div class="modal-overlay" id="domModal">
<div class="modal">
<h3>도메인 추가</h3>
<div class="form-group">
<label>도메인 (서브도메인 포함)</label>
<input id="dom-domain" placeholder="rank.example.com">
</div>
<div class="form-group">
<label>연결할 사이트</label>
<select id="dom-site"></select>
</div>
<div class="form-group">
<label>AdSense 설정 (선택)</label>
<select id="dom-adsense"><option value="">사이트 기본값 사용</option></select>
</div>
<div class="flex" style="justify-content:flex-end;gap:.5rem;margin-top:1rem">
<button class="btn btn-outline" onclick="document.getElementById('domModal').classList.remove('active')">취소</button>
<button class="btn btn-primary" onclick="saveDomain()">저장</button>
</div>
</div>
</div>
<script>
async function loadDomains() {
const [domains, sites, adsense] = await Promise.all([
api('GET', '/api/domains'),
api('GET', '/api/sites'),
api('GET', '/api/adsense'),
]);
document.getElementById('dom-site').innerHTML = sites.map(s => '<option value="' + s.id + '">' + s.name + '</option>').join('');
document.getElementById('dom-adsense').innerHTML = '<option value="">사이트 기본값</option>' + adsense.map(a => '<option value="' + a.id + '">' + a.name + '</option>').join('');
document.getElementById('dom-tbody').innerHTML = domains.map(d =>
'<tr><td><strong>' + d.domain + '</strong></td><td>' + (d.site_name || '-') + '</td>' +
'<td><span class="badge badge-' + (d.is_active ? 'success">활성' : 'danger">비활성') + '</span></td>' +
'<td class="text-muted">' + timeAgo(d.created_at) + '</td>' +
'<td><button class="btn btn-danger btn-sm" onclick="deleteDomain(' + d.id + ')">삭제</button></td></tr>'
).join('') || '<tr><td colspan="5" class="text-muted" style="text-align:center;padding:2rem">도메인 매핑을 추가하세요</td></tr>';
}
function openModal() { document.getElementById('domModal').classList.add('active'); }
async function saveDomain() {
const data = {
domain: document.getElementById('dom-domain').value,
site_id: parseInt(document.getElementById('dom-site').value),
adsense_config_id: document.getElementById('dom-adsense').value || null,
};
if (!data.domain) { toast('도메인을 입력하세요', 'error'); return; }
await api('POST', '/api/domains', data);
toast('도메인 추가 완료');
document.getElementById('domModal').classList.remove('active');
loadDomains();
}
async function deleteDomain(id) {
if (!confirm('삭제하시겠습니까?')) return;
await api('DELETE', '/api/domains/' + id);
toast('삭제 완료'); loadDomains();
}
loadDomains();
</script>
` }) %>