Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
366 lines
66 KiB
JavaScript
366 lines
66 KiB
JavaScript
/* ============================================================================
|
||
* 웹메이커 공통 모듈 런타임 (v1.0.3, v1.3.10 다국어 — 페이지 언어 날짜 형식·번역표 LT(), v1.4.18 일정관리 캘린더 R.calendar)
|
||
* 생성 사이트에 삽입된 모듈 위젯(data-wm-module)을 찾아 API 와 통신하고 내용을 그린다.
|
||
* 사이트별 설정은 모두 data-* 속성과 관리자 설정(API 응답)으로 들어오므로 이 파일에는 사이트 고유 값이 없다.
|
||
* - data-site : 사이트 ID - data-api : 모듈 API 기본 주소 (…/api/m/<siteId>)
|
||
* 회원 토큰은 localStorage 'wm_member_<siteId>' 에 저장되어 게시판·후기·예약·설문이 함께 쓴다.
|
||
* 이 파일을 사이트 안에 복사해 쓰려면 그대로 js/wm-modules.js 로 두고 <script src> 만 바꾸면 된다.
|
||
* ========================================================================== */
|
||
(function () {
|
||
if (window.__wmModulesLoaded) return; window.__wmModulesLoaded = true;
|
||
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); };
|
||
var nl2br = function (s) { return esc(s).replace(/\n/g, '<br>'); };
|
||
// v1.3.10 다국어: 페이지 언어(<html lang>)로 날짜를 쓰고, 화면 문구는 페이지에 들어 있는 번역표(<script type="application/json" data-wm-i18n>)로 바꾼다.
|
||
// 번역표는 다국어 사이트의 다른 언어 페이지에 서비스가 넣는다(lib/multilang.js). 없으면(기본 언어) 원문 그대로.
|
||
var PAGE_LANG = (document.documentElement.getAttribute('lang') || 'ko');
|
||
var DICT = (function () { try { var el = document.querySelector('script[data-wm-i18n]'); return el ? JSON.parse(el.textContent) : {}; } catch (e) { return {}; } })();
|
||
// v1.3.16: 문구는 코드에 직접 쓰지 않고 id 로만 쓴다. BASE 는 서비스가 이 파일을 보낼 때 채우는 원문(한국어) 표
|
||
// (server.js GET /wm-modules.js ← locales/site/ko.json). 페이지 번역표(DICT)에 그 언어 글자가 있으면 그것을 쓴다.
|
||
var BASE = /*@WM_BASE_MESSAGES@*/{};
|
||
/** LT('board.empty') / LT('members.welcome', { name: '홍길동' }) — {이름} 자리를 채운다 */
|
||
// v1.3.17 (B01): 1.3.16 에서는 이 함수 이름이 L 이라 지도 모듈의 Leaflet(window.L)을 가려 L.map 이 오류가 났다 → LT 로 바꿈
|
||
// v1.4.5 숫자 값(조회 수·댓글 수·남은 자리 등)은 3자리마다 쉼표 (페이지 언어 형식)
|
||
var numStr = function (v) { return typeof v === 'number' && isFinite(v) ? v.toLocaleString(document.documentElement.lang || undefined) : String(v); };
|
||
var LT = function (k, vars) { var s = DICT[k] || BASE[k] || k; return vars ? String(s).replace(/\{(\w+)\}/g, function (m, n) { return vars[n] !== undefined ? numStr(vars[n]) : m; }) : s; };
|
||
/** 관리자가 정한 설정 값(문의 유형·후기 분류 등)의 그 언어 번역 — 없으면 그대로 */
|
||
var LV = function (v) { return (v && DICT['v:' + v]) || v; };
|
||
/** v1.5.36 (요구 2): 위젯 속성(data-title 등)의 표시 글자. 속성이 없으면 문구표, 속성이 한국어 원문과 같은데 이 언어 번역이 있으면 번역 — 번역 단계를 거치지 않은 옛 페이지도 다른 언어에서 바르게 보이게 */
|
||
var AT = function (w, name, id) { var v = w.getAttribute(name); if (!v || !v.trim()) return LT(id); if (BASE[id] === v && DICT[id]) return DICT[id]; return v; };
|
||
/** v1.5.36: <input type=file> 의 브라우저 기본 글자("파일 선택")는 페이지 언어와 무관하게 브라우저 언어를 따르므로, 보이는 버튼을 따로 만들어 문구표 글자를 쓴다 */
|
||
var fileButton = function (input) {
|
||
if (!input || input.__wmFile) return; input.__wmFile = 1; var wrap = document.createElement('span'); wrap.className = 'wm-file'; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'wm-btn wm-file-btn'; btn.textContent = LT('inquiry.chooseFile'); var lab = document.createElement('span'); lab.className = 'wm-file-names'; lab.textContent = LT('inquiry.noFile');
|
||
input.parentNode.insertBefore(wrap, input); wrap.appendChild(btn); wrap.appendChild(lab); wrap.appendChild(input); input.style.position = 'absolute'; input.style.width = '1px'; input.style.height = '1px'; input.style.opacity = '0'; input.style.overflow = 'hidden'; input.tabIndex = -1;
|
||
btn.onclick = function () { input.click(); }; input.addEventListener('change', function () { var n = input.files ? input.files.length : 0; lab.textContent = n ? (n === 1 ? input.files[0].name : LT('inquiry.filesChosen', { n: n })) : LT('inquiry.noFile'); });
|
||
};
|
||
var fmtDate = function (s) { try { return new Date(s).toLocaleString(PAGE_LANG, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch (e) { return s; } };
|
||
var tokenKey = function (site) { return 'wm_member_' + site; };
|
||
var getToken = function (site) { try { return localStorage.getItem(tokenKey(site)) || ''; } catch (e) { return ''; } };
|
||
var setToken = function (site, t) { try { if (t) localStorage.setItem(tokenKey(site), t); else localStorage.removeItem(tokenKey(site)); } catch (e) {} };
|
||
/** API 호출: JSON 또는 FormData. 회원 토큰이 있으면 Authorization 헤더로 보낸다 */
|
||
function call(w, method, pathname, body, opts) {
|
||
var api = w.getAttribute('data-api'); var site = w.getAttribute('data-site'); var headers = {}; var tok = getToken(site);
|
||
if (tok) headers['Authorization'] = 'Bearer ' + tok;
|
||
headers['X-WM-Lang'] = PAGE_LANG; // v1.3.16 서버가 오류·안내 문구를 이 페이지 언어로 돌려준다
|
||
var init = { method: method, headers: headers, mode: 'cors', credentials: 'omit' };
|
||
if (body instanceof FormData) init.body = body; else if (body !== undefined) { headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
|
||
return fetch(api + pathname, init).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { if (!r.ok) { var e = new Error((j.error && LT(j.error)) || ('HTTP ' + r.status)); e.status = r.status; throw e; } return j; }); });
|
||
}
|
||
/** 폼 안의 name 필드 찾기 (form.elements 가 아닌 하위 어디에 있어도) */
|
||
var fld = function (form, name) { return form ? form.querySelector('[name="' + name + '"]') : null; };
|
||
var msg = function (w, text, ok) { var m = w.querySelector('.wm-msg'); if (m) { m.textContent = text || ''; m.className = 'wm-msg' + (ok ? ' ok' : (text ? ' err' : '')); } };
|
||
var baseCss = '.wm-btn{cursor:pointer}.wm-ghost{background:transparent}.wm-msg.err{color:#b91c1c}.wm-msg.ok{color:#047857}.wm-row{display:flex;gap:8px;flex-wrap:wrap}.wm-row>*{flex:1;min-width:140px}.wm-board table{width:100%;border-collapse:collapse}.wm-board td,.wm-board th{padding:8px 6px;border-bottom:1px solid #e5e7eb;text-align:left}.wm-board .wm-post-card{border:1px solid #e5e7eb;border-radius:10px;padding:14px;margin:10px 0}.wm-board .wm-comment{border-top:1px solid #eee;padding:8px 0}.wm-board .wm-comment.reply{margin-left:24px}.wm-pager button{margin:0 2px}.wm-pager .on{font-weight:700;text-decoration:underline}.wm-gallery-grid{display:grid;gap:10px}.wm-gallery-grid img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:8px;cursor:zoom-in}.wm-lightbox{position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:9999;flex-direction:column;color:#fff}.wm-lightbox img{max-width:92vw;max-height:80vh}.wm-slots{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}.wm-slot{padding:6px 10px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;cursor:pointer}.wm-slot.full{opacity:.4;cursor:not-allowed;text-decoration:line-through}.wm-slot.on{outline:2px solid #2563eb}.wm-kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(220px,1fr);gap:10px;overflow:auto}.wm-kanban-col{background:#f8fafc;border-radius:10px;padding:10px}.wm-kanban-col h4{margin:0 0 8px}.wm-review{border:1px solid #e5e7eb;border-radius:10px;padding:10px;margin:8px 0;background:#fff}.wm-stars{color:#f59e0b}.wm-faq details{border-bottom:1px solid #e5e7eb;padding:6px 0}.wm-faq summary{cursor:pointer;font-weight:600}.wm-map-canvas{width:100%;border-radius:10px;overflow:hidden}.wm-survey .wm-q{margin:12px 0}.wm-survey .wm-q label{display:block;font-weight:600;margin-bottom:4px}.wm-survey .wm-opt{display:block;font-weight:400}.wm-cats button{margin:0 4px 6px 0}.wm-cats .on{font-weight:700;text-decoration:underline}.wm-blog-card{border-bottom:1px solid #e5e7eb;padding:14px 0}.wm-blog-cover{max-width:100%;border-radius:8px;display:block;margin:6px 0}.wm-blog-search,.wm-library-search{display:flex;gap:6px;margin:8px 0}.wm-blog-search input,.wm-library-search input{flex:1}.wm-blog-body p{margin:0 0 1em}.wm-blog-tag{margin-right:4px;opacity:.8}.wm-library-item{display:flex;gap:12px;align-items:center;justify-content:space-between;border-bottom:1px solid #e5e7eb;padding:10px 0}.wm-library-info{min-width:0;flex:1}.wm-library-desc{margin:4px 0 0}.wm-lang-select{font:inherit;padding:2px 6px}.wm-blog-view .wm-cform{display:grid;gap:8px;max-width:640px;margin-top:10px}.wm-blog-view .wm-cform input,.wm-blog-view .wm-cform textarea{font:inherit;padding:6px 8px;width:100%;box-sizing:border-box}.wm-blog-view .wm-cform button{justify-self:start}.wm-blog-view .wm-meta{margin:4px 0 8px}' +
|
||
/* v1.4.1 게시판 목록 [검색][글쓰기] 한 줄: 버튼 글자가 줄바꿈되며(글쓰/기) 두 줄이 되던 것 — 버튼은 줄이지 않고 검색칸이 남은 폭을 쓴다 (블로그·자료실 검색 줄도 같게) */
|
||
'.wm-board-tools{display:flex;align-items:center;flex-wrap:nowrap;gap:8px;min-width:0;max-width:100%}.wm-board-tools .wm-board-search{flex:1 1 auto;min-width:0;width:auto}.wm-board-tools .wm-btn,.wm-blog-search .wm-btn,.wm-library-search .wm-btn{white-space:nowrap;flex:0 0 auto;width:auto;word-break:keep-all}.wm-blog-search,.wm-library-search{align-items:center}' +
|
||
/* v1.4.18 일정관리 캘린더: 월간 격자·주간/일간 시간표·일정 상세 팝업. 색은 .wm-cal-c-<색> 으로, 사이트는 .wm-cal 아래 규칙만 덧씌우면 된다 */
|
||
'.wm-cal-tools{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0}.wm-cal-tools .wm-cal-caption{font-weight:700;margin:0 8px;flex:1;min-width:120px}.wm-cal-tools .wm-cal-view.on{font-weight:700;text-decoration:underline}.wm-cal-grid{display:grid;grid-template-columns:repeat(7,1fr);border-left:1px solid #e5e7eb;border-top:1px solid #e5e7eb}.wm-cal-wd{padding:6px 4px;text-align:center;font-weight:600;font-size:13px;border-right:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;background:#f8fafc}.wm-cal-cell{min-height:92px;padding:4px;border-right:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;font-size:12px;overflow:hidden;cursor:pointer;min-width:0}.wm-cal-cell.other{opacity:.45}.wm-cal-cell.today .wm-cal-num,.wm-cal-head .today .wm-cal-num{background:#2563eb;color:#fff;border-radius:999px;padding:0 6px}.wm-cal-sun .wm-cal-num{color:#dc2626}.wm-cal-sat .wm-cal-num{color:#2563eb}.wm-cal-cell.today.wm-cal-sun .wm-cal-num,.wm-cal-cell.today.wm-cal-sat .wm-cal-num{color:#fff}.wm-cal-date{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.wm-cal-num{font-weight:600}.wm-cal-term{font-size:10px;color:#059669;border:1px solid #a7f3d0;border-radius:4px;padding:0 3px;white-space:nowrap}.wm-cal-sub{color:#6b7280;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wm-cal-ev{display:block;width:100%;text-align:left;border:0;border-left:3px solid #2563eb;background:#eff6ff;color:inherit;font:inherit;font-size:11px;padding:1px 4px;margin:2px 0;border-radius:3px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wm-cal-c-green{border-left-color:#059669;background:#ecfdf5}.wm-cal-c-red{border-left-color:#dc2626;background:#fef2f2}.wm-cal-c-orange{border-left-color:#ea580c;background:#fff7ed}.wm-cal-c-purple{border-left-color:#7c3aed;background:#f5f3ff}.wm-cal-c-gray{border-left-color:#6b7280;background:#f3f4f6}.wm-cal-more{background:none;border:0;color:#2563eb;font-size:11px;cursor:pointer;padding:0 4px}.wm-cal-week{overflow:auto}.wm-cal-week .wm-cal-row{display:grid;grid-template-columns:56px repeat(var(--wm-cal-cols,7),minmax(0,1fr));min-width:560px}.wm-cal-week.one .wm-cal-row{min-width:0}.wm-cal-head>div,.wm-cal-allday>div{border-right:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;padding:4px;font-size:12px;min-height:28px;min-width:0}.wm-cal-head{border-top:1px solid #e5e7eb}.wm-cal-head .wm-cal-sub{white-space:normal}.wm-cal-timegrid>div{border-right:1px solid #e5e7eb;position:relative;min-width:0}.wm-cal-hour{height:44px;border-bottom:1px solid #f1f5f9;box-sizing:border-box;font-size:10px;color:#6b7280;padding:2px 4px}.wm-cal-tev{position:absolute;box-sizing:border-box;overflow:hidden;white-space:normal;line-height:1.25;margin:0}.wm-cal-list{margin-top:10px}.wm-cal-list .wm-cal-ev{white-space:normal;padding:6px 8px;font-size:13px}.wm-cal-info{font-size:13px;color:#374151;margin:6px 0}.wm-cal-pop{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:9999}.wm-cal-pop .wm-cal-card{background:#fff;color:#111;border-radius:12px;padding:18px;max-width:420px;width:92vw;max-height:80vh;overflow:auto}.wm-cal-card h3{margin:0 0 8px}.wm-cal-card .wm-meta{color:#6b7280;font-size:13px;margin:4px 0}.wm-cal-card .wm-body{margin-top:8px}.wm-cal-empty{color:#6b7280;padding:12px 0}';
|
||
var st = document.createElement('style'); st.textContent = baseCss; document.head.appendChild(st);
|
||
|
||
var R = {};
|
||
// ---------------- 회원 ----------------
|
||
R.members = function (w) {
|
||
var site = w.getAttribute('data-site'); var status = w.querySelector('.wm-members-status'); var fl = w.querySelector('.wm-members-login'); var fs = w.querySelector('.wm-members-signup'); if (!status || !fl || !fs) { console.warn('wm members: 표준 마크업(.wm-members-status/.wm-members-login/.wm-members-signup)이 필요합니다'); return; }
|
||
function show(me) {
|
||
if (me) { status.innerHTML = '<span>' + esc(LT('members.welcome', { name: me.name })) + '</span> <button type="button" class="wm-btn wm-ghost wm-logout">' + LT('members.logout') + '</button>'; fl.hidden = true; fs.hidden = true; status.querySelector('.wm-logout').onclick = function () { setToken(site, ''); show(null); document.dispatchEvent(new CustomEvent('wm:member', { detail: null })); }; }
|
||
else { status.innerHTML = ''; fl.hidden = false; fs.hidden = true; }
|
||
}
|
||
function me() { if (!getToken(site)) return show(null); call(w, 'GET', '/members/me').then(function (r) { show(r.member); }).catch(function () { setToken(site, ''); show(null); }); }
|
||
w.querySelector('.wm-to-signup').onclick = function () { fl.hidden = true; fs.hidden = false; }; w.querySelector('.wm-to-login').onclick = function () { fs.hidden = true; fl.hidden = false; };
|
||
fl.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(fl)); call(w, 'POST', '/members/login', d).then(function (r) { setToken(site, r.token); show(r.member); document.dispatchEvent(new CustomEvent('wm:member', { detail: r.member })); }).catch(function (err) { msg(fl, err.message); }); };
|
||
fs.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(fs)); call(w, 'POST', '/members/signup', d).then(function (r) { if (r.pending) { msg(fs, LT('members.pending'), true); fs.reset(); } else { setToken(site, r.token); show(r.member); document.dispatchEvent(new CustomEvent('wm:member', { detail: r.member })); } }).catch(function (err) { msg(fs, err.message); }); };
|
||
me();
|
||
};
|
||
// ---------------- 게시판 ----------------
|
||
R.board = function (w) {
|
||
var site = w.getAttribute('data-site'); var style = w.getAttribute('data-style'); var fixedCat = w.getAttribute('data-category') || ''; var page = 1; var cat = fixedCat; var q = ''; var settings = null; var canWrite = false;
|
||
var bt = w.querySelector('.wm-board-title'); if (bt && !bt.textContent.trim()) bt.textContent = AT(w, 'data-title', 'board.title');
|
||
var mk = function (cls) { var d = document.createElement('div'); d.className = cls; w.appendChild(d); return d; };
|
||
var list = w.querySelector('.wm-board-list') || mk('wm-board-list'), pager = w.querySelector('.wm-board-pager') || mk('wm-board-pager'), view = w.querySelector('.wm-board-view') || mk('wm-board-view'), form = w.querySelector('.wm-board-form') || w.querySelector('form') || mk('wm-board-form'), cats = w.querySelector('.wm-board-cats') || mk('wm-board-cats');
|
||
function load() {
|
||
var qs = '?page=' + page + (cat ? '&cat=' + encodeURIComponent(cat) : '') + (q ? '&q=' + encodeURIComponent(q) : '') + (w.getAttribute('data-size') ? '&size=' + w.getAttribute('data-size') : '');
|
||
call(w, 'GET', '/board/posts' + qs).then(function (r) {
|
||
settings = r.settings; canWrite = r.canWrite; if (!style) style = settings.style || 'list';
|
||
if (wb) wb.style.display = canWrite ? '' : 'none';
|
||
if (!fixedCat && settings.categories && settings.categories.length) { cats.className = 'wm-board-cats wm-cats'; cats.innerHTML = '<button type="button" class="wm-btn wm-ghost' + (!cat ? ' on' : '') + '" data-c="">' + LT('common.all') + '</button>' + settings.categories.map(function (c) { return '<button type="button" class="wm-btn wm-ghost' + (cat === c ? ' on' : '') + '" data-c="' + esc(c) + '">' + esc(LV(c)) + '</button>'; }).join(''); cats.querySelectorAll('button').forEach(function (b) { b.onclick = function () { cat = b.getAttribute('data-c'); page = 1; load(); }; }); }
|
||
if (!r.items.length) list.innerHTML = '<p class="wm-empty">' + LT('board.empty') + '</p>';
|
||
else if (style === 'forum') list.innerHTML = r.items.map(function (p) { return '<article class="wm-post-card" data-id="' + p.id + '">' + (p.pinned ? '<span class="wm-pin">📌</span> ' : '') + '<h3 class="wm-post-title">' + (p.category ? '<span class="wm-cat">[' + esc(p.category) + ']</span> ' : '') + esc(p.title) + '</h3><p class="wm-excerpt">' + esc(p.excerpt) + '</p><div class="wm-meta">' + esc(p.author) + ' · ' + fmtDate(p.createdAt) + ' ' + esc(LT('board.metaViews', { n: p.views })) + ' ' + esc(LT('board.metaComments', { n: p.comments })) + '</div></article>'; }).join('');
|
||
else list.innerHTML = '<table><thead><tr><th>' + LT('common.subject') + '</th><th>' + LT('board.author') + '</th><th>' + LT('common.date') + '</th><th>' + LT('board.views') + '</th></tr></thead><tbody>' + r.items.map(function (p) { return '<tr data-id="' + p.id + '"><td>' + (p.pinned ? '📌 ' : '') + (p.category ? '<span class="wm-cat">[' + esc(p.category) + ']</span> ' : '') + esc(p.title) + (p.comments ? ' <span class="wm-cc">[' + p.comments + ']</span>' : '') + '</td><td>' + esc(p.author) + '</td><td>' + fmtDate(p.createdAt).slice(0, 12) + '</td><td>' + numStr(p.views || 0) + '</td></tr>'; }).join('') + '</tbody></table>';
|
||
list.querySelectorAll('[data-id]').forEach(function (el) { el.style.cursor = 'pointer'; el.onclick = function () { open(el.getAttribute('data-id')); }; });
|
||
var pages = Math.max(1, Math.ceil(r.total / r.size)); pager.className = 'wm-board-pager wm-pager'; pager.innerHTML = ''; for (var i = 1; i <= pages && i <= 50; i++) { (function (i) { var b = document.createElement('button'); b.type = 'button'; b.className = 'wm-btn wm-ghost' + (i === page ? ' on' : ''); b.textContent = i; b.onclick = function () { page = i; load(); }; pager.appendChild(b); })(i); }
|
||
}).catch(function (err) { list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; if (err.status === 401) list.innerHTML += '<p>' + LT('members.loginRequired') + '</p>'; });
|
||
}
|
||
function open(id, pw) {
|
||
call(w, 'GET', '/board/posts/' + id + (pw ? '?pw=' + encodeURIComponent(pw) : '')).then(function (r) {
|
||
list.hidden = true; pager.hidden = true; cats.hidden = true; form.hidden = true; view.hidden = false;
|
||
if (r.locked) { view.innerHTML = '<h3>' + esc(r.post.title) + '</h3><p>' + (r.needPassword ? LT('board.secretAskPw') : LT('board.secretOnlyOwner')) + '</p>' + (r.needPassword ? '<form class="wm-pw"><input name="pw" type="password" placeholder="' + LT('common.password') + '"><button class="wm-btn">' + LT('common.confirm') + '</button></form>' : '') + '<button type="button" class="wm-btn wm-ghost wm-back">' + LT('common.list') + '</button>'; var f = view.querySelector('.wm-pw'); if (f) f.onsubmit = function (e) { e.preventDefault(); open(id, f.pw.value); }; view.querySelector('.wm-back').onclick = back; return; }
|
||
var p = r.post;
|
||
var cm = r.comments.map(function (c) { return '<div class="wm-comment' + (c.parentId ? ' reply' : '') + '" data-id="' + c.id + '"><b>' + esc(c.author) + '</b> <span class="wm-meta">' + fmtDate(c.createdAt) + '</span><div>' + nl2br(c.body) + '</div>' + (r.canComment ? '<button type="button" class="wm-link wm-reply">' + LT('board.reply') + '</button> ' : '') + '<button type="button" class="wm-link wm-del-c">' + LT('common.delete') + '</button></div>'; }).join('');
|
||
view.innerHTML = '<article class="wm-post"><h3>' + (p.category ? '[' + esc(p.category) + '] ' : '') + esc(p.title) + '</h3><div class="wm-meta">' + esc(p.author) + ' · ' + fmtDate(p.createdAt) + ' ' + esc(LT('board.metaViews', { n: p.views })) + '</div><div class="wm-body">' + nl2br(p.body) + '</div></article><div class="wm-row"><button type="button" class="wm-btn wm-ghost wm-back">' + LT('common.list') + '</button>' + (r.canEdit ? '<button type="button" class="wm-btn wm-ghost wm-del">' + LT('common.delete') + '</button>' : '<button type="button" class="wm-btn wm-ghost wm-del">' + LT('board.deleteWithPw') + '</button>') + '</div><section class="wm-comments"><h4>' + esc(LT('board.comments', { n: r.comments.length })) + '</h4>' + cm + (r.canComment ? '<form class="wm-cform"><input type="hidden" name="parentId"><textarea name="body" rows="3" placeholder="' + LT('board.commentPlaceholder') + '" required></textarea><div class="wm-row wm-guest"><input name="name" placeholder="' + LT('common.name') + '"><input name="password" type="password" placeholder="' + LT('board.guestPwDelete') + '"></div><label class="wm-check"><input type="checkbox" name="secret"> ' + LT('board.commentSecret') + '</label><button class="wm-btn">' + LT('board.commentSubmit') + '</button><p class="wm-msg"></p></form>' : '') + '</section>';
|
||
view.querySelector('.wm-back').onclick = back;
|
||
view.querySelector('.wm-del').onclick = function () { var pwd = r.canEdit ? (pw || '') : prompt(LT('board.deleteAskPw')); /* v1.3.17 (B02): 비밀번호로 연 비회원 글은 그 비밀번호로 삭제 (회원·관리자는 빈 값) */ if (!r.canEdit && !pwd) return; if (!confirm(LT('board.deleteConfirm'))) return; call(w, 'DELETE', '/board/posts/' + id, { password: pwd }).then(function () { back(); load(); }).catch(function (e) { alert(e.message); }); };
|
||
var cf = view.querySelector('.wm-cform'); if (cf) { if (getToken(site)) cf.querySelector('.wm-guest').style.display = 'none'; cf.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(cf)); d.secret = !!cf.secret.checked; call(w, 'POST', '/board/posts/' + id + '/comments', d).then(function () { open(id, pw); }).catch(function (err) { msg(cf, err.message); }); }; }
|
||
view.querySelectorAll('.wm-reply').forEach(function (b) { b.onclick = function () { cf.parentId.value = b.closest('.wm-comment').getAttribute('data-id'); cf.body.focus(); cf.body.placeholder = LT('board.replyPlaceholder'); }; });
|
||
view.querySelectorAll('.wm-del-c').forEach(function (b) { b.onclick = function () { var cid = b.closest('.wm-comment').getAttribute('data-id'); var pwd = getToken(site) ? '' : prompt(LT('board.commentPw')); if (!getToken(site) && !pwd) return; call(w, 'DELETE', '/board/comments/' + cid, { password: pwd }).then(function () { open(id, pw); }).catch(function (e) { alert(e.message); }); }; });
|
||
}).catch(function (err) { alert(err.message); });
|
||
}
|
||
function back() { view.hidden = true; form.hidden = true; list.hidden = false; pager.hidden = false; cats.hidden = false; }
|
||
var wb = w.querySelector('.wm-board-write'); if (wb) wb.onclick = function () { list.hidden = true; pager.hidden = true; cats.hidden = true; view.hidden = true; form.hidden = false; var sel = fld(form, 'category'); if (sel) sel.innerHTML = (settings && settings.categories || []).map(function (c) { return '<option value="' + esc(c) + '">' + esc(LV(c)) + '</option>'; }).join(''); var g = form.querySelector('.wm-board-guest'); if (g) g.style.display = getToken(site) ? 'none' : ''; var ck = form.querySelector('.wm-check'); if (ck) ck.style.display = settings && settings.allowSecret ? '' : 'none'; };
|
||
var cb = w.querySelector('.wm-board-cancel'); if (cb) cb.onclick = back;
|
||
form.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(form)); var sc = fld(form, 'secret'); d.secret = !!(sc && sc.checked); call(w, 'POST', '/board/posts', d).then(function () { form.reset(); back(); page = 1; load(); }).catch(function (err) { msg(form, err.message); }); };
|
||
var search = w.querySelector('.wm-board-search'); if (search) search.onkeydown = function (e) { if (e.key === 'Enter') { q = search.value.trim(); page = 1; load(); } };
|
||
document.addEventListener('wm:member', function () { load(); });
|
||
load();
|
||
};
|
||
// ---------------- 문의폼 ----------------
|
||
R.inquiry = function (w) {
|
||
var it = w.querySelector('.wm-inquiry-title'); if (it && !it.textContent.trim()) it.textContent = AT(w, 'data-title', 'inquiry.title'); var sb = w.querySelector('.wm-inquiry-submit'); if (sb && !sb.textContent.trim()) sb.textContent = AT(w, 'data-button', 'common.send');
|
||
var form = w.querySelector('.wm-inquiry-form') || w.querySelector('form'); if (!form) return; var catEl = fld(form, 'category'), filesEl = fld(form, 'files');
|
||
call(w, 'GET', '/inquiry/settings').then(function (s) { if (catEl) catEl.innerHTML = (s.categories || []).map(function (c) { return '<option value="' + esc(c) + '">' + esc(LV(c)) + '</option>'; }).join(''); if (!s.allowFiles) { var fw = w.querySelector('.wm-inquiry-files'); if (fw) fw.style.display = 'none'; } else if (filesEl) { filesEl.setAttribute('data-max', s.maxFileMb || 10); fileButton(filesEl); } }).catch(function () {});
|
||
form.onsubmit = function (e) {
|
||
e.preventDefault(); if (!form.checkValidity()) { msg(form, LT('common.requiredCheck')); return; }
|
||
var fd = new FormData(form); fd.append('kind', w.getAttribute('data-kind') || 'contact'); fd.append('page', location.href);
|
||
var max = (parseInt(filesEl && filesEl.getAttribute('data-max'), 10) || 10) * 1048576; if (filesEl) for (var i = 0; i < filesEl.files.length; i++) if (filesEl.files[i].size > max) { msg(form, LT('inquiry.fileTooBig', { mb: max / 1048576 })); return; }
|
||
var b = form.querySelector('.wm-inquiry-submit') || form.querySelector('button[type=submit]') || form.querySelector('button'); if (b) b.disabled = true; msg(form, LT('common.sending'));
|
||
call(w, 'POST', '/inquiry/submit', fd).then(function (r) { msg(form, (r.message && LT(r.message)) || LT('common.receivedContact'), true); form.reset(); }).catch(function (err) { msg(form, err.message); }).then(function () { if (b) b.disabled = false; });
|
||
};
|
||
};
|
||
// ---------------- 설문 ----------------
|
||
R.survey = function (w) {
|
||
var form = w.querySelector('.wm-survey-form') || w.querySelector('form'); if (!form) return; var box = w.querySelector('.wm-survey-questions') || (function () { var d = document.createElement('div'); d.className = 'wm-survey-questions'; form.insertBefore(d, form.firstChild); return d; })(); var id = w.getAttribute('data-survey');
|
||
(id ? call(w, 'GET', '/survey/forms/' + id) : call(w, 'GET', '/survey/forms').then(function (r) { if (!r.items || !r.items.length) throw new Error(LT('survey.none')); return call(w, 'GET', '/survey/forms/' + r.items[0].id); })).then(function (r) {
|
||
var f = r.survey; var stt = w.querySelector('.wm-survey-title'); if (stt && !stt.textContent.trim()) stt.textContent = w.getAttribute('data-title') || f.title; var sd = w.querySelector('.wm-survey-desc'); if (sd && !sd.textContent.trim()) sd.textContent = f.description || '';
|
||
box.innerHTML = f.questions.map(function (q) {
|
||
var req = q.required ? ' <span class="wm-req">*</span>' : ''; var inner = '';
|
||
if (q.type === 'long') inner = '<textarea name="' + q.id + '" rows="3"' + (q.required ? ' required' : '') + '></textarea>';
|
||
else if (q.type === 'single') inner = q.options.map(function (o) { return '<label class="wm-opt"><input type="radio" name="' + q.id + '" value="' + esc(o) + '"' + (q.required ? ' required' : '') + '> ' + esc(o) + '</label>'; }).join('');
|
||
else if (q.type === 'multi') inner = q.options.map(function (o) { return '<label class="wm-opt"><input type="checkbox" name="' + q.id + '" value="' + esc(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||
else if (q.type === 'scale') { inner = '<div class="wm-row wm-scale">'; for (var i = q.min; i <= q.max; i++) inner += '<label class="wm-opt"><input type="radio" name="' + q.id + '" value="' + i + '"' + (q.required ? ' required' : '') + '> ' + i + '</label>'; inner += '</div>'; }
|
||
else inner = '<input name="' + q.id + '" type="' + (q.type === 'date' ? 'date' : q.type === 'email' ? 'email' : q.type === 'number' ? 'number' : 'text') + '"' + (q.required ? ' required' : '') + '>';
|
||
return '<div class="wm-q"><label>' + esc(q.label) + req + '</label>' + inner + '</div>';
|
||
}).join('');
|
||
form.onsubmit = function (e) { e.preventDefault(); var answers = {}; f.questions.forEach(function (q) { if (q.type === 'multi') answers[q.id] = Array.from(form.querySelectorAll('[name="' + q.id + '"]:checked')).map(function (x) { return x.value; }); else { var el = form.querySelector('[name="' + q.id + '"]' + (q.type === 'single' || q.type === 'scale' ? ':checked' : '')); answers[q.id] = el ? el.value : ''; } }); call(w, 'POST', '/survey/forms/' + f.id + '/responses', { answers: answers, respondent: (fld(form, 'respondent') || {}).value || '' }).then(function (r) { form.innerHTML = '<p class="wm-msg ok">' + esc((r.message && LT(r.message)) || f.thanks) + '</p>'; }).catch(function (err) { msg(form, err.message); }); };
|
||
}).catch(function (err) { var t = w.querySelector('.wm-survey-title'); if (t) t.textContent = err.message; form.hidden = true; });
|
||
};
|
||
// ---------------- 예약 ----------------
|
||
R.booking = function (w) {
|
||
var form = w.querySelector('.wm-booking-form') || w.querySelector('form'); if (!form) return; var slots = w.querySelector('.wm-booking-slots') || (function () { var d = document.createElement('div'); d.className = 'wm-booking-slots'; form.insertBefore(d, form.firstChild); return d; })(); var tt = w.querySelector('.wm-booking-title'); if (tt && !tt.textContent.trim()) tt.textContent = AT(w, 'data-title', 'booking.title'); var nl = w.querySelector('.wm-booking-note-label'); if (nl && !nl.textContent.trim()) nl.insertBefore(document.createTextNode(AT(w, 'data-note-label', 'booking.note')), nl.firstChild);
|
||
var dateEl = fld(form, 'date'), timeEl = fld(form, 'time'); if (!dateEl) { console.warn('wm booking: name="date" 입력이 없습니다'); return; } if (!timeEl) { timeEl = document.createElement('input'); timeEl.type = 'hidden'; timeEl.name = 'time'; form.appendChild(timeEl); }
|
||
var submit = form.querySelector('button[type=submit]') || form.querySelector('button');
|
||
call(w, 'GET', '/booking/settings').then(function (s) { var d = new Date(); d.setDate(d.getDate() + (s.leadDays || 0)); dateEl.min = d.toISOString().slice(0, 10); var m = new Date(); m.setDate(m.getDate() + (s.maxDays || 60)); dateEl.max = m.toISOString().slice(0, 10); }).catch(function () {});
|
||
dateEl.onchange = function () { timeEl.value = ''; submit.disabled = true; slots.innerHTML = LT('common.loading'); call(w, 'GET', '/booking/slots?date=' + dateEl.value).then(function (r) { slots.className = 'wm-booking-slots wm-slots'; if (r.closed) { slots.innerHTML = '<p>' + LT('booking.closedDate') + '</p>'; return; } if (!r.slots.length) { slots.innerHTML = '<p>' + LT('booking.noSlots') + '</p>'; return; } slots.innerHTML = r.slots.map(function (s) { return '<button type="button" class="wm-slot' + (s.left ? '' : ' full') + '" data-t="' + s.time + '"' + (s.left ? '' : ' disabled') + '>' + s.time + (s.left && s.left < 3 ? ' <small>' + esc(LT('booking.seatsLeft', { n: s.left })) + '</small>' : '') + '</button>'; }).join(''); slots.querySelectorAll('.wm-slot:not(.full)').forEach(function (b) { b.onclick = function () { slots.querySelectorAll('.wm-slot').forEach(function (x) { x.classList.remove('on'); }); b.classList.add('on'); timeEl.value = b.getAttribute('data-t'); submit.disabled = false; }; }); }).catch(function (err) { slots.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; }); };
|
||
form.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(form)); submit.disabled = true; call(w, 'POST', '/booking/reserve', d).then(function (r) { msg(form, (r.message && LT(r.message)) || (r.booking.status === 'confirmed' ? LT('booking.confirmed') : LT('booking.received')), true); form.reset(); slots.innerHTML = ''; }).catch(function (err) { msg(form, err.message); submit.disabled = false; }); };
|
||
};
|
||
// ---------------- 갤러리 ----------------
|
||
R.gallery = function (w) {
|
||
var grid = w.querySelector('.wm-gallery-grid'); if (!grid) return; var tagsBox = w.querySelector('.wm-gallery-tags') || document.createElement('div'); var gt = w.querySelector('.wm-gallery-title'); if (gt && !gt.textContent.trim()) gt.textContent = AT(w, 'data-title', 'gallery.title'); var tag = w.getAttribute('data-tag') || '';
|
||
call(w, 'GET', '/gallery/list').then(function (r) {
|
||
var cols = parseInt(w.getAttribute('data-columns'), 10) || parseInt(r.settings.columns, 10) || 3; grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
||
var all = r.items; var tags = []; all.forEach(function (i) { (i.tags || []).forEach(function (t) { if (tags.indexOf(t) < 0) tags.push(t); }); });
|
||
function draw() { var items = tag ? all.filter(function (i) { return (i.tags || []).indexOf(tag) >= 0; }) : all; grid.innerHTML = items.length ? items.map(function (i, idx) { return '<figure class="wm-gallery-item" data-i="' + idx + '"><img src="' + esc(i.thumb || i.src) + '" alt="' + esc(i.title) + '" loading="lazy">' + (i.title ? '<figcaption>' + esc(i.title) + (i.caption ? '<small> ' + esc(i.caption) + '</small>' : '') + '</figcaption>' : '') + '</figure>'; }).join('') : '<p class="wm-empty">' + LT('gallery.empty') + '</p>'; if (r.settings.lightbox !== false) grid.querySelectorAll('img').forEach(function (img, idx) { img.onclick = function () { var it = items[idx]; var lb = document.createElement('div'); lb.className = 'wm-lightbox'; lb.innerHTML = '<img src="' + esc(it.src) + '" alt=""><p>' + esc(it.title || '') + ' ' + esc(it.caption || '') + '</p>'; lb.onclick = function () { lb.remove(); }; document.body.appendChild(lb); }; }); }
|
||
if (!w.getAttribute('data-tag') && tags.length) { tagsBox.className = 'wm-gallery-tags wm-cats'; tagsBox.innerHTML = '<button type="button" class="wm-btn wm-ghost on" data-t="">' + LT('common.all') + '</button>' + tags.map(function (t) { return '<button type="button" class="wm-btn wm-ghost" data-t="' + esc(t) + '">' + esc(t) + '</button>'; }).join(''); tagsBox.querySelectorAll('button').forEach(function (b) { b.onclick = function () { tag = b.getAttribute('data-t'); tagsBox.querySelectorAll('button').forEach(function (x) { x.classList.toggle('on', x === b); }); draw(); }; }); }
|
||
draw();
|
||
}).catch(function (err) { grid.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; });
|
||
};
|
||
// ---------------- 지도 (Leaflet + OSM) ----------------
|
||
R.map = function (w) {
|
||
var canvas = w.querySelector('.wm-map-canvas') || (function () { var d = document.createElement('div'); d.className = 'wm-map-canvas'; w.insertBefore(d, w.firstChild); return d; })(); canvas.style.height = (parseInt(w.getAttribute('data-height'), 10) || 360) + 'px';
|
||
function ensureLeaflet(cb) { if (window.L) return cb(); var css = document.createElement('link'); css.rel = 'stylesheet'; css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; document.head.appendChild(css); var s = document.createElement('script'); s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; s.onload = cb; document.head.appendChild(s); }
|
||
function draw(lat, lng, label, address) { ensureLeaflet(function () { var zoom = parseInt(w.getAttribute('data-zoom'), 10) || 16; var map = L.map(canvas).setView([lat, lng], zoom); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); var mk = L.marker([lat, lng]).addTo(map); if (label || address) mk.bindPopup(esc(label || address)).openPopup(); var ad = w.querySelector('.wm-map-address'); if (ad && !ad.textContent.trim()) ad.textContent = address || ''; }); }
|
||
var lat = parseFloat(w.getAttribute('data-lat')), lng = parseFloat(w.getAttribute('data-lng')); var address = w.getAttribute('data-address') || ''; var label = w.getAttribute('data-label') || '';
|
||
var go = function (a, la, ln, lb) { if (isFinite(la) && isFinite(ln)) draw(la, ln, lb, a); else if (a) call(w, 'GET', '/map/geocode?q=' + encodeURIComponent(a)).then(function (r) { if (r && r.lat) draw(r.lat, r.lng, lb, a); else canvas.textContent = LT('map.notFound', { address: a }); }).catch(function () { canvas.textContent = LT('map.loadFailed'); }); else canvas.textContent = LT('map.noAddress'); };
|
||
if (!address && !(isFinite(lat) && isFinite(lng))) call(w, 'GET', '/map/settings').then(function (s) { go(s.address, parseFloat(s.lat), parseFloat(s.lng), s.label); }).catch(function () { go('', NaN, NaN, ''); }); else go(address, lat, lng, label);
|
||
};
|
||
// ---------------- 후기 ----------------
|
||
R.reviews = function (w) {
|
||
var list = w.querySelector('.wm-reviews-list'); var form = w.querySelector('.wm-reviews-form') || w.querySelector('form'); var rt = w.querySelector('.wm-reviews-title'); if (rt && !rt.textContent.trim()) rt.textContent = AT(w, 'data-title', 'reviews.title'); if (form && w.getAttribute('data-form') === '0') form.hidden = true; if (!list) return;
|
||
var stars = function (n) { return '<span class="wm-stars">' + '★★★★★'.slice(0, n) + '<span style="color:#d1d5db">' + '★★★★★'.slice(n) + '</span></span>'; };
|
||
function load() { call(w, 'GET', '/reviews/list').then(function (r) { var style = w.getAttribute('data-style') || r.settings.style || 'list'; var rc = fld(form, 'category'); if (rc) rc.innerHTML = '<option value="">' + LT('common.category') + '</option>' + (r.settings.categories || []).map(function (c) { return '<option value="' + esc(c) + '">' + esc(LV(c)) + '</option>'; }).join(''); var card = function (x) { return '<div class="wm-review">' + stars(x.rating) + ' <b>' + esc(x.name) + '</b>' + (x.category ? ' <span class="wm-cat">' + esc(x.category) + '</span>' : '') + '<p>' + nl2br(x.text) + '</p><div class="wm-meta">' + fmtDate(x.createdAt).slice(0, 12) + '</div></div>'; };
|
||
if (!r.items.length) list.innerHTML = '<p class="wm-empty">' + LT('reviews.empty') + '</p>';
|
||
else if (style === 'kanban') { var by = r.settings.kanbanBy || 'rating'; var cols = by === 'rating' ? [['5', '★5'], ['4', '★4'], ['3', LT('reviews.low')]] : (r.settings.categories || []).map(function (c) { return [c, LV(c)]; }).concat([['', LT('common.other')]]); list.innerHTML = '<div class="wm-kanban">' + cols.map(function (c) { var items = r.items.filter(function (x) { return by === 'rating' ? (c[0] === '3' ? x.rating <= 3 : String(x.rating) === c[0]) : (x.category || '') === c[0]; }); return '<div class="wm-kanban-col"><h4>' + esc(c[1]) + ' <small>' + items.length + '</small></h4>' + items.map(card).join('') + '</div>'; }).join('') + '</div>'; }
|
||
else list.innerHTML = r.items.map(card).join(''); }).catch(function (err) { list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; }); }
|
||
if (form) form.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(form)); call(w, 'POST', '/reviews/submit', d).then(function (r) { msg(form, r.pending ? LT('reviews.pending') : LT('reviews.posted'), true); form.reset(); load(); }).catch(function (err) { msg(form, err.message); }); };
|
||
load();
|
||
};
|
||
// ---------------- 블로그 (v1.4.0) ----------------
|
||
/** 글 본문: 관리자가 쓴 글자를 그대로 보이되(HTML 해석 안 함) 빈 줄은 문단, 줄바꿈은 <br>, http(s) 주소는 링크로 */
|
||
var blogBody = function (t) { return String(t || '').split(/\n{2,}/).map(function (p) { return '<p>' + esc(p).replace(/\n/g, '<br>').replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" rel="noopener" target="_blank">$1</a>') + '</p>'; }).join(''); };
|
||
R.blog = function (w) {
|
||
var list = w.querySelector('.wm-blog-list'), pager = w.querySelector('.wm-blog-pager'), view = w.querySelector('.wm-blog-view'), cats = w.querySelector('.wm-blog-cats'), form = w.querySelector('.wm-blog-search'); if (!list || !view) return;
|
||
var bt = w.querySelector('.wm-blog-title'); if (bt && !bt.textContent.trim()) bt.textContent = AT(w, 'data-title', 'blog.title');
|
||
var fixedCat = w.getAttribute('data-category') || ''; var st = { q: '', cat: fixedCat, tag: '', page: 1 }; var settings = null;
|
||
function showList(on) { list.hidden = !on; if (pager) pager.hidden = !on; if (cats) cats.hidden = !on; if (form) form.hidden = !on; view.hidden = on; }
|
||
function load() {
|
||
var qs = '?page=' + st.page + (st.q ? '&q=' + encodeURIComponent(st.q) : '') + (st.cat ? '&cat=' + encodeURIComponent(st.cat) : '') + (st.tag ? '&tag=' + encodeURIComponent(st.tag) : '') + (w.getAttribute('data-size') ? '&size=' + encodeURIComponent(w.getAttribute('data-size')) : '');
|
||
call(w, 'GET', '/blog/posts' + qs).then(function (r) {
|
||
settings = r.settings;
|
||
if (cats && !fixedCat) { var c = (settings.categories || []).map(function (x) { return '<button type="button" class="wm-btn wm-ghost' + (st.cat === x ? ' on' : '') + '" data-c="' + esc(x) + '">' + esc(LV(x)) + '</button>'; }).join(''); var tg = (r.tags || []).slice(0, 12).map(function (x) { return '<button type="button" class="wm-btn wm-ghost wm-blog-tag' + (st.tag === x.tag ? ' on' : '') + '" data-t="' + esc(x.tag) + '">#' + esc(x.tag) + '</button>'; }).join(''); cats.innerHTML = (c || tg) ? '<button type="button" class="wm-btn wm-ghost' + (!st.cat && !st.tag ? ' on' : '') + '" data-c="">' + LT('common.all') + '</button>' + c + tg : ''; cats.querySelectorAll('button').forEach(function (b) { b.onclick = function () { if (b.hasAttribute('data-t')) { st.tag = b.getAttribute('data-t'); st.cat = ''; } else { st.cat = b.getAttribute('data-c'); st.tag = ''; } st.page = 1; load(); }; }); }
|
||
if (!r.items.length) { list.innerHTML = '<p class="wm-empty">' + LT(st.q || st.tag ? 'blog.noResults' : 'blog.empty') + '</p>'; }
|
||
else list.innerHTML = r.items.map(function (p) { return '<article class="wm-blog-card" data-id="' + p.id + '">' + (p.cover ? '<img class="wm-blog-cover" src="' + esc(p.cover) + '" alt="" loading="lazy">' : '') + '<h3 class="wm-blog-card-title"><a href="#post-' + p.id + '">' + esc(p.title) + '</a></h3><div class="wm-meta">' + (p.category ? '<span class="wm-cat">' + esc(LV(p.category)) + '</span> · ' : '') + (p.author ? esc(p.author) + ' · ' : '') + fmtDate(p.createdAt).slice(0, 12) + ' · ' + esc(LT('blog.views', { n: p.views })) + ' · ' + esc(LT('blog.comments', { n: p.comments })) + '</div><p class="wm-excerpt">' + esc(p.summary) + '</p><a class="wm-link" href="#post-' + p.id + '">' + LT('blog.readMore') + '</a></article>'; }).join('');
|
||
var pages = Math.max(1, Math.ceil(r.total / r.size)); if (pager) { pager.innerHTML = pages > 1 ? Array.from({ length: pages }, function (_, i) { return '<button type="button" class="wm-btn wm-ghost' + (i + 1 === r.page ? ' on' : '') + '" data-p="' + (i + 1) + '">' + (i + 1) + '</button>'; }).join('') : ''; pager.querySelectorAll('button').forEach(function (b) { b.onclick = function () { st.page = Number(b.getAttribute('data-p')); load(); }; }); }
|
||
}).catch(function (err) { list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; });
|
||
}
|
||
function open(id) {
|
||
call(w, 'GET', '/blog/posts/' + encodeURIComponent(id)).then(function (r) {
|
||
var p = r.post; showList(false);
|
||
var cm = r.comments.map(function (c) { return '<div class="wm-comment"><b>' + esc(c.name) + '</b> <span class="wm-meta">' + fmtDate(c.createdAt) + '</span><div>' + esc(c.body).replace(/\n/g, '<br>') + '</div></div>'; }).join('');
|
||
var canWrite = r.canComment; var guest = !getToken(w.getAttribute('data-site'));
|
||
view.innerHTML = '<button type="button" class="wm-btn wm-ghost wm-blog-back">' + LT('blog.back') + '</button>' + (p.cover ? '<img class="wm-blog-cover" src="' + esc(p.cover) + '" alt="">' : '') + '<h3 class="wm-blog-view-title">' + esc(p.title) + '</h3><div class="wm-meta">' + (p.category ? '<span class="wm-cat">' + esc(LV(p.category)) + '</span> · ' : '') + (p.author ? esc(p.author) + ' · ' : '') + fmtDate(p.createdAt) + ' · ' + esc(LT('blog.views', { n: p.views })) + '</div>' + ((p.tags || []).length ? '<div class="wm-blog-tags">' + p.tags.map(function (t) { return '<span class="wm-blog-tag">#' + esc(t) + '</span>'; }).join(' ') + '</div>' : '') + '<div class="wm-blog-body">' + blogBody(p.body) + '</div>'
|
||
+ '<section class="wm-comments"><h4>' + esc(LT('blog.comments', { n: r.comments.length })) + '</h4>' + cm + (canWrite ? '<form class="wm-cform">' + (guest ? '<input name="name" placeholder="' + esc(LT('common.name')) + '" maxlength="40" required>' : '') + '<textarea name="body" rows="3" placeholder="' + esc(LT('blog.commentPh')) + '" maxlength="3000" required></textarea><button class="wm-btn">' + LT('blog.commentSubmit') + '</button><p class="wm-msg" aria-live="polite"></p></form>' : '<p class="wm-muted">' + LT('blog.commentsClosed') + '</p>') + '</section>';
|
||
view.querySelector('.wm-blog-back').onclick = function () { if (location.hash) history.pushState('', document.title, location.pathname + location.search); showList(true); load(); };
|
||
var cf = view.querySelector('.wm-cform'); if (cf) cf.onsubmit = function (e) { e.preventDefault(); var d = Object.fromEntries(new FormData(cf)); call(w, 'POST', '/blog/posts/' + encodeURIComponent(id) + '/comments', d).then(function (x) { if (x.pending) { msg(cf, x.message || LT('blog.commentPending'), true); cf.reset(); } else open(id); }).catch(function (err) { msg(cf, err.message); }); };
|
||
try { view.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
||
}).catch(function (err) { showList(true); list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; });
|
||
}
|
||
if (form) form.onsubmit = function (e) { e.preventDefault(); st.q = (form.q.value || '').trim(); st.page = 1; showList(true); load(); };
|
||
/** 주소의 #post-<ID> 로 글을 연다 (목록 링크·공유 주소·뒤로 가기) */
|
||
function route() { var m = /^#post-([a-f0-9]+)$/.exec(location.hash || ''); if (m) open(m[1]); else { showList(true); } }
|
||
window.addEventListener('hashchange', route); load(); route();
|
||
};
|
||
// ---------------- 자료실 (v1.4.0) ----------------
|
||
var fmtSize = function (n) { n = Number(n) || 0; if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'; if (n < 1073741824) return (n / 1048576).toFixed(1) + ' MB'; return (n / 1073741824).toFixed(2) + ' GB'; };
|
||
R.library = function (w) {
|
||
var list = w.querySelector('.wm-library-list'), cats = w.querySelector('.wm-library-cats'), form = w.querySelector('.wm-library-search'); if (!list) return;
|
||
var lt = w.querySelector('.wm-library-title'); if (lt && !lt.textContent.trim()) lt.textContent = AT(w, 'data-title', 'library.title');
|
||
var fixedCat = w.getAttribute('data-category') || ''; var st = { q: '', cat: fixedCat }; var site = w.getAttribute('data-site');
|
||
function load() {
|
||
call(w, 'GET', '/library/files?' + (st.q ? 'q=' + encodeURIComponent(st.q) + '&' : '') + (st.cat ? 'cat=' + encodeURIComponent(st.cat) : '')).then(function (r) {
|
||
var s = r.settings || {};
|
||
if (cats && !fixedCat) { cats.innerHTML = (s.categories || []).length ? '<button type="button" class="wm-btn wm-ghost' + (!st.cat ? ' on' : '') + '" data-c="">' + LT('common.all') + '</button>' + s.categories.map(function (x) { return '<button type="button" class="wm-btn wm-ghost' + (st.cat === x ? ' on' : '') + '" data-c="' + esc(x) + '">' + esc(LV(x)) + '</button>'; }).join('') : ''; cats.querySelectorAll('button').forEach(function (b) { b.onclick = function () { st.cat = b.getAttribute('data-c'); load(); }; }); }
|
||
if (!r.items.length) { list.innerHTML = '<p class="wm-empty">' + LT(st.q ? 'blog.noResults' : 'library.empty') + '</p>'; return; }
|
||
list.innerHTML = r.items.map(function (f) { return '<div class="wm-library-item" data-id="' + f.id + '"><div class="wm-library-info"><b class="wm-library-name">' + esc(f.title) + '</b><div class="wm-meta">' + esc(f.originalName) + ' · ' + fmtSize(f.size) + ' · ' + fmtDate(f.createdAt).slice(0, 12) + (f.downloads !== undefined ? ' · ' + esc(LT('library.downloads', { n: f.downloads })) : '') + (f.category ? ' · <span class="wm-cat">' + esc(LV(f.category)) + '</span>' : '') + '</div>' + (f.description ? '<p class="wm-library-desc">' + esc(f.description) + '</p>' : '') + '</div><button type="button" class="wm-btn wm-library-dl">' + LT('library.download') + '</button></div>'; }).join('');
|
||
list.querySelectorAll('.wm-library-dl').forEach(function (b) {
|
||
b.onclick = function () {
|
||
var id = b.closest('.wm-library-item').getAttribute('data-id'); var tok = getToken(site);
|
||
if (s.downloadRole === 'member' && !tok) { msg(w, LT('library.memberOnly')); return; }
|
||
// 링크로 내려받는다(큰 파일도 브라우저가 바로 저장). 회원 전용이면 토큰을 주소에 붙인다.
|
||
location.href = w.getAttribute('data-api') + '/library/files/' + encodeURIComponent(id) + '/download' + (tok ? '?t=' + encodeURIComponent(tok) : '');
|
||
};
|
||
});
|
||
}).catch(function (err) { list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; });
|
||
}
|
||
if (form) form.onsubmit = function (e) { e.preventDefault(); st.q = (form.q.value || '').trim(); load(); };
|
||
load();
|
||
};
|
||
// ---------------- 일정관리 캘린더 (v1.4.18) ----------------
|
||
// 월간·주간·일간 보기. 날짜 정보(음력·간지·절기)와 일정 발생은 서버(/calendar/range)가 계산해 주므로 여기서는 그리기만 한다.
|
||
// 설정 우선순위: data-view / data-lunar / data-ganji 속성(사이트가 정함) → 관리자 설정(API 응답 settings).
|
||
var calPad = function (n) { return (n < 10 ? '0' : '') + n; };
|
||
var calYmd = function (d) { return d.getFullYear() + '-' + calPad(d.getMonth() + 1) + '-' + calPad(d.getDate()); };
|
||
var calParse = function (s) { var p = String(s).split('-'); return new Date(+p[0], +p[1] - 1, +p[2]); };
|
||
var calAdd = function (d, n) { var x = new Date(d.getTime()); x.setDate(x.getDate() + n); return x; };
|
||
var calMin = function (t) { var m = /^(\d\d):(\d\d)$/.exec(t || ''); return m ? +m[1] * 60 + +m[2] : null; };
|
||
R.calendar = function (w) {
|
||
var body = w.querySelector('.wm-cal-body');
|
||
// v1.5.7: 표준 마크업(.wm-cal-title/.wm-cal-tools/.wm-cal-body)이 없는 위젯(AI 가 임의로 만든 구조·별칭 id)은 안내문을 비우고 골격을 만들어 그린다
|
||
if (!body) { var keepTitle = w.getAttribute('data-title') || (w.querySelector('h1,h2,h3') || {}).textContent || ''; w.innerHTML = ''; w.classList.add('wm-cal'); var t0 = document.createElement('h2'); t0.className = 'wm-cal-title'; t0.textContent = keepTitle.trim(); w.appendChild(t0); var tl = document.createElement('div'); tl.className = 'wm-cal-tools'; w.appendChild(tl); body = document.createElement('div'); body.className = 'wm-cal-body'; w.appendChild(body); }
|
||
var tools = w.querySelector('.wm-cal-tools') || (function () { var d = document.createElement('div'); d.className = 'wm-cal-tools'; w.insertBefore(d, body); return d; })();
|
||
var tt = w.querySelector('.wm-cal-title'); if (tt && !tt.textContent.trim()) tt.textContent = AT(w, 'data-title', 'calendar.title');
|
||
var fixedCat = w.getAttribute('data-category') || ''; var attrView = w.getAttribute('data-view') || '';
|
||
var settings = null; var view = attrView; var cur = new Date(); cur.setHours(0, 0, 0, 0); var todayStr = calYmd(cur);
|
||
var HOUR_PX = 44; var isKo = /^ko/i.test(PAGE_LANG);
|
||
var flag = function (attr, key) { var a = w.getAttribute(attr); return a === '' || a == null ? !!settings[key] : a === '1' || a === 'true'; };
|
||
var weekStart = function () { return settings && settings.weekStart === 1 ? 1 : 0; };
|
||
/** 보기 종류에 따른 조회 기간 [시작, 끝] (월간은 6주 격자) */
|
||
function rangeOf() {
|
||
if (view === 'day') return [cur, cur];
|
||
if (view === 'week') { var s = calAdd(cur, -((cur.getDay() - weekStart() + 7) % 7)); return [s, calAdd(s, 6)]; }
|
||
var first = new Date(cur.getFullYear(), cur.getMonth(), 1); var gs = calAdd(first, -((first.getDay() - weekStart() + 7) % 7)); return [gs, calAdd(gs, 41)];
|
||
}
|
||
var caption = function (s, e) {
|
||
try {
|
||
if (view === 'month') return new Date(cur.getFullYear(), cur.getMonth(), 1).toLocaleDateString(PAGE_LANG, { year: 'numeric', month: 'long' });
|
||
if (view === 'week') return s.toLocaleDateString(PAGE_LANG, { year: 'numeric', month: 'short', day: 'numeric' }) + ' – ' + e.toLocaleDateString(PAGE_LANG, { month: 'short', day: 'numeric' });
|
||
return cur.toLocaleDateString(PAGE_LANG, { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' });
|
||
} catch (e2) { return calYmd(cur); }
|
||
};
|
||
var wdName = function (wd) { try { return new Date(2023, 0, 1 + wd).toLocaleDateString(PAGE_LANG, { weekday: 'short' }); } catch (e) { return ['일', '월', '화', '수', '목', '금', '토'][wd]; } };
|
||
var gText = function (g) { return g ? (isKo ? g.hangul : g.hanja) : ''; };
|
||
/** 날짜 칸 아래 줄: 음력(음 9.4) · 일진(신유) — 설정에 따라 */
|
||
var subLine = function (info, full) {
|
||
if (!info) return ''; var parts = [];
|
||
if (flag('data-lunar', 'showLunar') && info.lunar) parts.push(esc(LT('calendar.lunarPrefix') + ' ' + (info.lunar.leap ? LT('calendar.leap') : '') + info.lunar.month + '.' + info.lunar.day));
|
||
if (flag('data-ganji', 'showGanji') && info.ganji) parts.push('<span class="wm-cal-gj" title="' + esc(isKo ? info.ganji.text : info.ganji.textHanja) + '">' + esc(full ? (isKo ? info.ganji.text : info.ganji.textHanja) : gText(info.ganji.day)) + '</span>');
|
||
return parts.join(' · ');
|
||
};
|
||
var termBadge = function (info) { return settings.showTerms !== false && info && info.term ? '<span class="wm-cal-term">' + esc(isKo ? info.term : (info.termHanja || info.term)) + '</span>' : ''; };
|
||
var evLabel = function (o) { return (o.allDay ? '' : '<b>' + esc(o.time) + '</b> ') + esc(o.title); };
|
||
var evClass = function (o) { return 'wm-cal-ev' + (o.color ? ' wm-cal-c-' + o.color : ''); };
|
||
function toolbar(s, e) {
|
||
tools.innerHTML = '<button type="button" class="wm-btn wm-ghost wm-cal-prev" aria-label="' + esc(LT('calendar.prev')) + '">‹</button><button type="button" class="wm-btn wm-ghost wm-cal-today">' + esc(LT('calendar.today')) + '</button><button type="button" class="wm-btn wm-ghost wm-cal-next" aria-label="' + esc(LT('calendar.next')) + '">›</button><span class="wm-cal-caption">' + esc(caption(s, e)) + '</span>'
|
||
+ ['month', 'week', 'day'].map(function (v) { return '<button type="button" class="wm-btn wm-ghost wm-cal-view' + (view === v ? ' on' : '') + '" data-v="' + v + '">' + esc(LT('calendar.' + v)) + '</button>'; }).join('');
|
||
var step = function (dir) { if (view === 'month') cur = new Date(cur.getFullYear(), cur.getMonth() + dir, 1); else cur = calAdd(cur, dir * (view === 'week' ? 7 : 1)); load(); };
|
||
tools.querySelector('.wm-cal-prev').onclick = function () { step(-1); }; tools.querySelector('.wm-cal-next').onclick = function () { step(1); };
|
||
tools.querySelector('.wm-cal-today').onclick = function () { cur = calParse(todayStr); load(); };
|
||
tools.querySelectorAll('.wm-cal-view').forEach(function (b) { b.onclick = function () { view = b.getAttribute('data-v'); load(); }; });
|
||
}
|
||
/** 일정 상세 팝업 */
|
||
function popup(o) {
|
||
var d = document.createElement('div'); d.className = 'wm-cal-pop';
|
||
var when = calParse(o.start).toLocaleDateString(PAGE_LANG, { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' }) + (o.days > 1 ? ' ~ ' + calParse(o.end).toLocaleDateString(PAGE_LANG, { month: 'long', day: 'numeric', weekday: 'short' }) : '') + (o.allDay ? '' : ' ' + o.time + (o.endTime ? ' – ' + o.endTime : ''));
|
||
var unit = o.unit === 'time' ? LT('calendar.unitTime') : o.unit === 'week' ? LT('calendar.unitWeek', { n: Math.round(o.days / 7) }) : LT('calendar.unitDay', { n: o.days });
|
||
d.innerHTML = '<div class="wm-cal-card" role="dialog"><h3>' + esc(o.title) + '</h3><div class="wm-meta">' + esc(when) + '</div><div class="wm-meta">' + esc(unit) + (o.category ? ' · ' + esc(LV(o.category)) : '') + (o.cal === 'lunar' && o.lunarText ? ' · ' + esc(LT('calendar.lunarBased')) + ' (' + esc(LT('calendar.lunarPrefix') + ' ' + o.lunarText) + ')' : '') + '</div>' + (o.location ? '<div class="wm-meta">' + esc(LT('calendar.location')) + ': ' + esc(o.location) + '</div>' : '') + (o.body ? '<div class="wm-body">' + nl2br(o.body) + '</div>' : '') + '<p><button type="button" class="wm-btn wm-cal-close">' + esc(LT('calendar.close')) + '</button></p></div>';
|
||
d.onclick = function (e) { if (e.target === d || e.target.classList.contains('wm-cal-close')) d.remove(); }; document.body.appendChild(d);
|
||
}
|
||
function bindEvents(root, byId) { root.querySelectorAll('.wm-cal-ev[data-id]').forEach(function (b) { b.onclick = function (e) { e.stopPropagation(); var o = byId[b.getAttribute('data-id')]; if (o) popup(o); }; }); }
|
||
/** 시간 단위 일정을 겹치지 않게 열 나누기: 시작 순으로 보며 끝나지 않은 일정과 겹치면 다음 열 */
|
||
function layout(items) {
|
||
var cols = []; items.forEach(function (o) { o._s = calMin(o.time) || 0; o._e = Math.max(o._s + 30, calMin(o.endTime) || (o._s + 60)); var c = 0; while (cols[c] && cols[c] > o._s) c++; cols[c] = o._e; o._c = c; });
|
||
var n = cols.length || 1; items.forEach(function (o) { o._n = n; });
|
||
}
|
||
function renderMonth(r, s) {
|
||
var info = {}; r.days.forEach(function (d) { info[d.date] = d; }); var byDay = {}; var byId = {};
|
||
r.events.forEach(function (o) { byId[o.id] = o; var d = calParse(o.start); for (var i = 0; i < o.days; i++) { var k = calYmd(calAdd(d, i)); (byDay[k] = byDay[k] || []).push(o); } });
|
||
var html = '<div class="wm-cal-grid wm-cal-month">'; for (var i = 0; i < 7; i++) html += '<div class="wm-cal-wd">' + esc(wdName((weekStart() + i) % 7)) + '</div>';
|
||
for (var j = 0; j < 42; j++) {
|
||
var d = calAdd(s, j); var k = calYmd(d); var evs = byDay[k] || []; var wd = d.getDay();
|
||
html += '<div class="wm-cal-cell' + (d.getMonth() !== cur.getMonth() ? ' other' : '') + (k === todayStr ? ' today' : '') + (wd === 0 ? ' wm-cal-sun' : wd === 6 ? ' wm-cal-sat' : '') + '" data-date="' + k + '"><div class="wm-cal-date"><span class="wm-cal-num">' + d.getDate() + '</span>' + termBadge(info[k]) + '</div><div class="wm-cal-sub">' + subLine(info[k]) + '</div><div class="wm-cal-evs">'
|
||
+ evs.slice(0, 3).map(function (o) { return '<button type="button" class="' + evClass(o) + '" data-id="' + esc(o.id) + '">' + evLabel(o) + '</button>'; }).join('') + (evs.length > 3 ? '<button type="button" class="wm-cal-more">' + esc(LT('calendar.more', { n: evs.length - 3 })) + '</button>' : '') + '</div></div>';
|
||
}
|
||
body.innerHTML = html + '</div>' + (r.events.length ? '' : '<p class="wm-cal-empty">' + esc(LT('calendar.empty')) + '</p>');
|
||
bindEvents(body, byId);
|
||
body.querySelectorAll('.wm-cal-cell').forEach(function (c) { c.onclick = function () { cur = calParse(c.getAttribute('data-date')); view = 'day'; load(); }; });
|
||
}
|
||
function renderWeek(r, s, e) {
|
||
var days = []; for (var d = s; d <= e; d = calAdd(d, 1)) days.push(d); var n = days.length; var info = {}; r.days.forEach(function (x) { info[x.date] = x; }); var byId = {}; r.events.forEach(function (o) { byId[o.id] = o; });
|
||
var allOf = function (k) { return r.events.filter(function (o) { return o.allDay && o.start <= k && o.end >= k; }); };
|
||
var timedOf = function (k) { return r.events.filter(function (o) { return !o.allDay && o.start === k; }); };
|
||
var h0 = settings.dayStart || 0, h1 = settings.dayEnd || 24;
|
||
var html = '<div class="wm-cal-week' + (n === 1 ? ' one' : '') + '" style="--wm-cal-cols:' + n + '">';
|
||
if (n === 1) { var di = info[calYmd(cur)]; if (di) html += '<div class="wm-cal-info">' + subLine(di, true) + (di.term ? ' · ' + esc(LT('calendar.termLabel')) + ': ' + esc(isKo ? di.term : di.termHanja || di.term) : '') + '</div>'; }
|
||
html += '<div class="wm-cal-row wm-cal-head"><div></div>' + days.map(function (d) { var k = calYmd(d); var wd = d.getDay(); return '<div class="' + (k === todayStr ? 'today' : '') + (wd === 0 ? ' wm-cal-sun' : wd === 6 ? ' wm-cal-sat' : '') + '"><span class="wm-cal-num">' + esc(wdName(wd)) + ' ' + d.getDate() + '</span> ' + termBadge(info[k]) + (n > 1 ? '<div class="wm-cal-sub">' + subLine(info[k]) + '</div>' : '') + '</div>'; }).join('') + '</div>';
|
||
html += '<div class="wm-cal-row wm-cal-allday"><div class="wm-cal-sub">' + esc(LT('calendar.allDay')) + '</div>' + days.map(function (d) { return '<div>' + allOf(calYmd(d)).map(function (o) { return '<button type="button" class="' + evClass(o) + '" data-id="' + esc(o.id) + '">' + evLabel(o) + '</button>'; }).join('') + '</div>'; }).join('') + '</div>';
|
||
html += '<div class="wm-cal-row wm-cal-timegrid"><div class="wm-cal-hours">'; for (var h = h0; h < h1; h++) html += '<div class="wm-cal-hour">' + calPad(h) + ':00</div>'; html += '</div>';
|
||
days.forEach(function (d) {
|
||
var k = calYmd(d); var items = timedOf(k); layout(items); html += '<div class="wm-cal-col" data-date="' + k + '">'; for (var h = h0; h < h1; h++) html += '<div class="wm-cal-hour"></div>';
|
||
items.forEach(function (o) { var top = Math.max(0, (o._s - h0 * 60) / 60 * HOUR_PX); var bottom = Math.min((h1 - h0) * HOUR_PX, (o._e - h0 * 60) / 60 * HOUR_PX); var ht = Math.max(22, bottom - top); var wpct = 100 / o._n; html += '<button type="button" class="' + evClass(o) + ' wm-cal-tev" data-id="' + esc(o.id) + '" style="top:' + top + 'px;height:' + ht + 'px;left:calc(' + (o._c * wpct) + '% + 2px);width:calc(' + wpct + '% - 4px)">' + evLabel(o) + '</button>'; });
|
||
html += '</div>';
|
||
});
|
||
html += '</div></div>';
|
||
if (n === 1) { var list = r.events.filter(function (o) { return o.start <= calYmd(cur) && o.end >= calYmd(cur); }); html += '<div class="wm-cal-list">' + (list.length ? list.map(function (o) { return '<button type="button" class="' + evClass(o) + '" data-id="' + esc(o.id) + '">' + evLabel(o) + (o.location ? ' <small>' + esc(o.location) + '</small>' : '') + '</button>'; }).join('') : '<p class="wm-cal-empty">' + esc(LT('calendar.empty')) + '</p>') + '</div>'; }
|
||
body.innerHTML = html; bindEvents(body, byId);
|
||
}
|
||
function load() {
|
||
if (!view) view = (settings && settings.defaultView) || 'month';
|
||
var se = rangeOf(); toolbar(se[0], se[1]); body.innerHTML = '<p class="wm-cal-empty">' + esc(LT('common.loading')) + '</p>';
|
||
call(w, 'GET', '/calendar/range?from=' + calYmd(se[0]) + '&to=' + calYmd(se[1]) + (fixedCat ? '&cat=' + encodeURIComponent(fixedCat) : '')).then(function (r) {
|
||
settings = r.settings || settings || {}; if (view === 'month') renderMonth(r, se[0]); else renderWeek(r, se[0], se[1]);
|
||
}).catch(function (err) { body.innerHTML = '<p class="wm-msg err">' + esc(err.message || LT('calendar.loadFailed')) + '</p>'; });
|
||
}
|
||
// 설정을 먼저 받아 기본 보기·주 시작 요일을 알고 나서 그린다
|
||
call(w, 'GET', '/calendar/settings').then(function (s) { settings = s || {}; load(); }).catch(function () { settings = {}; load(); });
|
||
};
|
||
// ---------------- FAQ ----------------
|
||
R.faq = function (w) {
|
||
var list = w.querySelector('.wm-faq-list'); if (!list) return; var catsBox = w.querySelector('.wm-faq-cats') || document.createElement('div'); var ft = w.querySelector('.wm-faq-title'); if (ft && !ft.textContent.trim()) ft.textContent = AT(w, 'data-title', 'faq.title'); var cat = w.getAttribute('data-category') || '';
|
||
call(w, 'GET', '/faq/list').then(function (r) { var cats = []; r.items.forEach(function (i) { if (i.category && cats.indexOf(i.category) < 0) cats.push(i.category); }); function draw() { var items = cat ? r.items.filter(function (i) { return i.category === cat; }) : r.items; list.innerHTML = items.length ? items.map(function (i) { return '<details><summary>' + esc(i.q) + '</summary><div class="wm-faq-a">' + nl2br(i.a) + '</div></details>'; }).join('') : '<p class="wm-empty">' + LT('faq.empty') + '</p>'; } if (!w.getAttribute('data-category') && cats.length) { catsBox.className = 'wm-faq-cats wm-cats'; catsBox.innerHTML = '<button type="button" class="wm-btn wm-ghost on" data-c="">' + LT('common.all') + '</button>' + cats.map(function (c) { return '<button type="button" class="wm-btn wm-ghost" data-c="' + esc(c) + '">' + esc(LV(c)) + '</button>'; }).join(''); catsBox.querySelectorAll('button').forEach(function (b) { b.onclick = function () { cat = b.getAttribute('data-c'); catsBox.querySelectorAll('button').forEach(function (x) { x.classList.toggle('on', x === b); }); draw(); }; }); } draw(); }).catch(function (err) { list.innerHTML = '<p class="wm-msg err">' + esc(err.message) + '</p>'; });
|
||
};
|
||
|
||
/**
|
||
* v1.5.7: 모듈 id 별칭. AI 가 표준 코드 없이 비슷한 이름(schedule·events·일정)으로 위젯을 만든 페이지도 일정관리 런타임이 그려 준다
|
||
* (2026-10-14 운영 사이트에서 data-wm-module="schedule" 이 빈 안내문만 보이던 문제). 표준 id 가 우선이다.
|
||
*/
|
||
var ALIAS = { schedule: 'calendar', schedules: 'calendar', events: 'calendar', event: 'calendar', agenda: 'calendar', '일정': 'calendar', '일정관리': 'calendar', '캘린더': 'calendar', bbs: 'board', forum: 'board', photos: 'gallery', photo: 'gallery', contact: 'inquiry', 'contact-form': 'inquiry', review: 'reviews', faqs: 'faq', news: 'blog', posts: 'blog', downloads: 'library', files: 'library', reservation: 'booking', member: 'members', login: 'members' };
|
||
function boot() { document.querySelectorAll('[data-wm-module]').forEach(function (w) { if (w.__wm) return; w.__wm = 1; var id = String(w.getAttribute('data-wm-module') || '').trim().toLowerCase(); var fn = R[id] || R[ALIAS[id]]; if (fn) { try { fn(w); } catch (e) { console.error('wm module error', e); } } }); }
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
|
||
window.wmModules = { boot: boot, call: call };
|
||
})();
|