Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
80 lines
7.3 KiB
JavaScript
80 lines
7.3 KiB
JavaScript
'use strict';
|
||
/* ============================================================================
|
||
* 사이트 템플릿 화면 (v1.3.10)
|
||
* - 생성 탭 "템플릿에서 시작" (#templateStartCard): 관리자가 사용을 허용한 템플릿을 디렉터리(업종·별도)와
|
||
* 해시태그로 찾아 미리보고, 이름을 정해 새 사이트로 시작한다 (GET /api/templates, POST /api/sites/template).
|
||
* - 관리자 빌더(/builder)의 관리 탭 "템플릿으로 등록" (#tplRegisterCard): 지금 사이트를 템플릿으로 담는다
|
||
* (POST /api/admin/templates). 사용자 사용 허용·수정은 관리자 콘솔 › 템플릿 탭에서 한다.
|
||
* 전역: window.TemplatesUI = { showStart, onSiteOpened, dirOptions }
|
||
* ========================================================================== */
|
||
(() => {
|
||
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
|
||
const btn = (label, cls, fn) => { const b = el('button', cls || 'ghost small-btn', label); b.type = 'button'; b.onclick = fn; return b; };
|
||
let curDir = ''; let chosen = null; let timer = null;
|
||
|
||
/** 디렉터리 이름 표시: ind:음식점·카페/한식 → 음식점·카페 › 한식 (업종 이름은 번역 사전으로) */
|
||
function dirLabel(dir, tree) {
|
||
if (!dir) return T('전체');
|
||
if (dir.startsWith('ind:')) return dir.slice(4).split('/').map((x) => T(x)).join(' › ');
|
||
const find = (nodes) => { for (const n of nodes || []) { if (n.id === dir) return n.name; const f = find(n.children); if (f) return f; } return null; };
|
||
return find(tree && tree.custom) || dir;
|
||
}
|
||
/** 디렉터리 선택 상자 옵션 (관리자 등록·수정 화면에서도 쓴다) */
|
||
function dirOptions(tree, sel, value) {
|
||
sel.innerHTML = ''; sel.appendChild(new Option(T('디렉터리 선택'), ''));
|
||
const g1 = document.createElement('optgroup'); g1.label = T('업종');
|
||
for (const i of tree.industries) { g1.appendChild(new Option(T(i.name), i.id)); for (const m of i.children) g1.appendChild(new Option(` ${T(i.name)} › ${T(m.name)}`, m.id)); }
|
||
sel.appendChild(g1);
|
||
if ((tree.custom || []).length) { const g2 = document.createElement('optgroup'); g2.label = T('별도 디렉터리'); const walk = (nodes, depth) => { for (const n of nodes) { g2.appendChild(new Option(`${' '.repeat(depth)}${n.name}`, n.id)); walk(n.children || [], depth + 1); } }; walk(tree.custom, 0); sel.appendChild(g2); }
|
||
if (value) sel.value = value;
|
||
}
|
||
|
||
// ---------- 생성 탭: 템플릿에서 시작 ----------
|
||
async function load() {
|
||
const q = $('tplQ').value.trim(); let d; try { d = await api(`/api/templates?q=${encodeURIComponent(q)}&dir=${encodeURIComponent(curDir)}`); } catch (e) { $('tplList').textContent = e.message; return; }
|
||
// 디렉터리 트리 (템플릿이 있는 곳만)
|
||
const tr = $('tplTree'); tr.innerHTML = '';
|
||
const node = (n, depth) => { if (!n.count) return; const b = el('button', 'tpl-dir' + (curDir === n.id ? ' on' : ''), `${n.kind === 'industry' ? T(n.name) : n.name} (${n.count})`); b.type = 'button'; b.style.paddingLeft = (8 + depth * 14) + 'px'; b.onclick = () => { curDir = curDir === n.id ? '' : n.id; load(); }; tr.appendChild(b); for (const c of n.children || []) node(c, depth + 1); };
|
||
const all = el('button', 'tpl-dir' + (curDir ? '' : ' on'), `${T('전체')} (${d.tree.total})`); all.type = 'button'; all.onclick = () => { curDir = ''; load(); }; tr.appendChild(all);
|
||
for (const n of d.tree.industries) node(n, 0); for (const n of d.tree.custom) node(n, 0);
|
||
// 해시태그
|
||
const tg = $('tplTags'); tg.innerHTML = ''; for (const t of d.tags.slice(0, 16)) { const c = el('button', 'tpl-tag', `#${t.tag}`); c.type = 'button'; c.onclick = () => { const v = $('tplQ').value; if (!v.includes('#' + t.tag)) $('tplQ').value = (v + ' #' + t.tag).trim(); load(); }; tg.appendChild(c); }
|
||
// 목록
|
||
const list = $('tplList'); list.innerHTML = '';
|
||
for (const t of d.items) {
|
||
const card = el('div', 'tpl-card' + (chosen === t.id ? ' on' : ''));
|
||
card.appendChild(el('b', '', t.title)); card.appendChild(el('div', 'muted small', dirLabel(t.dir, d.tree)));
|
||
if (t.description) card.appendChild(el('div', 'small', t.description));
|
||
const tags = el('div', 'tpl-tags'); for (const g of t.tags) tags.appendChild(el('span', 'tpl-tag', '#' + g)); card.appendChild(tags);
|
||
const row = el('div', 'row'); const pv = el('a', 'btn ghost small-btn', T('미리보기')); pv.href = `/api/templates/${t.id}/preview/`; pv.target = '_blank'; pv.rel = 'noopener'; row.appendChild(pv);
|
||
row.appendChild(btn(T('이 템플릿 고르기'), chosen === t.id ? 'primary small-btn' : 'ghost small-btn', () => { chosen = t.id; $('tplChosen').textContent = TF('선택한 템플릿: {1}', t.title); $('tplStart').disabled = false; if (!$('tplTitle').value) $('tplTitle').placeholder = t.title; load(); }));
|
||
card.appendChild(row); list.appendChild(card);
|
||
}
|
||
if (!d.items.length) list.appendChild(el('p', 'muted small', q || curDir ? T('조건에 맞는 템플릿이 없습니다.') : T('아직 사용할 수 있는 템플릿이 없습니다.')));
|
||
}
|
||
function showStart() { $('templateStartCard').classList.remove('hidden'); chosen = null; $('tplStart').disabled = true; $('tplChosen').textContent = ''; load(); }
|
||
$('tplQ').oninput = () => { clearTimeout(timer); timer = setTimeout(load, 250); };
|
||
$('tplStart').onclick = async () => {
|
||
if (!chosen) return; $('tplStart').disabled = true;
|
||
try { const site = await api('/api/sites/template', { method: 'POST', body: JSON.stringify({ templateId: chosen, title: $('tplTitle').value.trim(), contactEmail: $('tplEmail').value.trim(), tier: $('tierSelect').value || '', optimize: seoOptsOf('templateStartCard') }) }); $('tplTitle').value = ''; $('tplEmail').value = ''; await openSite(site.id); await refreshSiteList(); }
|
||
catch (e) { alert(e.message); $('tplStart').disabled = false; }
|
||
};
|
||
|
||
// ---------- 관리자 빌더: 이 사이트를 템플릿으로 등록 ----------
|
||
let tree = null;
|
||
async function onSiteOpened(site) {
|
||
const card = $('tplRegisterCard'); const admin = config && config.user && config.user.role === 'admin';
|
||
const ready = admin && site && (!site.plan || ['generated', 'published'].includes(site.plan.stage)) && (site.files || []).some((f) => /\.html?$/i.test(f));
|
||
card.classList.toggle('hidden', !ready); if (!ready) return;
|
||
if (!tree) { try { tree = (await api('/api/templates')).tree; } catch (_) { return; } }
|
||
dirOptions(tree, $('tplRegDir')); $('tplRegTitle').value = site.title; $('tplRegMsg').textContent = '';
|
||
}
|
||
$('tplRegGo').onclick = async () => {
|
||
if (!current) return; $('tplRegMsg').textContent = T('담는 중…');
|
||
try { const t = await api('/api/admin/templates', { method: 'POST', body: JSON.stringify({ siteId: current.id, title: $('tplRegTitle').value, tags: $('tplRegTags').value, dir: $('tplRegDir').value, description: $('tplRegDesc').value }) }); $('tplRegMsg').textContent = TF('템플릿 "{1}" 으로 등록했습니다 (파일 {2}개). 사용자 사용 허용은 관리자 콘솔 › 템플릿 에서 켭니다.', t.title, t.files); }
|
||
catch (e) { $('tplRegMsg').textContent = e.message; }
|
||
};
|
||
|
||
window.TemplatesUI = { showStart, onSiteOpened, dirOptions, dirLabel };
|
||
})();
|