(새 문서: // ========== Developer Tools Panel ========== (function () { var OWNER_USERNAME = 'Nxdsxn'; var START_PAGES = [ 'MediaWiki:Common.css', 'MediaWiki:Common.js', 'MediaWiki:Lang.js' ]; var JSZIP_URL = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'; function isWikiOwner() { return mw.config.get('wgUserName') === OWNER_USERNAME; } function setDevToolsStatus(text, type) { var status = document.getElementById('dev-tools-status'); if (!status) return; status.c...) |
편집 요약 없음 |
||
| (같은 사용자의 중간 판 7개는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
(function (mw, $) { | |||
'use strict'; | |||
var OWNER = 'Nxdsxn'; | |||
var | var JSZIP_URL = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'; | ||
var LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json'; | |||
var LANGUAGE_ORDER = ['ko', 'en', 'zh', 'ja', 'ru', 'es']; | |||
var LANGUAGE_LABELS = { | |||
ko: '한국어', | |||
en: 'English', | |||
zh: '中文', | |||
ja: '日本語', | |||
ru: 'Русский', | |||
es: 'Español' | |||
}; | |||
var LANGUAGE_STATUS_VALUES = { | |||
available: true, | |||
wip: true, | |||
unavailable: true | |||
}; | |||
var | function isOwner() { | ||
' | var name = mw.config.get('wgUserName') || ''; | ||
var groups = mw.config.get('wgUserGroups') || []; | |||
' | return name === OWNER || groups.indexOf('sysop') !== -1 || groups.indexOf('interface-admin') !== -1; | ||
} | |||
if (!isOwner()) return; | |||
if (window.DevToolsPanelInitialized) return; | |||
window.DevToolsPanelInitialized = true; | |||
var sourceFiles = [ | |||
{ title: 'MediaWiki:Common.css', out: 'css/Common.css', ctype: 'text/css' }, | |||
} | { title: 'MediaWiki:Theme.css', out: 'css/Theme.css', ctype: 'text/css' }, | ||
{ title: 'MediaWiki:Layout.css', out: 'css/Layout.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:MainPage.css', out: 'css/MainPage.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Components.css', out: 'css/Components.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Template.css', out: 'css/Template.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Template.Infobox.css', out: 'css/Template.Infobox.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Icons.css', out: 'css/Icons.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Editor.css', out: 'css/Editor.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Dialogs.css', out: 'css/Dialogs.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:DevTools.css', out: 'css/DevTools.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:AnecdoteViewer.css', out: 'css/AnecdoteViewer.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Test.css', out: 'css/Test.css', ctype: 'text/css' }, | |||
{ title: 'MediaWiki:Common.js', out: 'js/Common.js', ctype: 'text/javascript' }, | |||
{ title: 'MediaWiki:Lang.js', out: 'js/Lang.js', ctype: 'text/javascript' }, | |||
{ title: 'MediaWiki:DevTools.js', out: 'js/DevTools.js', ctype: 'text/javascript' }, | |||
{ title: 'MediaWiki:CategoryNav.js', out: 'js/CategoryNav.js', ctype: 'text/javascript' }, | |||
{ title: 'MediaWiki:AnecdoteViewer.js', out: 'js/AnecdoteViewer.js', ctype: 'text/javascript' } | |||
]; | |||
function | function escapeHtml(value) { | ||
return String(value == null ? '' : value) | |||
.replace(/&/g, '&') | |||
.replace(/</g, '<') | |||
.replace(/>/g, '>') | |||
.replace(/"/g, '"') | |||
.replace(/'/g, '''); | |||
} | |||
function normalizeDocumentToolPageName(value) { | |||
return String(value || '') | |||
} | .split('?')[0] | ||
.replace(/^\/index\.php\//, '') | |||
.replace(/_/g, ' ') | |||
.trim(); | |||
} | |||
function getDocumentToolPageName() { | |||
var pageName = mw.config.get('wgPageName') || ''; | |||
if (pageName) { | |||
} | return String(pageName).replace(/ /g, '_'); | ||
} | |||
pageName = normalizeDocumentToolPageName(window.location.pathname || ''); | |||
return | return pageName ? pageName.replace(/ /g, '_') : '대문'; | ||
} | |||
} | |||
var | function getDocumentToolDisplayTitle() { | ||
var pageName = getDocumentToolPageName(); | |||
return normalizeDocumentToolPageName(pageName) || '현재 문서'; | |||
} | |||
function getDocumentToolUrl(pageName, params) { | |||
pageName = pageName || getDocumentToolPageName(); | |||
params = params || {}; | |||
} | |||
if (mw.util && typeof mw.util.getUrl === 'function') { | |||
return mw.util.getUrl(pageName, params); | |||
} | } | ||
return; | var query = $.param(params); | ||
} | return '/index.php/' + encodeURI(String(pageName || '').replace(/ /g, '_')) + (query ? '?' + query : ''); | ||
} | |||
var | function getDocumentToolTalkPageName() { | ||
var namespaceNumber = Number(mw.config.get('wgNamespaceNumber')); | |||
var formattedNamespaces = mw.config.get('wgFormattedNamespaces') || {}; | |||
var title = String(mw.config.get('wgTitle') || '').trim(); | |||
var talkNamespace; | |||
var prefix; | |||
if (!Number.isFinite(namespaceNumber) || namespaceNumber < 0 || namespaceNumber % 2 !== 0) { | |||
return ''; | |||
} | } | ||
talkNamespace = namespaceNumber + 1; | |||
prefix = formattedNamespaces[String(talkNamespace)] || formattedNamespaces[talkNamespace] || ''; | |||
if (!prefix || !title) { | |||
return ''; | |||
} | } | ||
return prefix + ':' + title.replace(/ /g, '_'); | |||
return | } | ||
.replace(/ | |||
} | |||
function | function renderDocumentToolLink(label, href, disabled) { | ||
if (disabled || !href) { | |||
return '<span class="dev-tools-button dev-tools-link-button dev-tools-button-disabled" aria-disabled="true">' + escapeHtml(label) + '</span>'; | |||
} | |||
return '<a class="dev-tools-button dev-tools-link-button" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>'; | |||
} | |||
} | |||
function buildDocumentToolsHtml() { | |||
var namespaceNumber = Number(mw.config.get('wgNamespaceNumber')); | |||
var pageName = getDocumentToolPageName(); | |||
var isSpecial = Number.isFinite(namespaceNumber) && namespaceNumber < 0; | |||
var talkPageName = getDocumentToolTalkPageName(); | |||
return ' | return '' + | ||
} | '<div class="dev-tools-section dev-tools-document-section">' + | ||
'<div class="dev-tools-section-title">DOCUMENT TOOLS</div>' + | |||
'<div class="dev-tools-current-page" id="dev-tools-current-page">PAGE: ' + escapeHtml(getDocumentToolDisplayTitle()) + '</div>' + | |||
'<div class="dev-tools-grid dev-tools-doc-grid" id="dev-tools-doc-grid">' + | |||
renderDocumentToolLink('문서', getDocumentToolUrl(pageName), isSpecial) + | |||
renderDocumentToolLink('편집', getDocumentToolUrl(pageName, { action: 'edit' }), isSpecial) + | |||
renderDocumentToolLink('원본', getDocumentToolUrl(pageName, { action: 'raw' }), isSpecial) + | |||
renderDocumentToolLink('역사', getDocumentToolUrl(pageName, { action: 'history' }), isSpecial) + | |||
renderDocumentToolLink('토론', talkPageName ? getDocumentToolUrl(talkPageName) : '', !talkPageName) + | |||
renderDocumentToolLink('새로고침', getDocumentToolUrl(pageName, { action: 'purge' }), isSpecial) + | |||
'</div>' + | |||
'<div class="dev-tools-note">상단 문서 조작은 일반 화면에서 숨기고 이 패널로 모읍니다.</div>' + | |||
'</div>'; | |||
} | |||
function | function refreshDocumentTools() { | ||
var | var $section = $('.dev-tools-document-section'); | ||
if (!$section.length) return; | |||
$section.replaceWith(buildDocumentToolsHtml()); | |||
} | |||
} | |||
function buildPanel() { | |||
if ($('#dev-tools-panel').length) return; | |||
var collapsed = localStorage.getItem('dev-tools-collapsed') === 'true'; | |||
var html = '' + | |||
'<div id="dev-tools-panel" class="' + (collapsed ? 'dev-tools-collapsed' : '') + '">' + | |||
'<div id="dev-tools-header">' + | |||
'<div id="dev-tools-title"><span class="dev-tools-led"></span><span>ADMIN TOOLS</span></div>' + | |||
'<button type="button" id="dev-tools-toggle" aria-label="도구 접기">' + (collapsed ? '+' : '−') + '</button>' + | |||
'</div>' + | |||
'<div id="dev-tools-body">' + | |||
buildDocumentToolsHtml() + | |||
'<div class="dev-tools-section">' + | |||
'<div class="dev-tools-section-title">SOURCE PACKAGE</div>' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-export">CSS / JS ZIP 다운로드</button>' + | |||
'<div class="dev-tools-note">MediaWiki namespace의 CSS·JS 원본을 ZIP으로 묶습니다.</div>' + | |||
'</div>' + | |||
'<div class="dev-tools-section dev-tools-progress-section">' + | |||
'<div class="dev-tools-section-title">PROGRESS TEST</div>' + | |||
'<div class="dev-tools-progress-readout" id="dev-tools-progress-readout">SYNC</div>' + | |||
'<label class="dev-tools-label" for="dev-tools-xp-amount">XP AMOUNT</label>' + | |||
'<input id="dev-tools-xp-amount" class="dev-tools-input" type="number" min="1" step="1" value="10">' + | |||
'<label class="dev-tools-label" for="dev-tools-xp-reason">REASON</label>' + | |||
'<input id="dev-tools-xp-reason" class="dev-tools-input" type="text" value="관리자 테스트">' + | |||
'<div class="dev-tools-grid">' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-xp-add">XP 지급</button>' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-progress-refresh">동기화</button>' + | |||
'</div>' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-daily-reset">오늘 XP 보너스 리셋</button>' + | |||
'<button type="button" class="dev-tools-button dev-tools-danger" id="dev-tools-level-reset">레벨 리셋</button>' + | |||
'</div>' + | |||
'<div class="dev-tools-section dev-tools-language-section">' + | |||
'<div class="dev-tools-section-title">LANGUAGE STATUS</div>' + | |||
'<div class="dev-tools-language-page" id="dev-tools-language-page">PAGE: SYNC</div>' + | |||
'<div class="dev-tools-language-list" id="dev-tools-language-list">SYNC</div>' + | |||
'<div class="dev-tools-grid">' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-language-save">저장</button>' + | |||
'<button type="button" class="dev-tools-button" id="dev-tools-language-refresh">불러오기</button>' + | |||
'</div>' + | |||
'<div class="dev-tools-note">현재 문서의 언어 다이얼 상태를 JSON으로 저장합니다.</div>' + | |||
'</div>' + | |||
'<div id="dev-tools-status">READY</div>' + | |||
'</div>' + | |||
'</div>'; | |||
$('body').append(html); | |||
} | |||
} | |||
function | function setStatus(text, type) { | ||
var | var $status = $('#dev-tools-status'); | ||
$status.removeClass('dev-tools-status-ok dev-tools-status-error'); | |||
if (type === 'ok') $status.addClass('dev-tools-status-ok'); | |||
if (type === 'error') $status.addClass('dev-tools-status-error'); | |||
$status.text(text || ''); | |||
} | |||
function setButtonsDisabled(disabled) { | |||
$('#dev-tools-panel button, #dev-tools-panel input, #dev-tools-panel select').prop('disabled', !!disabled); | |||
$('#dev-tools-toggle').prop('disabled', false); | |||
} | |||
function loadScript(url) { | |||
return new Promise(function (resolve, reject) { | |||
if (window.JSZip) { | |||
resolve(); | |||
return; | |||
} | |||
var script = document.createElement('script'); | |||
script.src = url; | |||
script.onload = resolve; | |||
script.onerror = reject; | |||
document.head.appendChild(script); | |||
}); | |||
} | |||
function rawUrl(file) { | |||
title | return mw.util.getUrl(file.title, { | ||
action: 'raw', | |||
ctype: file.ctype | |||
} | }); | ||
} | |||
} | |||
title | function fetchRaw(file) { | ||
return fetch(rawUrl(file), { credentials: 'same-origin' }).then(function (res) { | |||
if (!res.ok) throw new Error(file.title + ' HTTP ' + res.status); | |||
return res.text(); | |||
}); | |||
} | |||
function downloadBlob(blob, filename) { | |||
var url = URL.createObjectURL(blob); | |||
} | var a = document.createElement('a'); | ||
a.href = url; | |||
a.download = filename; | |||
document.body.appendChild(a); | |||
a.click(); | |||
a.remove(); | |||
setTimeout(function () { | |||
URL.revokeObjectURL(url); | |||
}, 1000); | |||
} | |||
function exportSources() { | |||
setButtonsDisabled(true); | |||
setStatus('ZIP 생성 중...'); | |||
loadScript(JSZIP_URL) | |||
.then(function () { | |||
var zip = new JSZip(); | |||
var meta = []; | |||
return Promise.all(sourceFiles.map(function (file) { | |||
return fetchRaw(file).then(function (text) { | |||
} | zip.file(file.out, text); | ||
meta.push(file.out); | |||
}).catch(function (err) { | |||
zip.file(file.out + '.ERROR.txt', String(err && err.message ? err.message : err)); | |||
}); | |||
})).then(function () { | |||
zip.file('README.txt', 'CLBI Wiki CSS/JS export\nGenerated: ' + new Date().toISOString() + '\n\nFiles:\n' + meta.sort().join('\n') + '\n'); | |||
return zip.generateAsync({ type: 'blob' }); | |||
}); | |||
}) | |||
.then(function (blob) { | |||
var stamp = new Date().toISOString().replace(/[:.]/g, '-'); | |||
downloadBlob(blob, 'clbiwiki-source-' + stamp + '.zip'); | |||
setStatus('ZIP 다운로드 완료', 'ok'); | |||
}) | |||
.catch(function (err) { | |||
setStatus('ZIP 실패: ' + (err && err.message ? err.message : err), 'error'); | |||
}) | |||
.finally(function () { | |||
setButtonsDisabled(false); | |||
}); | |||
} | |||
function withApi(done, fail) { | |||
} | mw.loader.using(['mediawiki.api']).then(function () { | ||
done(new mw.Api()); | |||
}, function () { | |||
if (typeof fail === 'function') fail(); | |||
}); | |||
} | |||
function updateReadout(summary) { | |||
var | var $box = $('#dev-tools-progress-readout'); | ||
if (!summary) { | |||
$box.text('NO DATA'); | |||
return; | |||
} | |||
$box.html( | |||
'<span>' + (summary.isMaxLevel ? 'MAX ' : 'LVL ') + escapeHtml(summary.level) + '</span>' + | |||
'<span>' + escapeHtml(summary.totalXp) + ' XP</span>' + | |||
'<span>TODAY ' + escapeHtml(summary.dailyXp) + ' XP</span>' + | |||
'<span>READ ' + escapeHtml(summary.dailyReadCount) + '</span>' + | |||
'<span>DISC ' + escapeHtml(summary.discoveryCount) + '</span>' | |||
); | |||
} | |||
if ( | function refreshProgressSummary() { | ||
withApi(function (api) { | |||
} | api.get({ | ||
action: 'progress_summary', | |||
format: 'json', | |||
formatversion: 2 | |||
}).then(function (data) { | |||
var payload = data && data.progress_summary; | |||
if (payload && payload.available && payload.summary) { | |||
updateReadout(payload.summary); | |||
if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.requestSummary === 'function') { | |||
window.ProgressSystemWebUi.requestSummary(true); | |||
} | |||
setStatus('진행도 동기화 완료', 'ok'); | |||
} else { | |||
updateReadout(null); | |||
setStatus('진행도 없음', 'error'); | |||
} | |||
}).catch(function () { | |||
setStatus('동기화 실패', 'error'); | |||
}); | |||
}, function () { | |||
setStatus('mediawiki.api 로드 실패', 'error'); | |||
}); | |||
} | |||
function runProgressAdmin(operation, extra) { | |||
extra = extra || {}; | |||
setButtonsDisabled(true); | |||
setStatus('요청 처리 중...'); | |||
withApi(function (api) { | |||
var | var payload = $.extend({ | ||
action: 'progress_admin', | |||
format: 'json', | |||
formatversion: 2, | |||
errorformat: 'plaintext', | |||
operation: operation | |||
}, extra); | |||
api.postWithToken('csrf', payload).then(function (data) { | |||
var result = data && data.progress_admin; | |||
if (!result || !result.ok) { | |||
setStatus('실패: ' + (result && result.reason ? result.reason : 'unknown'), 'error'); | |||
} | return; | ||
} | |||
if ( | if (result.summary) { | ||
updateReadout(result.summary); | |||
if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.applySummary === 'function') { | |||
window.ProgressSystemWebUi.applySummary(result.summary, { | |||
animateGain: operation === 'add_xp' | |||
}); | |||
}); | } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.requestSummary === 'function') { | ||
} | window.ProgressSystemWebUi.requestSummary(); | ||
} | |||
} | |||
} | |||
} | |||
setStatus('완료: ' + operation, 'ok'); | |||
}).catch(function (code) { | |||
setStatus('API 실패: ' + code, 'error'); | |||
}; | }).always(function () { | ||
} | setButtonsDisabled(false); | ||
}); | |||
}, function () { | |||
setButtonsDisabled(false); | |||
setStatus('mediawiki.api 로드 실패', 'error'); | |||
}); | |||
} | |||
return ' | function normalizePageName(value) { | ||
} | return String(value || '') | ||
.split('?')[0] | |||
.replace(/^\/index\.php\//, '') | |||
.replace(/_/g, ' ') | |||
.trim(); | |||
} | |||
function | function getLanguageStatusPageKey() { | ||
var | var raw = String(mw.config.get('wgPageName') || '').trim(); | ||
var | var normalized = normalizePageName(raw); | ||
return normalized || raw || '대문'; | |||
} | |||
function normalizeLanguageStatusValue(value) { | |||
value = String(value == null ? '' : value).toLowerCase().trim(); | |||
return LANGUAGE_STATUS_VALUES[value] ? value : ''; | |||
} | |||
function getLanguageTargetTitle(lang) { | |||
var data = document.getElementById('clbi-lang-data'); | |||
var keys; | |||
var i; | |||
var value; | |||
if (!data || !lang) return ''; | |||
keys = [ | |||
'data-' + lang, | |||
'data-page-' + lang, | |||
'data-title-' + lang, | |||
'data-target-' + lang, | |||
'data-lang-' + lang | |||
]; | |||
for (i = 0; i < keys.length; i += 1) { | |||
value = data.getAttribute(keys[i]); | |||
if (value) return value; | |||
} | |||
return ''; | |||
} | |||
} | |||
function getLanguageStatusPages(registry) { | |||
if (!registry || typeof registry !== 'object') return {}; | |||
if (registry.pages && typeof registry.pages === 'object') return registry.pages; | |||
return registry; | |||
} | |||
function readLanguageStatusEntry(registry, pageKey) { | |||
var pages = getLanguageStatusPages(registry); | |||
var underscored = String(pageKey || '').replace(/ /g, '_'); | |||
if (pages[pageKey] && typeof pages[pageKey] === 'object') return pages[pageKey]; | |||
if (pages[underscored] && typeof pages[underscored] === 'object') return pages[underscored]; | |||
return | |||
return {}; | |||
} | |||
function fetchLanguageStatusRegistry(api) { | |||
return api.get({ | |||
action: 'query', | |||
prop: 'revisions', | |||
titles: LANGUAGE_STATUS_TITLE, | |||
rvprop: 'content', | |||
rvslots: 'main', | |||
format: 'json', | |||
formatversion: 2 | |||
}).then(function (data) { | |||
var pages = data && data.query ? data.query.pages : []; | |||
var page = pages && pages.length ? pages[0] : null; | |||
var text = ''; | |||
if (!page || page.missing) return {}; | |||
} | |||
if (page.revisions && page.revisions[0]) { | |||
if (page.revisions[0].slots && page.revisions[0].slots.main) { | |||
text = page.revisions[0].slots.main.content || ''; | |||
} else { | |||
text = page.revisions[0].content || ''; | |||
} | |||
} | |||
if ( | if (!text) return {}; | ||
} | |||
try { | |||
return JSON.parse(text); | |||
} catch (err) { | |||
setStatus('LanguageStatus JSON 파싱 실패', 'error'); | |||
return {}; | |||
} | |||
}); | |||
} | |||
function renderLanguageStatusEditor(registry) { | |||
var pageKey = getLanguageStatusPageKey(); | |||
var entry = readLanguageStatusEntry(registry, pageKey); | |||
var currentLang = (document.getElementById('clbi-lang-data') || {}).getAttribute | |||
? (document.getElementById('clbi-lang-data').getAttribute('data-lang') || 'ko') | |||
: 'ko'; | |||
var html = ''; | |||
$('#dev-tools-language-page').text('PAGE: ' + pageKey); | |||
LANGUAGE_ORDER.forEach(function (lang) { | |||
var stored = normalizeLanguageStatusValue(entry[lang]); | |||
var fallback = getLanguageTargetTitle(lang) ? 'available' : 'unavailable'; | |||
var value = stored || fallback; | |||
var isCurrent = lang === currentLang; | |||
html += '' + | |||
'<div class="dev-tools-language-row" data-lang="' + escapeHtml(lang) + '">' + | |||
'<span class="dev-tools-language-name">' + escapeHtml(LANGUAGE_LABELS[lang] || lang.toUpperCase()) + (isCurrent ? ' *' : '') + '</span>' + | |||
'<select class="dev-tools-select dev-tools-language-select" data-lang="' + escapeHtml(lang) + '">' + | |||
'<option value="available"' + (value === 'available' ? ' selected' : '') + '>AVAILABLE</option>' + | |||
'<option value="wip"' + (value === 'wip' ? ' selected' : '') + '>WIP</option>' + | |||
'<option value="unavailable"' + (value === 'unavailable' ? ' selected' : '') + '>UNAVAILABLE</option>' + | |||
'</select>' + | |||
'</div>'; | |||
}); | |||
$('#dev-tools-language-list').html(html || 'NO DATA'); | |||
} | |||
function refreshLanguageStatusEditor() { | |||
withApi(function (api) { | |||
} | setStatus('언어 상태 불러오는 중...'); | ||
fetchLanguageStatusRegistry(api).then(function (registry) { | |||
renderLanguageStatusEditor(registry || {}); | |||
setStatus('언어 상태 로드 완료', 'ok'); | |||
}).catch(function () { | |||
renderLanguageStatusEditor({}); | |||
setStatus('언어 상태 로드 실패', 'error'); | |||
}); | |||
}, function () { | |||
setStatus('mediawiki.api 로드 실패', 'error'); | |||
}); | |||
} | |||
function saveLanguageStatusEditor() { | |||
withApi(function (api) { | |||
var pageKey = getLanguageStatusPageKey(); | |||
setButtonsDisabled(true); | |||
setStatus('언어 상태 저장 중...'); | |||
fetchLanguageStatusRegistry(api).then(function (registry) { | |||
var pages = getLanguageStatusPages(registry); | |||
var nextRegistry = registry && typeof registry === 'object' ? registry : {}; | |||
var entry = {}; | |||
if (nextRegistry.pages && typeof nextRegistry.pages === 'object') { | |||
. | pages = nextRegistry.pages; | ||
} else { | |||
pages = nextRegistry; | |||
} | |||
$('.dev-tools-language-select').each(function () { | |||
var lang = $(this).data('lang'); | |||
var value = normalizeLanguageStatusValue($(this).val()); | |||
if ( | if (lang && value) { | ||
entry[lang] = value; | |||
} | } | ||
}); | |||
pages[pageKey] = entry; | |||
return api.postWithToken('csrf', { | |||
action: 'edit', | |||
title: LANGUAGE_STATUS_TITLE, | |||
text: JSON.stringify(nextRegistry, null, 2) + '\n', | |||
summary: 'Update language status: ' + pageKey, | |||
contentmodel: 'json', | |||
format: 'json', | |||
formatversion: 2 | |||
}); | |||
}).then(function () { | |||
setStatus('언어 상태 저장 완료', 'ok'); | |||
if (window.CLBI_LANGUAGE_STATUS && typeof window.CLBI_LANGUAGE_STATUS.reload === 'function') { | |||
}); | window.CLBI_LANGUAGE_STATUS.reload(); | ||
} | |||
}).catch(function (err) { | |||
setStatus('언어 상태 저장 실패: ' + (err && err.message ? err.message : err), 'error'); | |||
}).always(function () { | |||
setButtonsDisabled(false); | |||
}); | |||
}, function () { | |||
setButtonsDisabled(false); | |||
setStatus('mediawiki.api 로드 실패', 'error'); | |||
}); | |||
} | |||
function bindEvents() { | |||
$(document).on('click', '#dev-tools-panel .dev-tools-button-disabled', function (event) { | |||
.on('click | event.preventDefault(); | ||
event.stopPropagation(); | |||
}); | |||
$(document).on('click', '#dev-tools-toggle', function () { | |||
var $panel = $('#dev-tools-panel'); | |||
$panel.toggleClass('dev-tools-collapsed'); | |||
var collapsed = $panel.hasClass('dev-tools-collapsed'); | |||
localStorage.setItem('dev-tools-collapsed', collapsed ? 'true' : 'false'); | |||
$(this).text(collapsed ? '+' : '−'); | |||
}); | |||
$(document).on('click', '#dev-tools-export', exportSources); | |||
$(document).on('click', '#dev-tools-progress-refresh', refreshProgressSummary); | |||
$(document).on('click', '#dev-tools-language-refresh', refreshLanguageStatusEditor); | |||
$(document).on('click', '#dev-tools-language-save', saveLanguageStatusEditor); | |||
$(document).on('click', '#dev-tools-xp-add', function () { | |||
var amount = parseInt($('#dev-tools-xp-amount').val(), 10); | |||
var reason = $('#dev-tools-xp-reason').val() || '관리자 테스트'; | |||
if (!Number.isFinite(amount) || amount <= 0) { | |||
setStatus('XP 값이 잘못되었습니다.', 'error'); | |||
return; | |||
} | |||
runProgressAdmin('add_xp', { | |||
amount: amount, | |||
reason: reason | |||
}); | |||
}); | |||
$(document).on('click', '#dev-tools-daily-reset', function () { | |||
runProgressAdmin('reset_daily'); | |||
}); | |||
$(document).on('click', '#dev-tools-level-reset', function () { | |||
if (!window.confirm('레벨/총 경험치와 오늘 보너스 카운트를 리셋할까요?')) return; | |||
runProgressAdmin('reset_progress'); | |||
}); | |||
} | |||
$(function () { | |||
buildPanel(); | |||
bindEvents(); | |||
refreshProgressSummary(); | |||
refreshLanguageStatusEditor(); | |||
if (mw.hook) { | |||
mw.hook('wikipage.content').add(refreshDocumentTools); | |||
} | |||
}); | |||
})(mediaWiki, jQuery); | |||
2026년 5월 30일 (토) 23:55 기준 최신판
(function (mw, $) {
'use strict';
var OWNER = 'Nxdsxn';
var JSZIP_URL = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
var LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
var LANGUAGE_ORDER = ['ko', 'en', 'zh', 'ja', 'ru', 'es'];
var LANGUAGE_LABELS = {
ko: '한국어',
en: 'English',
zh: '中文',
ja: '日本語',
ru: 'Русский',
es: 'Español'
};
var LANGUAGE_STATUS_VALUES = {
available: true,
wip: true,
unavailable: true
};
function isOwner() {
var name = mw.config.get('wgUserName') || '';
var groups = mw.config.get('wgUserGroups') || [];
return name === OWNER || groups.indexOf('sysop') !== -1 || groups.indexOf('interface-admin') !== -1;
}
if (!isOwner()) return;
if (window.DevToolsPanelInitialized) return;
window.DevToolsPanelInitialized = true;
var sourceFiles = [
{ title: 'MediaWiki:Common.css', out: 'css/Common.css', ctype: 'text/css' },
{ title: 'MediaWiki:Theme.css', out: 'css/Theme.css', ctype: 'text/css' },
{ title: 'MediaWiki:Layout.css', out: 'css/Layout.css', ctype: 'text/css' },
{ title: 'MediaWiki:MainPage.css', out: 'css/MainPage.css', ctype: 'text/css' },
{ title: 'MediaWiki:Components.css', out: 'css/Components.css', ctype: 'text/css' },
{ title: 'MediaWiki:Template.css', out: 'css/Template.css', ctype: 'text/css' },
{ title: 'MediaWiki:Template.Infobox.css', out: 'css/Template.Infobox.css', ctype: 'text/css' },
{ title: 'MediaWiki:Icons.css', out: 'css/Icons.css', ctype: 'text/css' },
{ title: 'MediaWiki:Editor.css', out: 'css/Editor.css', ctype: 'text/css' },
{ title: 'MediaWiki:Dialogs.css', out: 'css/Dialogs.css', ctype: 'text/css' },
{ title: 'MediaWiki:DevTools.css', out: 'css/DevTools.css', ctype: 'text/css' },
{ title: 'MediaWiki:AnecdoteViewer.css', out: 'css/AnecdoteViewer.css', ctype: 'text/css' },
{ title: 'MediaWiki:Test.css', out: 'css/Test.css', ctype: 'text/css' },
{ title: 'MediaWiki:Common.js', out: 'js/Common.js', ctype: 'text/javascript' },
{ title: 'MediaWiki:Lang.js', out: 'js/Lang.js', ctype: 'text/javascript' },
{ title: 'MediaWiki:DevTools.js', out: 'js/DevTools.js', ctype: 'text/javascript' },
{ title: 'MediaWiki:CategoryNav.js', out: 'js/CategoryNav.js', ctype: 'text/javascript' },
{ title: 'MediaWiki:AnecdoteViewer.js', out: 'js/AnecdoteViewer.js', ctype: 'text/javascript' }
];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function normalizeDocumentToolPageName(value) {
return String(value || '')
.split('?')[0]
.replace(/^\/index\.php\//, '')
.replace(/_/g, ' ')
.trim();
}
function getDocumentToolPageName() {
var pageName = mw.config.get('wgPageName') || '';
if (pageName) {
return String(pageName).replace(/ /g, '_');
}
pageName = normalizeDocumentToolPageName(window.location.pathname || '');
return pageName ? pageName.replace(/ /g, '_') : '대문';
}
function getDocumentToolDisplayTitle() {
var pageName = getDocumentToolPageName();
return normalizeDocumentToolPageName(pageName) || '현재 문서';
}
function getDocumentToolUrl(pageName, params) {
pageName = pageName || getDocumentToolPageName();
params = params || {};
if (mw.util && typeof mw.util.getUrl === 'function') {
return mw.util.getUrl(pageName, params);
}
var query = $.param(params);
return '/index.php/' + encodeURI(String(pageName || '').replace(/ /g, '_')) + (query ? '?' + query : '');
}
function getDocumentToolTalkPageName() {
var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
var formattedNamespaces = mw.config.get('wgFormattedNamespaces') || {};
var title = String(mw.config.get('wgTitle') || '').trim();
var talkNamespace;
var prefix;
if (!Number.isFinite(namespaceNumber) || namespaceNumber < 0 || namespaceNumber % 2 !== 0) {
return '';
}
talkNamespace = namespaceNumber + 1;
prefix = formattedNamespaces[String(talkNamespace)] || formattedNamespaces[talkNamespace] || '';
if (!prefix || !title) {
return '';
}
return prefix + ':' + title.replace(/ /g, '_');
}
function renderDocumentToolLink(label, href, disabled) {
if (disabled || !href) {
return '<span class="dev-tools-button dev-tools-link-button dev-tools-button-disabled" aria-disabled="true">' + escapeHtml(label) + '</span>';
}
return '<a class="dev-tools-button dev-tools-link-button" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>';
}
function buildDocumentToolsHtml() {
var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
var pageName = getDocumentToolPageName();
var isSpecial = Number.isFinite(namespaceNumber) && namespaceNumber < 0;
var talkPageName = getDocumentToolTalkPageName();
return '' +
'<div class="dev-tools-section dev-tools-document-section">' +
'<div class="dev-tools-section-title">DOCUMENT TOOLS</div>' +
'<div class="dev-tools-current-page" id="dev-tools-current-page">PAGE: ' + escapeHtml(getDocumentToolDisplayTitle()) + '</div>' +
'<div class="dev-tools-grid dev-tools-doc-grid" id="dev-tools-doc-grid">' +
renderDocumentToolLink('문서', getDocumentToolUrl(pageName), isSpecial) +
renderDocumentToolLink('편집', getDocumentToolUrl(pageName, { action: 'edit' }), isSpecial) +
renderDocumentToolLink('원본', getDocumentToolUrl(pageName, { action: 'raw' }), isSpecial) +
renderDocumentToolLink('역사', getDocumentToolUrl(pageName, { action: 'history' }), isSpecial) +
renderDocumentToolLink('토론', talkPageName ? getDocumentToolUrl(talkPageName) : '', !talkPageName) +
renderDocumentToolLink('새로고침', getDocumentToolUrl(pageName, { action: 'purge' }), isSpecial) +
'</div>' +
'<div class="dev-tools-note">상단 문서 조작은 일반 화면에서 숨기고 이 패널로 모읍니다.</div>' +
'</div>';
}
function refreshDocumentTools() {
var $section = $('.dev-tools-document-section');
if (!$section.length) return;
$section.replaceWith(buildDocumentToolsHtml());
}
function buildPanel() {
if ($('#dev-tools-panel').length) return;
var collapsed = localStorage.getItem('dev-tools-collapsed') === 'true';
var html = '' +
'<div id="dev-tools-panel" class="' + (collapsed ? 'dev-tools-collapsed' : '') + '">' +
'<div id="dev-tools-header">' +
'<div id="dev-tools-title"><span class="dev-tools-led"></span><span>ADMIN TOOLS</span></div>' +
'<button type="button" id="dev-tools-toggle" aria-label="도구 접기">' + (collapsed ? '+' : '−') + '</button>' +
'</div>' +
'<div id="dev-tools-body">' +
buildDocumentToolsHtml() +
'<div class="dev-tools-section">' +
'<div class="dev-tools-section-title">SOURCE PACKAGE</div>' +
'<button type="button" class="dev-tools-button" id="dev-tools-export">CSS / JS ZIP 다운로드</button>' +
'<div class="dev-tools-note">MediaWiki namespace의 CSS·JS 원본을 ZIP으로 묶습니다.</div>' +
'</div>' +
'<div class="dev-tools-section dev-tools-progress-section">' +
'<div class="dev-tools-section-title">PROGRESS TEST</div>' +
'<div class="dev-tools-progress-readout" id="dev-tools-progress-readout">SYNC</div>' +
'<label class="dev-tools-label" for="dev-tools-xp-amount">XP AMOUNT</label>' +
'<input id="dev-tools-xp-amount" class="dev-tools-input" type="number" min="1" step="1" value="10">' +
'<label class="dev-tools-label" for="dev-tools-xp-reason">REASON</label>' +
'<input id="dev-tools-xp-reason" class="dev-tools-input" type="text" value="관리자 테스트">' +
'<div class="dev-tools-grid">' +
'<button type="button" class="dev-tools-button" id="dev-tools-xp-add">XP 지급</button>' +
'<button type="button" class="dev-tools-button" id="dev-tools-progress-refresh">동기화</button>' +
'</div>' +
'<button type="button" class="dev-tools-button" id="dev-tools-daily-reset">오늘 XP 보너스 리셋</button>' +
'<button type="button" class="dev-tools-button dev-tools-danger" id="dev-tools-level-reset">레벨 리셋</button>' +
'</div>' +
'<div class="dev-tools-section dev-tools-language-section">' +
'<div class="dev-tools-section-title">LANGUAGE STATUS</div>' +
'<div class="dev-tools-language-page" id="dev-tools-language-page">PAGE: SYNC</div>' +
'<div class="dev-tools-language-list" id="dev-tools-language-list">SYNC</div>' +
'<div class="dev-tools-grid">' +
'<button type="button" class="dev-tools-button" id="dev-tools-language-save">저장</button>' +
'<button type="button" class="dev-tools-button" id="dev-tools-language-refresh">불러오기</button>' +
'</div>' +
'<div class="dev-tools-note">현재 문서의 언어 다이얼 상태를 JSON으로 저장합니다.</div>' +
'</div>' +
'<div id="dev-tools-status">READY</div>' +
'</div>' +
'</div>';
$('body').append(html);
}
function setStatus(text, type) {
var $status = $('#dev-tools-status');
$status.removeClass('dev-tools-status-ok dev-tools-status-error');
if (type === 'ok') $status.addClass('dev-tools-status-ok');
if (type === 'error') $status.addClass('dev-tools-status-error');
$status.text(text || '');
}
function setButtonsDisabled(disabled) {
$('#dev-tools-panel button, #dev-tools-panel input, #dev-tools-panel select').prop('disabled', !!disabled);
$('#dev-tools-toggle').prop('disabled', false);
}
function loadScript(url) {
return new Promise(function (resolve, reject) {
if (window.JSZip) {
resolve();
return;
}
var script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function rawUrl(file) {
return mw.util.getUrl(file.title, {
action: 'raw',
ctype: file.ctype
});
}
function fetchRaw(file) {
return fetch(rawUrl(file), { credentials: 'same-origin' }).then(function (res) {
if (!res.ok) throw new Error(file.title + ' HTTP ' + res.status);
return res.text();
});
}
function downloadBlob(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () {
URL.revokeObjectURL(url);
}, 1000);
}
function exportSources() {
setButtonsDisabled(true);
setStatus('ZIP 생성 중...');
loadScript(JSZIP_URL)
.then(function () {
var zip = new JSZip();
var meta = [];
return Promise.all(sourceFiles.map(function (file) {
return fetchRaw(file).then(function (text) {
zip.file(file.out, text);
meta.push(file.out);
}).catch(function (err) {
zip.file(file.out + '.ERROR.txt', String(err && err.message ? err.message : err));
});
})).then(function () {
zip.file('README.txt', 'CLBI Wiki CSS/JS export\nGenerated: ' + new Date().toISOString() + '\n\nFiles:\n' + meta.sort().join('\n') + '\n');
return zip.generateAsync({ type: 'blob' });
});
})
.then(function (blob) {
var stamp = new Date().toISOString().replace(/[:.]/g, '-');
downloadBlob(blob, 'clbiwiki-source-' + stamp + '.zip');
setStatus('ZIP 다운로드 완료', 'ok');
})
.catch(function (err) {
setStatus('ZIP 실패: ' + (err && err.message ? err.message : err), 'error');
})
.finally(function () {
setButtonsDisabled(false);
});
}
function withApi(done, fail) {
mw.loader.using(['mediawiki.api']).then(function () {
done(new mw.Api());
}, function () {
if (typeof fail === 'function') fail();
});
}
function updateReadout(summary) {
var $box = $('#dev-tools-progress-readout');
if (!summary) {
$box.text('NO DATA');
return;
}
$box.html(
'<span>' + (summary.isMaxLevel ? 'MAX ' : 'LVL ') + escapeHtml(summary.level) + '</span>' +
'<span>' + escapeHtml(summary.totalXp) + ' XP</span>' +
'<span>TODAY ' + escapeHtml(summary.dailyXp) + ' XP</span>' +
'<span>READ ' + escapeHtml(summary.dailyReadCount) + '</span>' +
'<span>DISC ' + escapeHtml(summary.discoveryCount) + '</span>'
);
}
function refreshProgressSummary() {
withApi(function (api) {
api.get({
action: 'progress_summary',
format: 'json',
formatversion: 2
}).then(function (data) {
var payload = data && data.progress_summary;
if (payload && payload.available && payload.summary) {
updateReadout(payload.summary);
if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.requestSummary === 'function') {
window.ProgressSystemWebUi.requestSummary(true);
}
setStatus('진행도 동기화 완료', 'ok');
} else {
updateReadout(null);
setStatus('진행도 없음', 'error');
}
}).catch(function () {
setStatus('동기화 실패', 'error');
});
}, function () {
setStatus('mediawiki.api 로드 실패', 'error');
});
}
function runProgressAdmin(operation, extra) {
extra = extra || {};
setButtonsDisabled(true);
setStatus('요청 처리 중...');
withApi(function (api) {
var payload = $.extend({
action: 'progress_admin',
format: 'json',
formatversion: 2,
errorformat: 'plaintext',
operation: operation
}, extra);
api.postWithToken('csrf', payload).then(function (data) {
var result = data && data.progress_admin;
if (!result || !result.ok) {
setStatus('실패: ' + (result && result.reason ? result.reason : 'unknown'), 'error');
return;
}
if (result.summary) {
updateReadout(result.summary);
if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.applySummary === 'function') {
window.ProgressSystemWebUi.applySummary(result.summary, {
animateGain: operation === 'add_xp'
});
} else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.requestSummary === 'function') {
window.ProgressSystemWebUi.requestSummary();
}
}
setStatus('완료: ' + operation, 'ok');
}).catch(function (code) {
setStatus('API 실패: ' + code, 'error');
}).always(function () {
setButtonsDisabled(false);
});
}, function () {
setButtonsDisabled(false);
setStatus('mediawiki.api 로드 실패', 'error');
});
}
function normalizePageName(value) {
return String(value || '')
.split('?')[0]
.replace(/^\/index\.php\//, '')
.replace(/_/g, ' ')
.trim();
}
function getLanguageStatusPageKey() {
var raw = String(mw.config.get('wgPageName') || '').trim();
var normalized = normalizePageName(raw);
return normalized || raw || '대문';
}
function normalizeLanguageStatusValue(value) {
value = String(value == null ? '' : value).toLowerCase().trim();
return LANGUAGE_STATUS_VALUES[value] ? value : '';
}
function getLanguageTargetTitle(lang) {
var data = document.getElementById('clbi-lang-data');
var keys;
var i;
var value;
if (!data || !lang) return '';
keys = [
'data-' + lang,
'data-page-' + lang,
'data-title-' + lang,
'data-target-' + lang,
'data-lang-' + lang
];
for (i = 0; i < keys.length; i += 1) {
value = data.getAttribute(keys[i]);
if (value) return value;
}
return '';
}
function getLanguageStatusPages(registry) {
if (!registry || typeof registry !== 'object') return {};
if (registry.pages && typeof registry.pages === 'object') return registry.pages;
return registry;
}
function readLanguageStatusEntry(registry, pageKey) {
var pages = getLanguageStatusPages(registry);
var underscored = String(pageKey || '').replace(/ /g, '_');
if (pages[pageKey] && typeof pages[pageKey] === 'object') return pages[pageKey];
if (pages[underscored] && typeof pages[underscored] === 'object') return pages[underscored];
return {};
}
function fetchLanguageStatusRegistry(api) {
return api.get({
action: 'query',
prop: 'revisions',
titles: LANGUAGE_STATUS_TITLE,
rvprop: 'content',
rvslots: 'main',
format: 'json',
formatversion: 2
}).then(function (data) {
var pages = data && data.query ? data.query.pages : [];
var page = pages && pages.length ? pages[0] : null;
var text = '';
if (!page || page.missing) return {};
if (page.revisions && page.revisions[0]) {
if (page.revisions[0].slots && page.revisions[0].slots.main) {
text = page.revisions[0].slots.main.content || '';
} else {
text = page.revisions[0].content || '';
}
}
if (!text) return {};
try {
return JSON.parse(text);
} catch (err) {
setStatus('LanguageStatus JSON 파싱 실패', 'error');
return {};
}
});
}
function renderLanguageStatusEditor(registry) {
var pageKey = getLanguageStatusPageKey();
var entry = readLanguageStatusEntry(registry, pageKey);
var currentLang = (document.getElementById('clbi-lang-data') || {}).getAttribute
? (document.getElementById('clbi-lang-data').getAttribute('data-lang') || 'ko')
: 'ko';
var html = '';
$('#dev-tools-language-page').text('PAGE: ' + pageKey);
LANGUAGE_ORDER.forEach(function (lang) {
var stored = normalizeLanguageStatusValue(entry[lang]);
var fallback = getLanguageTargetTitle(lang) ? 'available' : 'unavailable';
var value = stored || fallback;
var isCurrent = lang === currentLang;
html += '' +
'<div class="dev-tools-language-row" data-lang="' + escapeHtml(lang) + '">' +
'<span class="dev-tools-language-name">' + escapeHtml(LANGUAGE_LABELS[lang] || lang.toUpperCase()) + (isCurrent ? ' *' : '') + '</span>' +
'<select class="dev-tools-select dev-tools-language-select" data-lang="' + escapeHtml(lang) + '">' +
'<option value="available"' + (value === 'available' ? ' selected' : '') + '>AVAILABLE</option>' +
'<option value="wip"' + (value === 'wip' ? ' selected' : '') + '>WIP</option>' +
'<option value="unavailable"' + (value === 'unavailable' ? ' selected' : '') + '>UNAVAILABLE</option>' +
'</select>' +
'</div>';
});
$('#dev-tools-language-list').html(html || 'NO DATA');
}
function refreshLanguageStatusEditor() {
withApi(function (api) {
setStatus('언어 상태 불러오는 중...');
fetchLanguageStatusRegistry(api).then(function (registry) {
renderLanguageStatusEditor(registry || {});
setStatus('언어 상태 로드 완료', 'ok');
}).catch(function () {
renderLanguageStatusEditor({});
setStatus('언어 상태 로드 실패', 'error');
});
}, function () {
setStatus('mediawiki.api 로드 실패', 'error');
});
}
function saveLanguageStatusEditor() {
withApi(function (api) {
var pageKey = getLanguageStatusPageKey();
setButtonsDisabled(true);
setStatus('언어 상태 저장 중...');
fetchLanguageStatusRegistry(api).then(function (registry) {
var pages = getLanguageStatusPages(registry);
var nextRegistry = registry && typeof registry === 'object' ? registry : {};
var entry = {};
if (nextRegistry.pages && typeof nextRegistry.pages === 'object') {
pages = nextRegistry.pages;
} else {
pages = nextRegistry;
}
$('.dev-tools-language-select').each(function () {
var lang = $(this).data('lang');
var value = normalizeLanguageStatusValue($(this).val());
if (lang && value) {
entry[lang] = value;
}
});
pages[pageKey] = entry;
return api.postWithToken('csrf', {
action: 'edit',
title: LANGUAGE_STATUS_TITLE,
text: JSON.stringify(nextRegistry, null, 2) + '\n',
summary: 'Update language status: ' + pageKey,
contentmodel: 'json',
format: 'json',
formatversion: 2
});
}).then(function () {
setStatus('언어 상태 저장 완료', 'ok');
if (window.CLBI_LANGUAGE_STATUS && typeof window.CLBI_LANGUAGE_STATUS.reload === 'function') {
window.CLBI_LANGUAGE_STATUS.reload();
}
}).catch(function (err) {
setStatus('언어 상태 저장 실패: ' + (err && err.message ? err.message : err), 'error');
}).always(function () {
setButtonsDisabled(false);
});
}, function () {
setButtonsDisabled(false);
setStatus('mediawiki.api 로드 실패', 'error');
});
}
function bindEvents() {
$(document).on('click', '#dev-tools-panel .dev-tools-button-disabled', function (event) {
event.preventDefault();
event.stopPropagation();
});
$(document).on('click', '#dev-tools-toggle', function () {
var $panel = $('#dev-tools-panel');
$panel.toggleClass('dev-tools-collapsed');
var collapsed = $panel.hasClass('dev-tools-collapsed');
localStorage.setItem('dev-tools-collapsed', collapsed ? 'true' : 'false');
$(this).text(collapsed ? '+' : '−');
});
$(document).on('click', '#dev-tools-export', exportSources);
$(document).on('click', '#dev-tools-progress-refresh', refreshProgressSummary);
$(document).on('click', '#dev-tools-language-refresh', refreshLanguageStatusEditor);
$(document).on('click', '#dev-tools-language-save', saveLanguageStatusEditor);
$(document).on('click', '#dev-tools-xp-add', function () {
var amount = parseInt($('#dev-tools-xp-amount').val(), 10);
var reason = $('#dev-tools-xp-reason').val() || '관리자 테스트';
if (!Number.isFinite(amount) || amount <= 0) {
setStatus('XP 값이 잘못되었습니다.', 'error');
return;
}
runProgressAdmin('add_xp', {
amount: amount,
reason: reason
});
});
$(document).on('click', '#dev-tools-daily-reset', function () {
runProgressAdmin('reset_daily');
});
$(document).on('click', '#dev-tools-level-reset', function () {
if (!window.confirm('레벨/총 경험치와 오늘 보너스 카운트를 리셋할까요?')) return;
runProgressAdmin('reset_progress');
});
}
$(function () {
buildPanel();
bindEvents();
refreshProgressSummary();
refreshLanguageStatusEditor();
if (mw.hook) {
mw.hook('wikipage.content').add(refreshDocumentTools);
}
});
})(mediaWiki, jQuery);