참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
(function () {
'use strict';
var DEFAULT_YEARS = [
'1950', '1960', '1970', '1980', '1990', '2000', '2010', '2020',
'2030', '2040', '2050', '2060', '2070', '2080', '2090', '2100'
];
var CONTINENT_TEMPLATE = [
{ key: 'america', label: '아메리카', regions: ['북아메리카', '남아메리카'] },
{ key: 'europe', label: '유럽', regions: ['북유럽', '서유럽', '중앙유럽', '동유럽', '동남유럽', '남유럽', '남서유럽'] },
{ key: 'africa', label: '아프리카', regions: ['북아프리카', '서아프리카', '중앙아프리카', '동아프리카', '남아프리카'] },
{ key: 'asia', label: '아시아', regions: ['서아시아', '중앙아시아', '북아시아', '남아시아', '동아시아', '동남아시아'] },
{ key: 'oceania', label: '오세아니아', regions: ['오세아니아'] },
{ key: 'antarctica', label: '남극', regions: ['남극'] }
];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function encodeWikiTitle(title) {
return String(title || '')
.trim()
.replace(/ /g, '_')
.split('/')
.map(function (part) { return encodeURIComponent(part); })
.join('/');
}
function replaceYearTemplate(template, year) {
return String(template || '').replace(/\{year\}/g, String(year || ''));
}
function normalizeFileTitle(value) {
return String(value || '')
.trim()
.replace(/^\[\[:?\s*/i, '')
.replace(/\]\]$/i, '')
.replace(/^파일\s*:/i, '')
.replace(/^File\s*:/i, '')
.replace(/^이미지\s*:/i, '')
.replace(/^Image\s*:/i, '')
.replace(/^:+/, '')
.trim();
}
function buildFileRedirectUrl(value) {
var file = normalizeFileTitle(value);
return file ? '/index.php/Special:Redirect/file/' + encodeWikiTitle(file) : '';
}
function normalizeRegionKey(label) {
return String(label || '')
.trim()
.replace(/\s+/g, '_')
.replace(/[^A-Za-z0-9가-힣_\-]/g, '') || 'region';
}
function makeEmptyState(year) {
return {
version: 1,
year: parseInt(year || '1950', 10),
description: '국가 및 조합 ' + year + '년 당대 존재 국가 패널 데이터. Manage:Nation_List에서 편집한다.',
continents: CONTINENT_TEMPLATE.map(function (continent) {
return {
key: continent.key,
label: continent.label,
regions: continent.regions.map(function (region) {
return {
key: normalizeRegionKey(region),
label: region,
items: []
};
})
};
})
};
}
function normalizeFlag(flag) {
if (!flag || typeof flag !== 'object') flag = {};
return {
file: normalizeFileTitle(flag.file || flag.flag_file || flag.flag || ''),
width: String(flag.width || flag.flag_width || '16px').trim() || '16px',
border: flag.border !== false,
decorator: flag.decorator === 'top' ? 'top' : ''
};
}
function normalizeItem(item) {
var flags;
item = item && typeof item === 'object' ? item : {};
flags = Array.isArray(item.flags) ? item.flags.map(normalizeFlag) : [];
if (!flags.length && (item.flag_file || item.flag)) {
flags.push(normalizeFlag({ file: item.flag_file || item.flag, width: item.flag_width || '16px', border: item.border !== false, decorator: item.decorator || '' }));
}
return {
page: String(item.page || item.wiki_title || '').trim(),
label: String(item.label || item.name || item.name_ko || item.page || '').trim(),
external_url: String(item.external_url || item.url || '').trim(),
flags: flags,
note: String(item.note || '').trim(),
enabled: item.enabled !== false
};
}
function normalizeState(payload, year) {
var state = makeEmptyState(year);
var byKey = {};
if (!payload || typeof payload !== 'object') return state;
if (Array.isArray(payload.continents)) {
payload.continents.forEach(function (continent) {
if (continent && continent.key) byKey[continent.key] = continent;
});
}
state.version = payload.version || 1;
state.year = payload.year || state.year;
state.description = payload.description || state.description;
state.continents = state.continents.map(function (baseContinent) {
var source = byKey[baseContinent.key] || {};
var regionsByKey = {};
if (Array.isArray(source.regions)) {
source.regions.forEach(function (region) {
if (!region) return;
if (region.key) regionsByKey[region.key] = region;
if (region.label) regionsByKey[normalizeRegionKey(region.label)] = region;
});
}
return {
key: baseContinent.key,
label: source.label || baseContinent.label,
regions: baseContinent.regions.map(function (baseRegion) {
var region = regionsByKey[baseRegion.key] || regionsByKey[normalizeRegionKey(baseRegion.label)] || {};
return {
key: region.key || baseRegion.key,
label: region.label || baseRegion.label,
items: Array.isArray(region.items) ? region.items.map(normalizeItem) : []
};
})
};
});
return state;
}
function getItemPrimaryFlag(item) {
if (!item.flags || !item.flags.length) item.flags = [normalizeFlag({})];
return item.flags[0];
}
function normalizeMapPages(payload) {
var items = payload && payload.items ? payload.items : {};
var pages = {};
Object.keys(items).forEach(function (key) {
var entry = items[key] || {};
var page = String(entry.page || key || '').trim();
if (page) pages[page] = true;
});
return pages;
}
function NationListManager(root) {
this.root = root;
this.years = String(root.getAttribute('data-years') || DEFAULT_YEARS.join(' ')).split(/[\s,|]+/).filter(Boolean);
this.year = root.getAttribute('data-year') || this.years[0] || '1950';
this.titleTemplate = root.getAttribute('data-list-title-template') || 'MediaWiki:{year}_Nation_List.json';
this.urlTemplate = root.getAttribute('data-list-url-template') || '/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json';
this.mapTitleTemplate = root.getAttribute('data-map-title-template') || 'MediaWiki:{year}_Nation_Link_Map.json';
this.mapUrlTemplate = root.getAttribute('data-map-url-template') || '/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json';
this.baseUrl = root.getAttribute('data-link-base') || '/index.php/';
this.state = null;
this.mapPages = null;
this.activeContinent = 'america';
this.query = '';
this.status = '';
this.sourceExists = false;
this.loading = false;
}
NationListManager.prototype.getListTitle = function () {
return replaceYearTemplate(this.titleTemplate, this.year);
};
NationListManager.prototype.getListUrl = function () {
return replaceYearTemplate(this.urlTemplate, this.year);
};
NationListManager.prototype.getMapUrl = function () {
return replaceYearTemplate(this.mapUrlTemplate, this.year);
};
NationListManager.prototype.init = function () {
this.root.classList.add('nation-list-manager-ready');
this.loadYear(this.year);
};
NationListManager.prototype.loadYear = function (year) {
var self = this;
this.year = String(year || this.year || '1950');
this.loading = true;
this.status = this.year + '년 데이터 불러오는 중...';
this.renderShell();
return fetch(this.getListUrl(), { credentials: 'same-origin', cache: 'no-cache' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
})
.then(function (text) {
var trimmed = String(text || '').trim();
if (!trimmed) throw new Error('empty');
self.state = normalizeState(JSON.parse(trimmed), self.year);
self.sourceExists = true;
self.status = self.year + '년 목록 로드 완료';
})
.catch(function () {
self.state = makeEmptyState(self.year);
self.sourceExists = false;
self.status = self.year + '년 목록 문서가 없어 빈 틀로 시작합니다.';
})
.then(function () {
return self.loadMapWarnings();
})
.finally(function () {
self.loading = false;
self.render();
});
};
NationListManager.prototype.loadMapWarnings = function () {
var self = this;
this.mapPages = null;
return fetch(this.getMapUrl(), { credentials: 'same-origin', cache: 'no-cache' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
})
.then(function (text) {
var trimmed = String(text || '').trim();
self.mapPages = trimmed ? normalizeMapPages(JSON.parse(trimmed)) : {};
})
.catch(function () {
self.mapPages = null;
});
};
NationListManager.prototype.getActiveContinent = function () {
var continents = this.state && this.state.continents ? this.state.continents : [];
var active = continents.find(function (continent) { return continent.key === this.activeContinent; }, this);
return active || continents[0] || null;
};
NationListManager.prototype.getAllItems = function () {
var rows = [];
if (!this.state || !Array.isArray(this.state.continents)) return rows;
this.state.continents.forEach(function (continent) {
(continent.regions || []).forEach(function (region) {
(region.items || []).forEach(function (item) {
rows.push({ continent: continent, region: region, item: item });
});
});
});
return rows;
};
NationListManager.prototype.getWarnings = function () {
var mapPages = this.mapPages;
var listPages = {};
var missingInMap = [];
var missingInList = [];
if (mapPages === null) {
return [{ type: 'soft', text: this.year + '년 링크맵을 불러오지 못했습니다. 링크맵 문서가 없으면 이 경고는 정상입니다.' }];
}
this.getAllItems().forEach(function (row) {
var page = String(row.item.page || '').trim();
if (!page || row.item.enabled === false) return;
listPages[page] = true;
if (!mapPages[page]) missingInMap.push(page);
});
Object.keys(mapPages).forEach(function (page) {
if (!listPages[page]) missingInList.push(page);
});
if (!missingInMap.length && !missingInList.length) {
return [{ type: 'ok', text: '국가 목록과 지구본 링크맵의 page 값이 맞습니다.' }];
}
if (missingInMap.length) {
missingInList = missingInList.slice(0, 80);
}
return [
missingInMap.length ? { type: 'warn', text: '목록에는 있으나 링크맵에 없는 page: ' + missingInMap.slice(0, 80).join(', ') + (missingInMap.length > 80 ? ' 외 ' + (missingInMap.length - 80) + '개' : '') } : null,
missingInList.length ? { type: 'warn', text: '링크맵에는 있으나 목록에 없는 page: ' + missingInList.slice(0, 80).join(', ') + (missingInList.length > 80 ? ' 외 ' + (missingInList.length - 80) + '개' : '') } : null
].filter(Boolean);
};
NationListManager.prototype.toText = function () {
var out = normalizeState(this.state, this.year);
out.year = parseInt(this.year, 10);
out.description = this.state.description || ('국가 및 조합 ' + this.year + '년 당대 존재 국가 패널 데이터. Manage:Nation_List에서 편집한다.');
return JSON.stringify(out, null, 2);
};
NationListManager.prototype.save = function () {
var self = this;
var title = this.getListTitle();
if (!window.mw || !mw.Api) {
this.setStatus('저장 실패: mw.Api를 사용할 수 없습니다.');
return;
}
this.setStatus(title + ' 저장 중...');
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: title,
text: this.toText(),
summary: 'Update ' + this.year + ' nation list',
format: 'json'
}).done(function () {
self.sourceExists = true;
self.setStatus(title + ' 저장 완료');
self.loadMapWarnings().then(function () { self.render(); });
}).fail(function (code, data) {
var message = data && data.error && data.error.info ? data.error.info : code;
self.setStatus('저장 실패: ' + message);
});
};
NationListManager.prototype.setStatus = function (text) {
this.status = text || '';
var el = this.root.querySelector('.nation-list-status');
if (el) el.textContent = this.status;
};
NationListManager.prototype.renderShell = function () {
this.root.innerHTML = '<div class="nation-list-loading">' + escapeHtml(this.status || 'loading...') + '</div>';
};
NationListManager.prototype.render = function () {
var state = this.state || makeEmptyState(this.year);
var active = this.getActiveContinent();
var allItems = this.getAllItems();
var enabledCount = allItems.filter(function (row) { return row.item.enabled !== false; }).length;
var warnings = this.getWarnings();
this.root.innerHTML = '' +
'<div class="nation-list-shell">' +
'<div class="nation-list-head">' +
'<div class="nation-list-title">Nation List Manager</div>' +
'<div class="nation-list-summary">' + escapeHtml(this.year) + '년 · 전체 ' + allItems.length + ' · 사용 ' + enabledCount + ' · ' + (this.sourceExists ? '문서 있음' : '새 문서') + '</div>' +
'</div>' +
'<div class="nation-list-toolbar">' +
'<label>연도 <select class="nation-list-year-select">' + this.years.map(function (year) {
return '<option value="' + escapeHtml(year) + '"' + (year === this.year ? ' selected' : '') + '>' + escapeHtml(year) + '</option>';
}, this).join('') + '</select></label>' +
'<input type="search" class="nation-list-search" placeholder="문서명, 표시명, 국기 검색" value="' + escapeHtml(this.query) + '">' +
'<button type="button" class="nation-list-reload">다시 불러오기</button>' +
'<button type="button" class="nation-list-save">저장</button>' +
'<span class="nation-list-status">' + escapeHtml(this.status) + '</span>' +
'</div>' +
'<div class="nation-list-warning-box">' + warnings.map(function (warning) {
return '<div class="nation-list-warning is-' + escapeHtml(warning.type) + '">' + escapeHtml(warning.text) + '</div>';
}).join('') + '</div>' +
'<div class="nation-list-continent-tabs">' + state.continents.map(function (continent) {
return '<button type="button" class="nation-list-continent-tab' + (active && active.key === continent.key ? ' is-active' : '') + '" data-continent="' + escapeHtml(continent.key) + '">' + escapeHtml(continent.label) + '</button>';
}).join('') + '</div>' +
'<div class="nation-list-body">' + (active ? this.renderContinent(active) : '') + '</div>' +
'</div>';
this.bind();
};
NationListManager.prototype.renderContinent = function (continent) {
return (continent.regions || []).map(function (region) {
return this.renderRegion(continent, region);
}, this).join('');
};
NationListManager.prototype.renderRegion = function (continent, region) {
var items = (region.items || []).filter(function (item) {
var q = this.query.trim().toLowerCase();
var flag = getItemPrimaryFlag(item);
var haystack = [item.page, item.label, item.external_url, flag.file, item.note].join(' ').toLowerCase();
return !q || haystack.indexOf(q) !== -1;
}, this);
return '' +
'<section class="nation-list-region" data-continent="' + escapeHtml(continent.key) + '" data-region="' + escapeHtml(region.key) + '">' +
'<div class="nation-list-region-head">' +
'<div><span class="nation-list-region-title">' + escapeHtml(region.label) + '</span><span class="nation-list-region-count">' + (region.items || []).length + '개</span></div>' +
'<button type="button" class="nation-list-add" data-continent="' + escapeHtml(continent.key) + '" data-region="' + escapeHtml(region.key) + '">항목 추가</button>' +
'</div>' +
'<div class="nation-list-table-wrap">' +
'<table class="nation-list-table">' +
'<thead><tr>' +
'<th class="nation-list-order-col">순서</th>' +
'<th>문서명 / 외부 URL</th>' +
'<th>표시명</th>' +
'<th>국기 파일</th>' +
'<th class="nation-list-small-col">크기</th>' +
'<th class="nation-list-check-col">테두리</th>' +
'<th class="nation-list-check-col">TOP</th>' +
'<th>메모</th>' +
'<th class="nation-list-check-col">사용</th>' +
'<th class="nation-list-action-col">작업</th>' +
'</tr></thead>' +
'<tbody>' + (items.length ? items.map(function (item) {
return this.renderItemRow(continent, region, item, region.items.indexOf(item));
}, this).join('') : '<tr><td colspan="10" class="nation-list-empty">이 지역에는 항목이 없습니다.</td></tr>') + '</tbody>' +
'</table>' +
'</div>' +
'</section>';
};
NationListManager.prototype.renderItemRow = function (continent, region, item, index) {
var flag = getItemPrimaryFlag(item);
var flagUrl = buildFileRedirectUrl(flag.file);
var linkValue = item.external_url ? item.external_url : item.page;
return '' +
'<tr data-continent="' + escapeHtml(continent.key) + '" data-region="' + escapeHtml(region.key) + '" data-index="' + index + '">' +
'<td class="nation-list-order-col"><button type="button" class="nation-list-move" data-dir="-1">↑</button><button type="button" class="nation-list-move" data-dir="1">↓</button></td>' +
'<td><input type="text" data-field="link" value="' + escapeHtml(linkValue) + '" placeholder="문서명 또는 https://..."></td>' +
'<td><input type="text" data-field="label" value="' + escapeHtml(item.label) + '" placeholder="표시명"></td>' +
'<td><div class="nation-list-flag-cell">' + (flagUrl ? '<img src="' + escapeHtml(flagUrl) + '" alt="">' : '<span></span>') + '<input type="text" data-field="flag" value="' + escapeHtml(flag.file) + '" placeholder="파일명.svg"></div></td>' +
'<td class="nation-list-small-col"><input type="text" data-field="width" value="' + escapeHtml(flag.width) + '"></td>' +
'<td class="nation-list-check-col"><input type="checkbox" data-field="border"' + (flag.border !== false ? ' checked' : '') + '></td>' +
'<td class="nation-list-check-col"><input type="checkbox" data-field="top"' + (flag.decorator === 'top' ? ' checked' : '') + '></td>' +
'<td><input type="text" data-field="note" value="' + escapeHtml(item.note) + '"></td>' +
'<td class="nation-list-check-col"><input type="checkbox" data-field="enabled"' + (item.enabled !== false ? ' checked' : '') + '></td>' +
'<td class="nation-list-action-col"><button type="button" class="nation-list-delete">삭제</button></td>' +
'</tr>';
};
NationListManager.prototype.bind = function () {
var self = this;
var yearSelect = this.root.querySelector('.nation-list-year-select');
var search = this.root.querySelector('.nation-list-search');
var save = this.root.querySelector('.nation-list-save');
var reload = this.root.querySelector('.nation-list-reload');
if (yearSelect) {
yearSelect.addEventListener('change', function () {
self.activeContinent = 'america';
self.query = '';
self.loadYear(yearSelect.value);
});
}
if (search) {
search.addEventListener('input', function () {
self.query = search.value;
self.render();
var next = self.root.querySelector('.nation-list-search');
if (next) {
next.focus();
next.setSelectionRange(next.value.length, next.value.length);
}
});
}
if (save) save.addEventListener('click', function () { self.save(); });
if (reload) reload.addEventListener('click', function () { self.loadYear(self.year); });
this.root.querySelectorAll('.nation-list-continent-tab').forEach(function (tab) {
tab.addEventListener('click', function () {
self.activeContinent = tab.getAttribute('data-continent') || self.activeContinent;
self.render();
});
});
this.root.querySelectorAll('.nation-list-add').forEach(function (button) {
button.addEventListener('click', function () {
self.addItem(button.getAttribute('data-continent'), button.getAttribute('data-region'));
});
});
this.root.querySelectorAll('.nation-list-table tbody tr[data-index]').forEach(function (tr) {
tr.querySelectorAll('input[data-field]').forEach(function (input) {
input.addEventListener('input', function () {
self.updateRow(tr, input);
});
input.addEventListener('change', function () {
self.updateRow(tr, input);
if (input.getAttribute('data-field') === 'flag') self.render();
});
});
tr.querySelectorAll('.nation-list-move').forEach(function (button) {
button.addEventListener('click', function () {
self.moveItem(tr, parseInt(button.getAttribute('data-dir') || '0', 10));
});
});
var del = tr.querySelector('.nation-list-delete');
if (del) {
del.addEventListener('click', function () {
self.deleteItem(tr);
});
}
});
};
NationListManager.prototype.findRegion = function (continentKey, regionKey) {
var continent = (this.state.continents || []).find(function (item) { return item.key === continentKey; });
if (!continent) return null;
return (continent.regions || []).find(function (item) { return item.key === regionKey; }) || null;
};
NationListManager.prototype.getRowItem = function (tr) {
var region = this.findRegion(tr.getAttribute('data-continent'), tr.getAttribute('data-region'));
var index = parseInt(tr.getAttribute('data-index') || '-1', 10);
if (!region || index < 0 || !region.items || !region.items[index]) return null;
return { region: region, index: index, item: region.items[index] };
};
NationListManager.prototype.updateRow = function (tr, input) {
var row = this.getRowItem(tr);
var field = input.getAttribute('data-field');
var flag;
var value;
if (!row) return;
flag = getItemPrimaryFlag(row.item);
value = input.type === 'checkbox' ? input.checked : input.value;
if (field === 'link') {
value = String(value || '').trim();
if (/^https?:\/\//i.test(value)) {
row.item.external_url = value;
row.item.page = '';
} else {
row.item.page = value;
row.item.external_url = '';
}
} else if (field === 'label') {
row.item.label = value;
} else if (field === 'flag') {
flag.file = normalizeFileTitle(value);
} else if (field === 'width') {
flag.width = String(value || '').trim() || '16px';
} else if (field === 'border') {
flag.border = !!value;
} else if (field === 'top') {
flag.decorator = value ? 'top' : '';
} else if (field === 'note') {
row.item.note = value;
} else if (field === 'enabled') {
row.item.enabled = !!value;
}
this.setStatus('수정됨');
};
NationListManager.prototype.addItem = function (continentKey, regionKey) {
var region = this.findRegion(continentKey, regionKey);
if (!region) return;
region.items = region.items || [];
region.items.push(normalizeItem({ page: '', label: '', flags: [normalizeFlag({})], enabled: true }));
this.setStatus('항목 추가됨');
this.render();
};
NationListManager.prototype.deleteItem = function (tr) {
var row = this.getRowItem(tr);
if (!row) return;
row.region.items.splice(row.index, 1);
this.setStatus('항목 삭제됨');
this.render();
};
NationListManager.prototype.moveItem = function (tr, direction) {
var row = this.getRowItem(tr);
var next;
var item;
if (!row || !direction) return;
next = row.index + direction;
if (next < 0 || next >= row.region.items.length) return;
item = row.region.items.splice(row.index, 1)[0];
row.region.items.splice(next, 0, item);
this.setStatus('순서 변경됨');
this.render();
};
function init(root) {
(root || document).querySelectorAll('.nation-list-manager').forEach(function (node) {
if (node.getAttribute('data-nation-list-manager-ready') === '1') return;
node.setAttribute('data-nation-list-manager-ready', '1');
new NationListManager(node).init();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { init(document); });
} else {
init(document);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
init($content && $content[0] ? $content[0] : document);
});
}
}());