미디어위키:DevTools.js: 두 판 사이의 차이

(새 문서: // ========== 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번째 줄:
// ========== Developer Tools Panel ==========
(function (mw, $) {
    'use strict';


(function () {
    var OWNER = 'Nxdsxn';
var OWNER_USERNAME = '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
    };


var START_PAGES = [
    function isOwner() {
'MediaWiki:Common.css',
        var name = mw.config.get('wgUserName') || '';
'MediaWiki:Common.js',
        var groups = mw.config.get('wgUserGroups') || [];
'MediaWiki:Lang.js'
        return name === OWNER || groups.indexOf('sysop') !== -1 || groups.indexOf('interface-admin') !== -1;
];
    }


var JSZIP_URL = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
    if (!isOwner()) return;
    if (window.DevToolsPanelInitialized) return;
    window.DevToolsPanelInitialized = true;


function isWikiOwner() {
    var sourceFiles = [
return mw.config.get('wgUserName') === OWNER_USERNAME;
        { 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 setDevToolsStatus(text, type) {
    function escapeHtml(value) {
var status = document.getElementById('dev-tools-status');
        return String(value == null ? '' : value)
if (!status) return;
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }


status.className = '';


if (type === 'error') {
    function normalizeDocumentToolPageName(value) {
status.classList.add('dev-tools-status-error');
        return String(value || '')
}
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
    }


if (type === 'ok') {
    function getDocumentToolPageName() {
status.classList.add('dev-tools-status-ok');
        var pageName = mw.config.get('wgPageName') || '';
}


status.textContent = text || '';
        if (pageName) {
}
            return String(pageName).replace(/ /g, '_');
        }


function loadExternalScriptOnce(url, globalName) {
        pageName = normalizeDocumentToolPageName(window.location.pathname || '');
return new Promise(function (resolve, reject) {
        return pageName ? pageName.replace(/ /g, '_') : '대문';
if (globalName && window[globalName]) {
    }
resolve();
return;
}


var existing = document.querySelector('script[data-dev-tool-src="' + url + '"]');
    function getDocumentToolDisplayTitle() {
        var pageName = getDocumentToolPageName();
        return normalizeDocumentToolPageName(pageName) || '현재 문서';
    }


if (existing) {
    function getDocumentToolUrl(pageName, params) {
existing.addEventListener('load', function () {
        pageName = pageName || getDocumentToolPageName();
resolve();
        params = params || {};
});


existing.addEventListener('error', function () {
        if (mw.util && typeof mw.util.getUrl === 'function') {
reject(new Error('script load failed: ' + url));
            return mw.util.getUrl(pageName, params);
});
        }


return;
        var query = $.param(params);
}
        return '/index.php/' + encodeURI(String(pageName || '').replace(/ /g, '_')) + (query ? '?' + query : '');
    }


var script = document.createElement('script');
    function getDocumentToolTalkPageName() {
script.src = url;
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
script.async = true;
        var formattedNamespaces = mw.config.get('wgFormattedNamespaces') || {};
script.setAttribute('data-dev-tool-src', url);
        var title = String(mw.config.get('wgTitle') || '').trim();
        var talkNamespace;
        var prefix;


script.onload = function () {
        if (!Number.isFinite(namespaceNumber) || namespaceNumber < 0 || namespaceNumber % 2 !== 0) {
resolve();
            return '';
};
        }


script.onerror = function () {
        talkNamespace = namespaceNumber + 1;
reject(new Error('script load failed: ' + url));
        prefix = formattedNamespaces[String(talkNamespace)] || formattedNamespaces[talkNamespace] || '';
};


document.head.appendChild(script);
        if (!prefix || !title) {
});
            return '';
}
        }


function normalizeWikiTitle(title) {
        return prefix + ':' + title.replace(/ /g, '_');
return String(title || '')
    }
.replace(/_/g, ' ')
.trim();
}


function titleToFilePath(title) {
    function renderDocumentToolLink(label, href, disabled) {
var clean = normalizeWikiTitle(title).replace(/^MediaWiki:/i, '');
        if (disabled || !href) {
var lower = clean.toLowerCase();
            return '<span class="dev-tools-button dev-tools-link-button dev-tools-button-disabled" aria-disabled="true">' + escapeHtml(label) + '</span>';
        }


if (lower.endsWith('.css')) {
        return '<a class="dev-tools-button dev-tools-link-button" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>';
return 'css/' + clean;
    }
}


if (lower.endsWith('.js')) {
    function buildDocumentToolsHtml() {
return 'js/' + clean;
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
}
        var pageName = getDocumentToolPageName();
        var isSpecial = Number.isFinite(namespaceNumber) && namespaceNumber < 0;
        var talkPageName = getDocumentToolTalkPageName();


return 'other/' + clean;
        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 fetchWikiPageSource(title) {
    function refreshDocumentTools() {
var api = new mw.Api();
        var $section = $('.dev-tools-document-section');


return api.get({
        if (!$section.length) return;
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];


if (!page || page.missing) {
        $section.replaceWith(buildDocumentToolsHtml());
throw new Error('missing page: ' + title);
    }
}


var revision = page.revisions && page.revisions[0];
    function buildPanel() {
var content = '';
        if ($('#dev-tools-panel').length) return;


if (revision && revision.slots && revision.slots.main) {
        var collapsed = localStorage.getItem('dev-tools-collapsed') === 'true';
content = revision.slots.main.content || '';
        var html = '' +
} else if (revision) {
            '<div id="dev-tools-panel" class="' + (collapsed ? 'dev-tools-collapsed' : '') + '">' +
content = revision.content || revision['*'] || '';
                '<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>';


return {
        $('body').append(html);
title: normalizeWikiTitle(title),
    }
content: content
};
});
}


function extractImportedMediaWikiTitles(source) {
    function setStatus(text, type) {
var result = [];
        var $status = $('#dev-tools-status');
var seen = {};
        $status.removeClass('dev-tools-status-ok dev-tools-status-error');
var regex = /@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?/gi;
        if (type === 'ok') $status.addClass('dev-tools-status-ok');
var match;
        if (type === 'error') $status.addClass('dev-tools-status-error');
        $status.text(text || '');
    }


while ((match = regex.exec(source)) !== null) {
    function setButtonsDisabled(disabled) {
var rawUrl = match[1];
        $('#dev-tools-panel button, #dev-tools-panel input, #dev-tools-panel select').prop('disabled', !!disabled);
var title = '';
        $('#dev-tools-toggle').prop('disabled', false);
    }


try {
    function loadScript(url) {
var parsed = new URL(rawUrl, window.location.origin);
        return new Promise(function (resolve, reject) {
title = parsed.searchParams.get('title') || '';
            if (window.JSZip) {
                resolve();
                return;
            }


if (!title && parsed.pathname) {
            var script = document.createElement('script');
var pathMatch = parsed.pathname.match(/\/index\.php\/(.+)$/);
            script.src = url;
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
        });
    }


if (pathMatch) {
    function rawUrl(file) {
title = decodeURIComponent(pathMatch[1]);
        return mw.util.getUrl(file.title, {
}
            action: 'raw',
}
            ctype: file.ctype
} catch (err) {
        });
title = '';
    }
}


title = normalizeWikiTitle(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();
        });
    }


if (!/^MediaWiki:/i.test(title)) {
    function downloadBlob(blob, filename) {
continue;
        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);
    }


if (!/\.(css|js)$/i.test(title)) {
    function exportSources() {
continue;
        setButtonsDisabled(true);
}
        setStatus('ZIP 생성 중...');


if (seen[title]) {
        loadScript(JSZIP_URL)
continue;
            .then(function () {
}
                var zip = new JSZip();
                var meta = [];


seen[title] = true;
                return Promise.all(sourceFiles.map(function (file) {
result.push(title);
                    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);
            });
    }


return result;
    function withApi(done, fail) {
}
        mw.loader.using(['mediawiki.api']).then(function () {
            done(new mw.Api());
        }, function () {
            if (typeof fail === 'function') fail();
        });
    }


async function collectWikiAssets() {
    function updateReadout(summary) {
var queue = START_PAGES.slice();
        var $box = $('#dev-tools-progress-readout');
var visited = {};
        if (!summary) {
var files = [];
            $box.text('NO DATA');
var failed = [];
            return;
        }


while (queue.length) {
        $box.html(
var title = normalizeWikiTitle(queue.shift());
            '<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 (!title || visited[title]) {
    function refreshProgressSummary() {
continue;
        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');
        });
    }


visited[title] = true;
    function runProgressAdmin(operation, extra) {
setDevToolsStatus('수집 : ' + title);
        extra = extra || {};
        setButtonsDisabled(true);
        setStatus('요청 처리 ...');


try {
        withApi(function (api) {
var page = await fetchWikiPageSource(title);
            var payload = $.extend({
                action: 'progress_admin',
                format: 'json',
                formatversion: 2,
                errorformat: 'plaintext',
                operation: operation
            }, extra);


files.push({
            api.postWithToken('csrf', payload).then(function (data) {
title: page.title,
                var result = data && data.progress_admin;
path: titleToFilePath(page.title),
                if (!result || !result.ok) {
content: page.content
                    setStatus('실패: ' + (result && result.reason ? result.reason : 'unknown'), 'error');
});
                    return;
                }


if (/\.css$/i.test(page.title)) {
                if (result.summary) {
var imports = extractImportedMediaWikiTitles(page.content);
                    updateReadout(result.summary);


imports.forEach(function (importedTitle) {
                    if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.applySummary === 'function') {
if (!visited[importedTitle]) {
                        window.ProgressSystemWebUi.applySummary(result.summary, {
queue.push(importedTitle);
                            animateGain: operation === 'add_xp'
}
                        });
});
                    } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.requestSummary === 'function') {
}
                        window.ProgressSystemWebUi.requestSummary();
} catch (err) {
                    }
failed.push({
                }
title: title,
error: err && err.message ? err.message : String(err)
});
}
}


return {
                setStatus('완료: ' + operation, 'ok');
files: files,
            }).catch(function (code) {
failed: failed
                setStatus('API 실패: ' + code, 'error');
};
            }).always(function () {
}
                setButtonsDisabled(false);
            });
        }, function () {
            setButtonsDisabled(false);
            setStatus('mediawiki.api 로드 실패', 'error');
        });
    }


function makeBackupFilename() {
var now = new Date();
var yyyy = now.getFullYear();
var mm = String(now.getMonth() + 1).padStart(2, '0');
var dd = String(now.getDate()).padStart(2, '0');
var hh = String(now.getHours()).padStart(2, '0');
var mi = String(now.getMinutes()).padStart(2, '0');


return 'wiki-assets-' + yyyy + '-' + mm + '-' + dd + '-' + hh + mi + '.zip';
    function normalizePageName(value) {
}
        return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
    }


function downloadBlob(blob, filename) {
    function getLanguageStatusPageKey() {
var url = URL.createObjectURL(blob);
        var raw = String(mw.config.get('wgPageName') || '').trim();
var link = document.createElement('a');
        var normalized = normalizePageName(raw);
        return normalized || raw || '대문';
    }


link.href = url;
    function normalizeLanguageStatusValue(value) {
link.download = filename;
        value = String(value == null ? '' : value).toLowerCase().trim();
document.body.appendChild(link);
        return LANGUAGE_STATUS_VALUES[value] ? value : '';
link.click();
    }


setTimeout(function () {
    function getLanguageTargetTitle(lang) {
URL.revokeObjectURL(url);
        var data = document.getElementById('clbi-lang-data');
link.remove();
        var keys;
}, 1000);
        var i;
}
        var value;


async function downloadWikiAssetsZip() {
        if (!data || !lang) return '';
var button = document.getElementById('dev-tools-backup-assets');


if (button) {
        keys = [
button.disabled = true;
            'data-' + lang,
button.textContent = '백업 생성 중...';
            'data-page-' + lang,
}
            'data-title-' + lang,
            'data-target-' + lang,
            'data-lang-' + lang
        ];


try {
        for (i = 0; i < keys.length; i += 1) {
setDevToolsStatus('JSZip 로딩 중...');
            value = data.getAttribute(keys[i]);
await loadExternalScriptOnce(JSZIP_URL, 'JSZip');
            if (value) return value;
        }


if (!window.JSZip) {
        return '';
throw new Error('JSZip을 사용할 수 없습니다.');
    }
}


var collected = await collectWikiAssets();
    function getLanguageStatusPages(registry) {
var zip = new JSZip();
        if (!registry || typeof registry !== 'object') return {};
        if (registry.pages && typeof registry.pages === 'object') return registry.pages;
        return registry;
    }


collected.files.forEach(function (file) {
    function readLanguageStatusEntry(registry, pageKey) {
zip.file(file.path, file.content);
        var pages = getLanguageStatusPages(registry);
});
        var underscored = String(pageKey || '').replace(/ /g, '_');


var manifest = {
        if (pages[pageKey] && typeof pages[pageKey] === 'object') return pages[pageKey];
createdAt: new Date().toISOString(),
        if (pages[underscored] && typeof pages[underscored] === 'object') return pages[underscored];
wiki: window.location.origin,
user: mw.config.get('wgUserName') || '',
startPages: START_PAGES,
included: collected.files.map(function (file) {
return {
title: file.title,
path: file.path,
bytes: new Blob([file.content]).size
};
}),
failed: collected.failed
};


zip.file('manifest.json', JSON.stringify(manifest, null, 2));
        return {};
    }


setDevToolsStatus('ZIP 압축 중...');
    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 = '';


var blob = await zip.generateAsync({
            if (!page || page.missing) return {};
type: 'blob',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
});


downloadBlob(blob, makeBackupFilename());
            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 (collected.failed.length) {
            if (!text) return {};
setDevToolsStatus('완료. 일부 문서는 manifest.json에 실패 기록됨.', 'error');
} else {
setDevToolsStatus('완료. CSS/JS 백업 ZIP 다운로드됨.', 'ok');
}
} catch (err) {
console.error(err);
setDevToolsStatus(err && err.message ? err.message : '백업 생성 실패', 'error');
} finally {
if (button) {
button.disabled = false;
button.textContent = 'CSS/JS ZIP 백업';
}
}
}


function ensureDeveloperToolsPanel() {
            try {
if (!isWikiOwner()) {
                return JSON.parse(text);
var existing = document.getElementById('dev-tools-panel');
            } catch (err) {
                setStatus('LanguageStatus JSON 파싱 실패', 'error');
                return {};
            }
        });
    }


if (existing) {
    function renderLanguageStatusEditor(registry) {
existing.remove();
        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 = '';


return;
        $('#dev-tools-language-page').text('PAGE: ' + pageKey);
}


if (document.getElementById('dev-tools-panel')) {
        LANGUAGE_ORDER.forEach(function (lang) {
return;
            var stored = normalizeLanguageStatusValue(entry[lang]);
}
            var fallback = getLanguageTargetTitle(lang) ? 'available' : 'unavailable';
            var value = stored || fallback;
            var isCurrent = lang === currentLang;


var collapsed = localStorage.getItem('dev-tools-collapsed') === 'true';
            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>';
        });


var panel = document.createElement('div');
        $('#dev-tools-language-list').html(html || 'NO DATA');
panel.id = 'dev-tools-panel';
    }


if (collapsed) {
    function refreshLanguageStatusEditor() {
panel.classList.add('dev-tools-collapsed');
        withApi(function (api) {
}
            setStatus('언어 상태 불러오는 중...');
            fetchLanguageStatusRegistry(api).then(function (registry) {
                renderLanguageStatusEditor(registry || {});
                setStatus('언어 상태 로드 완료', 'ok');
            }).catch(function () {
                renderLanguageStatusEditor({});
                setStatus('언어 상태 로드 실패', 'error');
            });
        }, function () {
            setStatus('mediawiki.api 로드 실패', 'error');
        });
    }


panel.innerHTML =
    function saveLanguageStatusEditor() {
'<div id="dev-tools-header">' +
        withApi(function (api) {
'<div id="dev-tools-title">' +
            var pageKey = getLanguageStatusPageKey();
'<span class="clbi-icon" style="--icon:var(--ic-ui-004);width:16px;height:16px;"></span>' +
'<span>Developer Tools</span>' +
'</div>' +
'<button type="button" id="dev-tools-toggle" aria-label="개발자 도구 접기">' + (collapsed ? '+' : '−') + '</button>' +
'</div>' +
'<div id="dev-tools-body">' +
'<div class="dev-tools-section">' +
'<button type="button" class="dev-tools-button" id="dev-tools-backup-assets">CSS/JS ZIP 백업</button>' +
'<div class="dev-tools-note">Common.css의 MediaWiki @import를 자동 추적하고 Common.js, Lang.js를 함께 묶습니다.</div>' +
'<div id="dev-tools-status"></div>' +
'</div>' +
'</div>';


document.body.appendChild(panel);
            setButtonsDisabled(true);
}
            setStatus('언어 상태 저장 중...');


$(function () {
            fetchLanguageStatusRegistry(api).then(function (registry) {
ensureDeveloperToolsPanel();
                var pages = getLanguageStatusPages(registry);
                var nextRegistry = registry && typeof registry === 'object' ? registry : {};
                var entry = {};


$(document)
                if (nextRegistry.pages && typeof nextRegistry.pages === 'object') {
.off('click.devToolsToggle')
                    pages = nextRegistry.pages;
.on('click.devToolsToggle', '#dev-tools-toggle', function (e) {
                } else {
e.preventDefault();
                    pages = nextRegistry;
e.stopPropagation();
                }


var panel = document.getElementById('dev-tools-panel');
                $('.dev-tools-language-select').each(function () {
                    var lang = $(this).data('lang');
                    var value = normalizeLanguageStatusValue($(this).val());


if (!panel) {
                    if (lang && value) {
return;
                        entry[lang] = value;
}
                    }
                });


var nextCollapsed = !panel.classList.contains('dev-tools-collapsed');
                pages[pageKey] = entry;


panel.classList.toggle('dev-tools-collapsed', nextCollapsed);
                return api.postWithToken('csrf', {
localStorage.setItem('dev-tools-collapsed', nextCollapsed ? 'true' : 'false');
                    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');


this.textContent = nextCollapsed ? '+' : '';
                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');
        });
    }


$(document)
    function bindEvents() {
.off('click.devToolsBackupAssets')
        $(document).on('click', '#dev-tools-panel .dev-tools-button-disabled', function (event) {
.on('click.devToolsBackupAssets', '#dev-tools-backup-assets', function (e) {
            event.preventDefault();
e.preventDefault();
            event.stopPropagation();
e.stopPropagation();
        });


downloadWikiAssetsZip();
        $(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 ? '+' : '−');
        });


// ========== Developer Tools Panel End ==========
        $(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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }


    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);