참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
(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
};
var DOCUMENT_TOOLS_GROUPS = {
editor: true,
'senior-editor': true,
'devtools-access': true
};
var MANAGE_TOOLS_GROUPS = {
'senior-editor': true,
'devtools-access': true
};
function isOwner() {
return (mw.config.get('wgUserName') || '') === OWNER;
}
function hasAnyGroup(groupMap) {
var groups = mw.config.get('wgUserGroups') || [];
return groups.some(function (group) {
return !!groupMap[String(group || '')];
});
}
function hasDocumentToolsAccess() {
return isOwner() || hasAnyGroup(DOCUMENT_TOOLS_GROUPS);
}
function hasManageToolsAccess() {
return isOwner() || hasAnyGroup(MANAGE_TOOLS_GROUPS);
}
function hasDevToolsAccess() {
return hasDocumentToolsAccess() || hasManageToolsAccess();
}
if (!hasDevToolsAccess()) return;
if (window.DevToolsPanelInitialized) return;
window.DevToolsPanelInitialized = true;
var SOURCE_SEED_TITLES = [
'MediaWiki:Common.css',
'MediaWiki:Theme.css',
'MediaWiki:Layout.css',
'MediaWiki:MainPage.css',
'MediaWiki:Components.css',
'MediaWiki:Template.css',
'MediaWiki:Template.Infobox.css',
'MediaWiki:Icons.css',
'MediaWiki:Editor.css',
'MediaWiki:Dialogs.css',
'MediaWiki:DevTools.css',
'MediaWiki:AnecdoteViewer.css',
'MediaWiki:Test.css',
'MediaWiki:Common.js',
'MediaWiki:Lang.js',
'MediaWiki:DevTools.js',
'MediaWiki:CategoryNav.js',
'MediaWiki:AnecdoteViewer.js',
'MediaWiki:NationsGlobe.js',
'MediaWiki:NationsPanel.js',
'MediaWiki:Shortcuts.js',
'MediaWiki:1950_Nation_Link_Editor.js',
'MediaWiki:1950_Nation_Link_Editor.css',
'MediaWiki:1950_Nation_Link_Map.json',
'MediaWiki:Nation_List_Manager.js',
'MediaWiki:Nation_List_Manager.css',
'MediaWiki:1950_Nation_List.json',
'MediaWiki:LanguageStatus.json',
'Manage:1950_Nation_Link_Table',
'Manage:Nation_List'
];
var SOURCE_TEXT_EXTENSIONS = {
css: 'text/css',
js: 'text/javascript',
json: 'application/json',
geojson: 'application/geo+json',
topojson: 'application/json',
gpx: 'application/gpx+xml',
kml: 'application/vnd.google-earth.kml+xml',
txt: 'text/plain',
map: 'application/json',
lua: 'text/plain',
less: 'text/css',
scss: 'text/css',
xml: 'text/xml',
html: 'text/html',
htm: 'text/html',
md: 'text/plain',
csv: 'text/csv',
tsv: 'text/tab-separated-values',
yml: 'text/plain',
yaml: 'text/plain'
};
var SOURCE_FILE_EXTENSIONS = {
css: true,
js: true,
json: true,
geojson: true,
topojson: true,
gpx: true,
kml: true,
kmz: true,
gpkg: true,
shp: true,
shx: true,
dbf: true,
prj: true,
cpg: true,
txt: true,
map: true,
lua: true,
xml: true,
html: true,
htm: true,
md: true,
csv: true,
tsv: true,
yml: true,
yaml: true
};
var SOURCE_ALWAYS_NAMESPACE_IDS = {
8: true,
10: true,
828: true
};
var SOURCE_ALWAYS_NAMESPACE_NAMES = {
mediawiki: true,
template: true,
'틀': true,
module: true,
manage: true,
'관리': true
};
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, extraClass) {
var className = 'dev-tools-button dev-tools-link-button' + (extraClass ? ' ' + extraClass : '');
if (disabled || !href) {
return '<span class="' + escapeHtml(className + ' dev-tools-button-disabled') + '" aria-disabled="true">' + escapeHtml(label) + '</span>';
}
return '<a class="' + escapeHtml(className) + '" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>';
}
function renderDocumentToolButton(label, id, disabled, extraClass) {
var className = 'dev-tools-button' + (extraClass ? ' ' + extraClass : '');
if (disabled || !id) {
return '<span class="' + escapeHtml(className + ' dev-tools-button-disabled') + '" aria-disabled="true">' + escapeHtml(label) + '</span>';
}
return '<button type="button" class="' + escapeHtml(className) + '" id="' + escapeHtml(id) + '">' + escapeHtml(label) + '</button>';
}
function normalizeMoveTitle(value) {
var text = String(value || '')
.split('#')[0]
.split('?')[0]
.trim();
text = text.replace(/^https?:\/\/[^/]+\/index\.php\/?/i, '');
text = text.replace(/^\/index\.php\/?/i, '');
try {
text = decodeURIComponent(text);
} catch (err) {
// Keep the original text if it is not a valid encoded title.
}
return text.replace(/_/g, ' ').trim();
}
function getMoveNamespacePrefix(title) {
var text = normalizeMoveTitle(title);
var index = text.indexOf(':');
if (index <= 0) return '';
return text.slice(0, index);
}
function normalizeMoveTargetTitle(fromTitle, targetTitle) {
var from = normalizeMoveTitle(fromTitle);
var target = normalizeMoveTitle(targetTitle);
var prefix;
if (!target) return '';
if (target.indexOf(':') !== -1) return target;
prefix = getMoveNamespacePrefix(from);
return prefix ? prefix + ':' + target : target;
}
function normalizeMoveCompare(value) {
return normalizeMoveTitle(value).replace(/_/g, ' ').trim().toLowerCase();
}
function isCurrentMoveSource(fromTitle) {
return normalizeMoveCompare(fromTitle) === normalizeMoveCompare(getDocumentToolPageName());
}
function getMoveTitleBody(title) {
var text = normalizeMoveTitle(title);
var index = text.indexOf(':');
return index > 0 ? text.slice(index + 1) : text;
}
function normalizeApiTitle(title) {
return normalizeMoveTitle(title).replace(/ /g, '_');
}
function escapeRegExp(value) {
return String(value == null ? '' : value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function uniqueStrings(values) {
var seen = {};
var result = [];
values.forEach(function (value) {
value = String(value || '').trim();
if (!value || seen[value]) return;
seen[value] = true;
result.push(value);
});
return result;
}
function isFileTitle(title) {
var prefix = getMoveNamespacePrefix(title).toLowerCase();
return prefix === 'file' || prefix === '파일';
}
function getFileTitleAliases(title) {
var body = getMoveTitleBody(title);
if (!body) return [];
return uniqueStrings([
'파일:' + body,
'File:' + body,
'file:' + body,
'Image:' + body,
'image:' + body
]);
}
function getTitleReferenceVariants(title) {
var normalized = normalizeMoveTitle(title);
var underscored = normalized.replace(/ /g, '_');
var variants = [normalized, underscored];
if (isFileTitle(normalized)) {
variants = variants.concat(getFileTitleAliases(normalized));
getFileTitleAliases(normalized).forEach(function (alias) {
variants.push(alias.replace(/ /g, '_'));
});
}
return uniqueStrings(variants).sort(function (a, b) {
return b.length - a.length;
});
}
function getTitleReferenceReplacement(fromVariant, fromTitle, toTitle) {
var fromPrefix = getMoveNamespacePrefix(fromVariant);
var toBody = getMoveTitleBody(toTitle);
var normalizedTo = normalizeMoveTitle(toTitle);
var replacement;
if (isFileTitle(fromVariant) && toBody) {
replacement = fromPrefix ? fromPrefix + ':' + toBody : normalizedTo;
} else {
replacement = normalizedTo;
}
if (fromVariant.indexOf('_') !== -1) {
replacement = replacement.replace(/ /g, '_');
}
return replacement;
}
function replaceAllText(value, from, to) {
return String(value).split(from).join(to);
}
function replaceRenameReferences(text, fromTitle, toTitle, includePlainText) {
var next = String(text || '');
var variants = getTitleReferenceVariants(fromTitle);
var changed = false;
variants.forEach(function (variant) {
var replacement = getTitleReferenceReplacement(variant, fromTitle, toTitle);
var before = next;
var escaped = escapeRegExp(variant).replace(/[ _]+/g, '[ _]+');
next = next.replace(new RegExp('(\\[\\[\\s*)' + escaped + '(?=\\s*(?:[\\]#|]))', 'gi'), '$1' + replacement);
next = next.replace(new RegExp('(\\[\\[\\s*)' + escaped + '(?=\\s*\\|)', 'gi'), '$1' + replacement);
if (includePlainText) {
next = replaceAllText(next, variant, replacement);
}
if (next !== before) changed = true;
});
return {
text: next,
changed: changed || next !== String(text || '')
};
}
function isRenameSourceLikeTitle(title) {
var ext = getTitleExtension(title);
var nsName = normalizeNamespaceName(getTitleNamespaceName(title));
if (SOURCE_TEXT_EXTENSIONS[ext]) return true;
if (SOURCE_ALWAYS_NAMESPACE_NAMES[nsName]) return true;
return false;
}
function queryRenameReferencePages(api, fromTitle) {
var normalized = normalizeApiTitle(fromTitle);
var requests = [
api.get({
action: 'query',
list: 'backlinks',
bltitle: normalized,
bllimit: 'max',
blfilterredir: 'nonredirects',
format: 'json',
formatversion: 2
}).then(function (data) {
var list = data && data.query ? data.query.backlinks : [];
return (list || []).map(function (item) { return item.title; });
}).catch(function () { return []; })
];
if (isFileTitle(fromTitle)) {
requests.push(api.get({
action: 'query',
list: 'imageusage',
iutitle: normalized,
iulimit: 'max',
format: 'json',
formatversion: 2
}).then(function (data) {
var list = data && data.query ? data.query.imageusage : [];
return (list || []).map(function (item) { return item.title; });
}).catch(function () { return []; }));
}
return Promise.all(requests).then(function (chunks) {
var titles = [];
chunks.forEach(function (chunk) {
titles = titles.concat(chunk || []);
});
if (hasManageToolsAccess()) {
titles = titles.concat(SOURCE_SEED_TITLES);
}
titles.push(getDocumentToolPageName());
return uniqueStrings(titles).filter(function (title) {
return normalizeMoveCompare(title) !== normalizeMoveCompare(fromTitle);
});
});
}
function fetchRenamePageText(api, title) {
return api.get({
action: 'query',
prop: 'revisions|info',
titles: normalizeApiTitle(title),
rvprop: 'content|ids',
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 null;
if (page.ns === 6) return null;
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 || '';
}
}
return {
title: page.title || title,
text: text,
ns: page.ns,
contentmodel: page.contentmodel || '',
revid: page.revisions && page.revisions[0] ? page.revisions[0].revid : null
};
});
}
function editRenameReferencePage(api, page, nextText, fromTitle, toTitle) {
var payload = {
action: 'edit',
title: page.title,
text: nextText,
summary: 'Update references after rename: ' + fromTitle + ' → ' + toTitle,
minor: 1,
nocreate: 1,
format: 'json',
formatversion: 2,
errorformat: 'plaintext'
};
if (page.contentmodel) payload.contentmodel = page.contentmodel;
if (page.revid) payload.baserevid = page.revid;
return api.postWithToken('csrf', payload);
}
function updateRenameReferences(api, fromTitle, toTitle) {
var checked = $('#dev-tools-rename-update-references').prop('checked');
if (!checked) {
return Promise.resolve({ scanned: 0, changed: 0, failed: 0 });
}
return queryRenameReferencePages(api, fromTitle).then(function (titles) {
var total = titles.length;
var index = 0;
var changed = 0;
var failed = 0;
var chain = Promise.resolve();
titles.forEach(function (title) {
chain = chain.then(function () {
index += 1;
setStatus('참조 정리 중... ' + index + '/' + total);
return fetchRenamePageText(api, title).then(function (page) {
var result;
var includePlainText;
if (!page) return;
includePlainText = isRenameSourceLikeTitle(page.title);
result = replaceRenameReferences(page.text, fromTitle, toTitle, includePlainText);
if (!result.changed) return;
return editRenameReferencePage(api, page, result.text, fromTitle, toTitle).then(function () {
changed += 1;
});
}).catch(function () {
failed += 1;
});
});
});
return chain.then(function () {
return { scanned: total, changed: changed, failed: failed };
});
});
}
function isNationsDocumentPage() {
var pageName = normalizeDocumentToolPageName(getDocumentToolPageName());
var title = normalizeDocumentToolPageName(mw.config.get('wgTitle') || '');
var candidates = [pageName, title];
return candidates.some(function (value) {
value = String(value || '').replace(/_/g, ' ').trim().toLowerCase();
return value === '국가 및 조합' || value === 'nations & factions' || value === 'nations and factions';
});
}
function buildNationsManageToolsHtml() {
if (!hasManageToolsAccess()) return '';
if (!isNationsDocumentPage()) return '';
return '' +
'<div class="dev-tools-section dev-tools-nations-manage-section">' +
'<div class="dev-tools-section-title">NATIONS MANAGE</div>' +
'<div class="dev-tools-grid dev-tools-doc-grid" id="dev-tools-nations-manage-grid">' +
renderDocumentToolLink('1950 링크표', getDocumentToolUrl('Manage:1950_Nation_Link_Table'), false) +
renderDocumentToolLink('국가 목록', getDocumentToolUrl('Manage:Nation_List'), false) +
'</div>' +
'<div class="dev-tools-note">국가 및 조합 전용 관리 페이지로 이동합니다.</div>' +
'</div>';
}
function buildOwnerToolsHtml() {
if (!isOwner()) return '';
return '' +
'<div class="dev-tools-section dev-tools-owner-section">' +
'<div class="dev-tools-section-title">OWNER</div>' +
'<div class="dev-tools-grid dev-tools-doc-grid" id="dev-tools-owner-grid">' +
renderDocumentToolLink('사용자 권한', getDocumentToolUrl('Special:UserRights'), false) +
'</div>' +
'<div class="dev-tools-note">사용자 그룹과 권한을 관리합니다.</div>' +
'</div>';
}
function buildSourcePackageToolsHtml() {
if (!isOwner()) return '';
return '' +
'<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">위키 소스 ZIP 다운로드</button>' +
'<div class="dev-tools-note">위키 전체를 스캔해 CSS·JS·JSON·GeoJSON·Template·Manage·Module 원본과 source-like 파일을 ZIP으로 묶습니다. SVG는 제외합니다.</div>' +
'</div>';
}
function buildProgressToolsHtml() {
if (!isOwner()) return '';
return '' +
'<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>';
}
function buildLanguageStatusToolsHtml() {
if (!isOwner()) return '';
return '' +
'<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>';
}
function buildDocumentToolsHtml() {
var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
var pageName = getDocumentToolPageName();
var displayTitle = getDocumentToolDisplayTitle();
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(displayTitle) + '</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) +
renderDocumentToolButton('이름변경', 'dev-tools-rename-toggle', isSpecial) +
renderDocumentToolLink('삭제', getDocumentToolUrl(pageName, { action: 'delete' }), isSpecial, 'dev-tools-danger') +
'</div>' +
'<div class="dev-tools-rename-box" id="dev-tools-rename-box" hidden>' +
'<label class="dev-tools-label" for="dev-tools-rename-from">현재 이름</label>' +
'<input id="dev-tools-rename-from" class="dev-tools-input" type="text" value="' + escapeHtml(displayTitle) + '">' +
'<label class="dev-tools-label" for="dev-tools-rename-to">새 이름</label>' +
'<input id="dev-tools-rename-to" class="dev-tools-input" type="text" placeholder="새 문서명 또는 파일명">' +
'<label class="dev-tools-label" for="dev-tools-rename-reason">사유</label>' +
'<input id="dev-tools-rename-reason" class="dev-tools-input" type="text" value="이름 변경">' +
'<label class="dev-tools-label"><input id="dev-tools-rename-legacy" type="checkbox" checked> 기존 이름 레거시 유지</label>' +
'<label class="dev-tools-label"><input id="dev-tools-rename-update-references" type="checkbox" checked> 사용처와 설정 참조 정리</label>' +
'<label class="dev-tools-label"><input id="dev-tools-rename-talk" type="checkbox" checked> 토론 문서도 같이 이름 변경</label>' +
'<label class="dev-tools-label"><input id="dev-tools-rename-subpages" type="checkbox"> 하위 문서도 같이 이름 변경</label>' +
'<button type="button" class="dev-tools-button" id="dev-tools-rename-submit">실제 이름 변경 실행</button>' +
'<div class="dev-tools-note">문서/파일의 실제 제목을 새 이름으로 바꾸고, 기존 이름은 넘겨주기 레거시 경로로 남깁니다. MediaWiki 내부 처리는 이름 변경 API로 수행됩니다.</div>' +
'</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">' +
buildOwnerToolsHtml() +
buildDocumentToolsHtml() +
buildNationsManageToolsHtml() +
buildSourcePackageToolsHtml() +
buildProgressToolsHtml() +
buildLanguageStatusToolsHtml() +
'<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 getTitleExtension(title) {
var match = String(title || '').split('?')[0].split('#')[0].match(/\.([A-Za-z0-9]+)$/);
return match ? match[1].toLowerCase() : '';
}
function normalizeNamespaceName(name) {
return String(name || '').replace(/_/g, ' ').trim().toLowerCase();
}
function getTitleNamespaceName(title) {
var text = String(title || '');
var idx = text.indexOf(':');
return idx === -1 ? '' : text.slice(0, idx);
}
function getTitleBody(title) {
var text = String(title || '');
var idx = text.indexOf(':');
return idx === -1 ? text : text.slice(idx + 1);
}
function sanitizePathPart(value) {
var text = String(value == null ? '' : value).trim();
if (!text) return '_';
return text
.replace(/[\\:*?"<>|]/g, '_')
.replace(/\s+/g, ' ')
.replace(/^\.+$/, '_')
.replace(/^\.+/, '')
.replace(/\.+$/, '') || '_';
}
function sanitizeTitlePath(value) {
return String(value || '')
.replace(/_/g, ' ')
.split('/')
.map(sanitizePathPart)
.join('/');
}
function namespaceFolder(source) {
var nsName = source.namespaceName || getTitleNamespaceName(source.title);
var normalized = normalizeNamespaceName(nsName);
if (source.ns === 6 || normalized === 'file' || normalized === '파일') return 'file';
if (source.ns === 8 || normalized === 'mediawiki') return 'mediawiki';
if (source.ns === 10 || normalized === 'template' || normalized === '틀') return 'template';
if (source.ns === 828 || normalized === 'module') return 'module';
if (normalized === 'manage' || normalized === '관리') return 'manage';
if (!normalized) return 'page';
return sanitizePathPart(normalized);
}
function contentTypeForTitle(title) {
return SOURCE_TEXT_EXTENSIONS[getTitleExtension(title)] || 'text/plain';
}
function outputPathForSource(source) {
var title = source.title || '';
var body = getTitleBody(title);
var ext = getTitleExtension(title);
var folder;
if ((source.ns === 8 || normalizeNamespaceName(getTitleNamespaceName(title)) === 'mediawiki') && ext === 'css') {
return 'css/' + sanitizeTitlePath(body);
}
if ((source.ns === 8 || normalizeNamespaceName(getTitleNamespaceName(title)) === 'mediawiki') && ext === 'js') {
return 'js/' + sanitizeTitlePath(body);
}
folder = namespaceFolder(source);
return folder + '/' + sanitizeTitlePath(body || title);
}
function rawUrl(file) {
return mw.util.getUrl(file.title, {
action: 'raw',
ctype: file.ctype || contentTypeForTitle(file.title)
});
}
function fetchRaw(file) {
return fetch(rawUrl(file), { credentials: 'same-origin', cache: 'no-cache' }).then(function (res) {
if (!res.ok) throw new Error(file.title + ' HTTP ' + res.status);
return res.text();
});
}
function fetchFileBlob(api, file) {
return api.get({
action: 'query',
prop: 'imageinfo',
titles: file.title,
iiprop: 'url|mime|size',
format: 'json',
formatversion: 2
}).then(function (data) {
var pages = data && data.query ? data.query.pages : [];
var page = pages && pages.length ? pages[0] : null;
var info = page && page.imageinfo && page.imageinfo.length ? page.imageinfo[0] : null;
if (!info || !info.url) throw new Error(file.title + ' file url not found');
return fetch(info.url, { credentials: 'same-origin', cache: 'no-cache' }).then(function (res) {
if (!res.ok) throw new Error(file.title + ' HTTP ' + res.status);
return res.blob();
});
});
}
function isFileSourcePage(page, ext) {
return (page.ns === 6 || normalizeNamespaceName(getTitleNamespaceName(page.title)) === 'file' || normalizeNamespaceName(getTitleNamespaceName(page.title)) === '파일') && !!SOURCE_FILE_EXTENSIONS[ext];
}
function shouldExportSourcePage(page, namespaceInfo) {
var title = page && page.title ? page.title : '';
var ext = getTitleExtension(title);
var nsName = namespaceInfo && namespaceInfo.namesById ? namespaceInfo.namesById[page.ns] : getTitleNamespaceName(title);
var normalized = normalizeNamespaceName(nsName || getTitleNamespaceName(title));
if (!title) return false;
if (ext === 'svg') return false;
if (isFileSourcePage(page, ext)) return true;
if (SOURCE_TEXT_EXTENSIONS[ext]) return true;
if (SOURCE_ALWAYS_NAMESPACE_IDS[page.ns]) return true;
if (SOURCE_ALWAYS_NAMESPACE_NAMES[normalized]) return true;
return false;
}
function sourceFromTitle(title, namespaceInfo) {
var nsName = getTitleNamespaceName(title);
var normalizedNs = normalizeNamespaceName(nsName);
var ns = null;
if (namespaceInfo && namespaceInfo.idsByName && normalizedNs && namespaceInfo.idsByName.hasOwnProperty(normalizedNs)) {
ns = namespaceInfo.idsByName[normalizedNs];
}
return {
title: title,
ns: ns,
namespaceName: nsName,
ctype: contentTypeForTitle(title),
kind: (ns === 6 || normalizedNs === 'file' || normalizedNs === '파일') ? 'file' : 'page'
};
}
function canonicalSourceKey(source) {
var title = String(source && source.title ? source.title : '').replace(/_/g, ' ').trim();
var body = getTitleBody(title).replace(/_/g, ' ').trim();
var ns = source && source.ns != null ? String(source.ns) : '';
var nsName = normalizeNamespaceName(source && source.namespaceName ? source.namespaceName : getTitleNamespaceName(title));
if (!title) return '';
/*
* Namespace aliases can expose the same page twice during whole-wiki scan.
* Example: MediaWiki:Common.css from the seed list and the localized namespace
* title returned by allpages. Deduplicate by namespace id + title body, not by
* the literal namespace prefix, so those aliases do not become Common-2.css.
*/
if (ns) {
return 'ns:' + ns + ':' + body.toLowerCase();
}
return 'title:' + (nsName ? nsName + ':' : '') + body.toLowerCase();
}
function uniqueSources(sources) {
var seen = {};
var outPaths = {};
var result = [];
sources.forEach(function (source) {
var key = canonicalSourceKey(source);
var basePath;
var path;
var ext;
var n;
if (!key || seen[key]) return;
seen[key] = true;
source.ctype = source.ctype || contentTypeForTitle(source.title);
source.kind = source.kind || (source.ns === 6 ? 'file' : 'page');
basePath = outputPathForSource(source);
path = basePath;
n = 2;
while (outPaths[path]) {
ext = path.match(/(\.[^/.]+)$/);
if (ext) {
path = basePath.slice(0, -ext[1].length) + '-' + n + ext[1];
} else {
path = basePath + '-' + n;
}
n += 1;
}
outPaths[path] = true;
source.out = path;
result.push(source);
});
return result.sort(function (a, b) {
return a.out.localeCompare(b.out);
});
}
function getSourceNamespaces(api) {
return api.get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
format: 'json',
formatversion: 2
}).then(function (data) {
var raw = data && data.query ? data.query.namespaces : {};
var list = Array.isArray(raw) ? raw : Object.keys(raw || {}).map(function (key) { return raw[key]; });
var ids = [];
var namesById = {};
var idsByName = {};
list.forEach(function (item) {
var id = parseInt(item.id != null ? item.id : item.ns, 10);
var name = item.name || item.canonical || '';
if (!Number.isFinite(id) || id < 0) return;
ids.push(id);
namesById[id] = name;
idsByName[normalizeNamespaceName(name)] = id;
if (item.canonical) idsByName[normalizeNamespaceName(item.canonical)] = id;
});
ids.sort(function (a, b) { return a - b; });
return {
ids: ids,
namesById: namesById,
idsByName: idsByName
};
});
}
function fetchAllPagesInNamespace(api, ns) {
var pages = [];
function next(apcontinue) {
var params = {
action: 'query',
list: 'allpages',
apnamespace: ns,
apfilterredir: 'nonredirects',
aplimit: 'max',
format: 'json',
formatversion: 2
};
if (apcontinue) params.apcontinue = apcontinue;
return api.get(params).then(function (data) {
var chunk = data && data.query ? data.query.allpages : [];
if (chunk && chunk.length) pages = pages.concat(chunk);
if (data && data.continue && data.continue.apcontinue) {
return next(data.continue.apcontinue);
}
return pages;
});
}
return next();
}
function scanSourcePages(api, onProgress) {
return getSourceNamespaces(api).then(function (namespaceInfo) {
var all = [];
var chain = Promise.resolve();
var index = 0;
namespaceInfo.ids.forEach(function (ns) {
chain = chain.then(function () {
index += 1;
if (onProgress) onProgress('스캔 중: namespace ' + ns + ' (' + index + '/' + namespaceInfo.ids.length + ')');
return fetchAllPagesInNamespace(api, ns).then(function (pages) {
pages.forEach(function (page) {
var ext = getTitleExtension(page.title);
var source;
if (!shouldExportSourcePage(page, namespaceInfo)) return;
source = {
title: page.title,
ns: page.ns,
namespaceName: namespaceInfo.namesById[page.ns] || getTitleNamespaceName(page.title),
ctype: contentTypeForTitle(page.title),
kind: isFileSourcePage(page, ext) ? 'file' : 'page'
};
all.push(source);
});
});
});
});
return chain.then(function () {
SOURCE_SEED_TITLES.forEach(function (title) {
all.push(sourceFromTitle(title, namespaceInfo));
});
return uniqueSources(all);
});
});
}
function mapLimit(items, limit, iterator) {
var index = 0;
var active = 0;
var done = 0;
var results = [];
return new Promise(function (resolve) {
function launch() {
var current;
while (active < limit && index < items.length) {
current = index;
index += 1;
active += 1;
Promise.resolve(iterator(items[current], current)).then(function (result) {
results[current] = result;
}).catch(function (err) {
results[current] = { error: err };
}).finally(function () {
active -= 1;
done += 1;
if (done >= items.length) {
resolve(results);
} else {
launch();
}
});
}
if (!items.length) resolve(results);
}
launch();
});
}
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('위키 소스 스캔 준비 중...');
loadScript(JSZIP_URL)
.then(function () {
return new Promise(function (resolve, reject) {
withApi(function (api) {
resolve(api);
}, function () {
reject(new Error('mediawiki.api 로드 실패'));
});
});
})
.then(function (api) {
var zip = new JSZip();
var meta = [];
var errors = [];
return scanSourcePages(api, setStatus).then(function (sources) {
setStatus('소스 수집 중: 0/' + sources.length);
return mapLimit(sources, 6, function (file, index) {
var fetcher = file.kind === 'file' ? fetchFileBlob(api, file) : fetchRaw(file);
return fetcher.then(function (payload) {
zip.file(file.out, payload);
meta.push({
title: file.title,
out: file.out,
kind: file.kind,
ns: file.ns,
ctype: file.ctype
});
if ((index + 1) % 5 === 0 || index + 1 === sources.length) {
setStatus('소스 수집 중: ' + (index + 1) + '/' + sources.length);
}
}).catch(function (err) {
var message = String(err && err.message ? err.message : err);
var errorPath = '_errors/' + sanitizeTitlePath(file.title) + '.ERROR.txt';
zip.file(errorPath, message);
errors.push({
title: file.title,
out: file.out,
error: message
});
});
}).then(function () {
var sortedFiles = meta.slice().sort(function (a, b) { return a.out.localeCompare(b.out); });
var readme = 'CLBI Wiki source export\n' +
'Generated: ' + new Date().toISOString() + '\n' +
'Mode: whole-wiki source scan\n' +
'Included: ' + sortedFiles.length + '\n' +
'Errors: ' + errors.length + '\n\n' +
'Scope:\n' +
'- CSS / JS pages in any namespace\n' +
'- JSON / GeoJSON / TopoJSON and other source files, excluding SVG\n' +
'- MediaWiki / Template / Module / Manage namespace source pages\n' +
'- Source-like files in File namespace, including GeoJSON/GPKG/shapefile sidecars when present\n' +
'- Seeded known CLBI source pages are added even if namespace scan misses them\n\n' +
'Files:\n' + sortedFiles.map(function (item) {
return item.out + ' <= ' + item.title;
}).join('\n') + '\n';
zip.file('README.txt', readme);
zip.file('manifest.json', JSON.stringify({
generated: new Date().toISOString(),
mode: 'whole-wiki-source-scan',
included: sortedFiles.length,
errors: errors,
files: sortedFiles
}, null, 2) + '\n');
setStatus('ZIP 압축 중...');
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 toggleRenameBox() {
var box = document.getElementById('dev-tools-rename-box');
var targetInput;
if (!box) return;
box.hidden = !box.hidden;
if (!box.hidden) {
targetInput = document.getElementById('dev-tools-rename-to');
if (targetInput) targetInput.focus();
}
}
function runDocumentRename() {
var fromTitle = normalizeMoveTitle($('#dev-tools-rename-from').val());
var toTitle = normalizeMoveTargetTitle(fromTitle, $('#dev-tools-rename-to').val());
var reason = String($('#dev-tools-rename-reason').val() || '이름 변경').trim();
var keepLegacy = $('#dev-tools-rename-legacy').prop('checked');
var moveTalk = $('#dev-tools-rename-talk').prop('checked');
var moveSubpages = $('#dev-tools-rename-subpages').prop('checked');
var shouldUpdateReferences = $('#dev-tools-rename-update-references').prop('checked');
var payload;
if (!fromTitle) {
setStatus('현재 이름이 비어 있습니다.', 'error');
return;
}
if (!toTitle) {
setStatus('새 이름이 비어 있습니다.', 'error');
return;
}
if (normalizeMoveCompare(fromTitle) === normalizeMoveCompare(toTitle)) {
setStatus('현재 이름과 새 이름이 같습니다.', 'error');
return;
}
if (!window.confirm('실제 이름을 변경할까요?\n\n' + fromTitle + '\n→ ' + toTitle + (keepLegacy ? '\n\n기존 이름은 레거시 넘겨주기로 유지됩니다.' : '\n\n기존 이름 레거시는 남기지 않습니다.'))) {
return;
}
payload = {
action: 'move',
from: fromTitle,
to: toTitle,
reason: reason,
format: 'json',
formatversion: 2,
errorformat: 'plaintext'
};
if (moveTalk) payload.movetalk = 1;
if (moveSubpages) payload.movesubpages = 1;
if (!keepLegacy) payload.noredirect = 1;
setButtonsDisabled(true);
setStatus('실제 이름 변경 중...');
withApi(function (api) {
api.postWithToken('csrf', payload).then(function (data) {
var moved = data && data.move;
var movedTo = moved && (moved.to || toTitle);
if (!shouldUpdateReferences) {
return { movedTo: movedTo, referenceResult: null };
}
return updateRenameReferences(api, fromTitle, movedTo || toTitle).then(function (referenceResult) {
return { movedTo: movedTo, referenceResult: referenceResult };
});
}).then(function (result) {
var movedTo = result && result.movedTo ? result.movedTo : toTitle;
var referenceResult = result && result.referenceResult;
var message = '이름 변경 완료: ' + movedTo;
if (referenceResult) {
message += ' / 참조 ' + referenceResult.changed + '개 정리';
if (referenceResult.failed) message += ' / 실패 ' + referenceResult.failed + '개';
}
setStatus(message, referenceResult && referenceResult.failed ? 'error' : 'ok');
if (isCurrentMoveSource(fromTitle)) {
window.setTimeout(function () {
window.location.href = getDocumentToolUrl(movedTo);
}, 700);
} else {
setButtonsDisabled(false);
$('#dev-tools-rename-from').val(movedTo);
$('#dev-tools-rename-to').val('');
}
}).catch(function (code, details) {
var message = code;
if (details && details.error && details.error.info) {
message = details.error.info;
} else if (details && details.info) {
message = details.info;
} else if (code && code.message) {
message = code.message;
}
setButtonsDisabled(false);
setStatus('이름 변경 실패: ' + message, 'error');
});
}, 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-rename-toggle', toggleRenameBox);
$(document).on('click', '#dev-tools-rename-submit', runDocumentRename);
$(document).on('keydown', '#dev-tools-rename-to', function (event) {
if (event.key === 'Enter') runDocumentRename();
});
$(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();
if (isOwner()) {
refreshProgressSummary();
refreshLanguageStatusEditor();
}
if (mw.hook) {
mw.hook('wikipage.content').add(refreshDocumentTools);
}
});
})(mediaWiki, jQuery);