fix: SSR 데이터 주입 방식 수정 - layout에서 ssrData script 태그 직접 출력

- app.js: initScript() 헬퍼로 <script>var __INIT__=데이터</script> 생성
- layout.ejs: <%- ssrData %> 로 body 뒤에 script 태그 삽입
- 모든 페이지에서 첫 로드 시 즉시 데이터 표시 확인 완료

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-03-27 12:56:59 +09:00
parent dcae228a24
commit 8c8e3ffeaa
2 changed files with 18 additions and 20 deletions
+17 -19
View File
@@ -97,44 +97,42 @@ app.get('/logout', (req, res) => {
// ===== 루트 → 관리자로 리다이렉트 ===== // ===== 루트 → 관리자로 리다이렉트 =====
app.get('/', (req, res) => res.redirect('/admin')); app.get('/', (req, res) => res.redirect('/admin'));
// 서버사이드 데이터를 script 태그로 만드는 헬퍼
function initScript(data) {
return '<script>var __INIT__=' + JSON.stringify(data).replace(/</g, '\\u003c') + ';</script>';
}
// ===== 관리자 페이지 (데이터 서버사이드 렌더링) ===== // ===== 관리자 페이지 (데이터 서버사이드 렌더링) =====
app.get('/admin', adminAuth, async (req, res) => { app.get('/admin', adminAuth, async (req, res) => {
try { try {
const sites = await db.query(`SELECT s.*, (SELECT COUNT(*) FROM crawl_results WHERE site_id = s.id) AS crawl_count FROM sites s ORDER BY s.id`); const sites = await db.query(`SELECT s.*, (SELECT COUNT(*) FROM crawl_results WHERE site_id = s.id) AS crawl_count FROM sites s ORDER BY s.id`);
const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id'); const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id');
const logs = 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 10`); const logs = 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 10`);
res.render('admin/dashboard', { res.render('admin/dashboard', { ssrData: initScript({ sites: sites.rows, adsense: adsense.rows, logs: logs.rows }) });
initialData: JSON.stringify({ sites: sites.rows, adsense: adsense.rows, logs: logs.rows }) } catch (e) { res.render('admin/dashboard', { ssrData: initScript({}) }); }
});
} catch (e) { res.render('admin/dashboard', { initialData: '{}' }); }
}); });
app.get('/admin/sites', adminAuth, async (req, res) => { app.get('/admin/sites', adminAuth, async (req, res) => {
try { try {
const sites = 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`); const sites = 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`);
const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id'); const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id');
res.render('admin/sites', { res.render('admin/sites', { ssrData: initScript({ sites: sites.rows, adsense: adsense.rows }) });
initialData: JSON.stringify({ sites: sites.rows, adsense: adsense.rows }) } catch (e) { res.render('admin/sites', { ssrData: initScript({}) }); }
});
} catch (e) { res.render('admin/sites', { initialData: '{}' }); }
}); });
app.get('/admin/sites/:id', adminAuth, async (req, res) => { app.get('/admin/sites/:id', adminAuth, async (req, res) => {
try { try {
const site = await db.query('SELECT * FROM sites WHERE id = $1', [req.params.id]); const site = await db.query('SELECT * FROM sites WHERE id = $1', [req.params.id]);
const results = 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 20`, [req.params.id]); const results = 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 20`, [req.params.id]);
res.render('admin/site-detail', { res.render('admin/site-detail', { siteId: req.params.id, ssrData: initScript({ site: site.rows[0] || {}, results: results.rows }) });
siteId: req.params.id, } catch (e) { res.render('admin/site-detail', { siteId: req.params.id, ssrData: initScript({}) }); }
initialData: JSON.stringify({ site: site.rows[0] || {}, results: results.rows })
});
} catch (e) { res.render('admin/site-detail', { siteId: req.params.id, initialData: '{}' }); }
}); });
app.get('/admin/adsense', adminAuth, async (req, res) => { app.get('/admin/adsense', adminAuth, async (req, res) => {
try { try {
const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id'); const adsense = await db.query('SELECT * FROM adsense_configs ORDER BY id');
res.render('admin/adsense', { initialData: JSON.stringify(adsense.rows) }); res.render('admin/adsense', { ssrData: initScript(adsense.rows) });
} catch (e) { res.render('admin/adsense', { initialData: '[]' }); } } catch (e) { res.render('admin/adsense', { ssrData: initScript([]) }); }
}); });
app.get('/admin/domains', adminAuth, async (req, res) => { app.get('/admin/domains', adminAuth, async (req, res) => {
@@ -142,15 +140,15 @@ app.get('/admin/domains', adminAuth, async (req, res) => {
const domains = 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`); const domains = 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`);
const sites = await db.query('SELECT id, name FROM sites ORDER BY id'); const sites = await db.query('SELECT id, name FROM sites ORDER BY id');
const adsense = await db.query('SELECT id, name FROM adsense_configs ORDER BY id'); const adsense = await db.query('SELECT id, name FROM adsense_configs ORDER BY id');
res.render('admin/domains', { initialData: JSON.stringify({ domains: domains.rows, sites: sites.rows, adsense: adsense.rows }) }); res.render('admin/domains', { ssrData: initScript({ domains: domains.rows, sites: sites.rows, adsense: adsense.rows }) });
} catch (e) { res.render('admin/domains', { initialData: '{}' }); } } catch (e) { res.render('admin/domains', { ssrData: initScript({}) }); }
}); });
app.get('/admin/logs', adminAuth, async (req, res) => { app.get('/admin/logs', adminAuth, async (req, res) => {
try { try {
const logs = 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 100`); const logs = 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 100`);
res.render('admin/logs', { initialData: JSON.stringify(logs.rows) }); res.render('admin/logs', { ssrData: initScript(logs.rows) });
} catch (e) { res.render('admin/logs', { initialData: '[]' }); } } catch (e) { res.render('admin/logs', { ssrData: initScript([]) }); }
}); });
// ===== API ===== // ===== API =====
+1 -1
View File
@@ -119,7 +119,7 @@ tr:hover td{background:rgba(255,255,255,.02)}
</div> </div>
</div> </div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script>var __INIT__ = <%- typeof initialData !== 'undefined' ? initialData : '{}' %>;</script> <%- typeof ssrData !== 'undefined' ? ssrData : '' %>
<script> <script>
function api(method, url, data) { function api(method, url, data) {
const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin' }; const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin' };