참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
// ========== 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.className = '';
if (type === 'error') {
status.classList.add('dev-tools-status-error');
}
if (type === 'ok') {
status.classList.add('dev-tools-status-ok');
}
status.textContent = text || '';
}
function loadExternalScriptOnce(url, globalName) {
return new Promise(function (resolve, reject) {
if (globalName && window[globalName]) {
resolve();
return;
}
var existing = document.querySelector('script[data-dev-tool-src="' + url + '"]');
if (existing) {
existing.addEventListener('load', function () {
resolve();
});
existing.addEventListener('error', function () {
reject(new Error('script load failed: ' + url));
});
return;
}
var script = document.createElement('script');
script.src = url;
script.async = true;
script.setAttribute('data-dev-tool-src', url);
script.onload = function () {
resolve();
};
script.onerror = function () {
reject(new Error('script load failed: ' + url));
};
document.head.appendChild(script);
});
}
function normalizeWikiTitle(title) {
return String(title || '')
.replace(/_/g, ' ')
.trim();
}
function titleToFilePath(title) {
var clean = normalizeWikiTitle(title).replace(/^MediaWiki:/i, '');
var lower = clean.toLowerCase();
if (lower.endsWith('.css')) {
return 'css/' + clean;
}
if (lower.endsWith('.js')) {
return 'js/' + clean;
}
return 'other/' + clean;
}
function fetchWikiPageSource(title) {
var api = new mw.Api();
return api.get({
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) {
throw new Error('missing page: ' + title);
}
var revision = page.revisions && page.revisions[0];
var content = '';
if (revision && revision.slots && revision.slots.main) {
content = revision.slots.main.content || '';
} else if (revision) {
content = revision.content || revision['*'] || '';
}
return {
title: normalizeWikiTitle(title),
content: content
};
});
}
function extractImportedMediaWikiTitles(source) {
var result = [];
var seen = {};
var regex = /@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?/gi;
var match;
while ((match = regex.exec(source)) !== null) {
var rawUrl = match[1];
var title = '';
try {
var parsed = new URL(rawUrl, window.location.origin);
title = parsed.searchParams.get('title') || '';
if (!title && parsed.pathname) {
var pathMatch = parsed.pathname.match(/\/index\.php\/(.+)$/);
if (pathMatch) {
title = decodeURIComponent(pathMatch[1]);
}
}
} catch (err) {
title = '';
}
title = normalizeWikiTitle(title);
if (!/^MediaWiki:/i.test(title)) {
continue;
}
if (!/\.(css|js)$/i.test(title)) {
continue;
}
if (seen[title]) {
continue;
}
seen[title] = true;
result.push(title);
}
return result;
}
async function collectWikiAssets() {
var queue = START_PAGES.slice();
var visited = {};
var files = [];
var failed = [];
while (queue.length) {
var title = normalizeWikiTitle(queue.shift());
if (!title || visited[title]) {
continue;
}
visited[title] = true;
setDevToolsStatus('수집 중: ' + title);
try {
var page = await fetchWikiPageSource(title);
files.push({
title: page.title,
path: titleToFilePath(page.title),
content: page.content
});
if (/\.css$/i.test(page.title)) {
var imports = extractImportedMediaWikiTitles(page.content);
imports.forEach(function (importedTitle) {
if (!visited[importedTitle]) {
queue.push(importedTitle);
}
});
}
} catch (err) {
failed.push({
title: title,
error: err && err.message ? err.message : String(err)
});
}
}
return {
files: files,
failed: failed
};
}
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 downloadBlob(blob, filename) {
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
setTimeout(function () {
URL.revokeObjectURL(url);
link.remove();
}, 1000);
}
async function downloadWikiAssetsZip() {
var button = document.getElementById('dev-tools-backup-assets');
if (button) {
button.disabled = true;
button.textContent = '백업 생성 중...';
}
try {
setDevToolsStatus('JSZip 로딩 중...');
await loadExternalScriptOnce(JSZIP_URL, 'JSZip');
if (!window.JSZip) {
throw new Error('JSZip을 사용할 수 없습니다.');
}
var collected = await collectWikiAssets();
var zip = new JSZip();
collected.files.forEach(function (file) {
zip.file(file.path, file.content);
});
var manifest = {
createdAt: new Date().toISOString(),
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));
setDevToolsStatus('ZIP 압축 중...');
var blob = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
});
downloadBlob(blob, makeBackupFilename());
if (collected.failed.length) {
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() {
if (!isWikiOwner()) {
var existing = document.getElementById('dev-tools-panel');
if (existing) {
existing.remove();
}
return;
}
if (document.getElementById('dev-tools-panel')) {
return;
}
var collapsed = localStorage.getItem('dev-tools-collapsed') === 'true';
var panel = document.createElement('div');
panel.id = 'dev-tools-panel';
if (collapsed) {
panel.classList.add('dev-tools-collapsed');
}
panel.innerHTML =
'<div id="dev-tools-header">' +
'<div id="dev-tools-title">' +
'<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);
}
$(function () {
ensureDeveloperToolsPanel();
$(document)
.off('click.devToolsToggle')
.on('click.devToolsToggle', '#dev-tools-toggle', function (e) {
e.preventDefault();
e.stopPropagation();
var panel = document.getElementById('dev-tools-panel');
if (!panel) {
return;
}
var nextCollapsed = !panel.classList.contains('dev-tools-collapsed');
panel.classList.toggle('dev-tools-collapsed', nextCollapsed);
localStorage.setItem('dev-tools-collapsed', nextCollapsed ? 'true' : 'false');
this.textContent = nextCollapsed ? '+' : '−';
});
$(document)
.off('click.devToolsBackupAssets')
.on('click.devToolsBackupAssets', '#dev-tools-backup-assets', function (e) {
e.preventDefault();
e.stopPropagation();
downloadWikiAssetsZip();
});
});
})();
// ========== Developer Tools Panel End ==========