참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
(function () {
'use strict';
var HISTORY_EVENT_MANAGER_BUILD = '20260710-history-events-manager-fix53';
var DEFAULT_ERAS = [
'1950', '1960', '1970', '1980', '1990', '2000', '2010', '2020',
'2030', '2040', '2050', '2060', '2070', '2080', '2090', '2100'
];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function formatMwApiFailure(code, data) {
var parts = [];
var xhr = data && (data.xhr || data.jqXHR);
if (data && data.error && data.error.info) parts.push(data.error.info);
if (data && data.exception) parts.push(String(data.exception));
if (xhr) {
if (xhr.status) parts.push('HTTP ' + xhr.status + (xhr.statusText ? ' ' + xhr.statusText : ''));
if (xhr.responseJSON && xhr.responseJSON.error && xhr.responseJSON.error.info) parts.push(xhr.responseJSON.error.info);
if (!xhr.status && xhr.responseText) parts.push(String(xhr.responseText).slice(0, 180));
}
if (!parts.length && data && data.textStatus) parts.push(String(data.textStatus));
if (!parts.length) parts.push(String(code || 'unknown'));
return parts.filter(Boolean).join(' / ');
}
function replaceYearTemplate(template, year) {
return String(template || '').replace(/\{year\}/g, String(year || ''));
}
function normalizeYear(value) {
var text = String(value || '').trim().replace(/[^0-9]/g, '').slice(0, 4);
return text.length === 4 ? text : '';
}
function normalizeEvent(item) {
item = item && typeof item === 'object' ? item : {};
return {
period: String(item.period || item.date || item.range || item.year || '').trim(),
title: String(item.title || item.event || item.name || item.label || '').trim(),
page: String(item.page || item.wiki_title || item.title_page || '').trim(),
external_url: String(item.external_url || item.url || '').trim(),
major: item.major === true || item.type === 'major',
note: String(item.note || '').trim(),
enabled: item.enabled !== false
};
}
function makeEmptyState(era) {
var start = parseInt(era || '1950', 10);
var years = [];
var i;
if (!isFinite(start)) start = 1950;
for (i = 0; i < 10; i += 1) {
years.push({ year: String(start + i), label: String(start + i), items: [] });
}
return {
version: 1,
era: parseInt(era || start, 10),
description: '시대 ' + era + '년 역사적 사건 패널 데이터. Manage:History_Events에서 편집한다.',
years: years
};
}
function normalizeState(payload, era) {
var state = makeEmptyState(era);
var sourceYears = [];
if (!payload || typeof payload !== 'object') return state;
if (Array.isArray(payload.years)) {
sourceYears = payload.years;
} else if (payload.events && typeof payload.events === 'object') {
Object.keys(payload.events).forEach(function (year) {
sourceYears.push({ year: year, label: year, items: payload.events[year] });
});
}
state.version = payload.version || 1;
state.era = payload.era || payload.year || state.era;
state.description = payload.description || state.description;
state.years = sourceYears.map(function (row) {
var year;
var items;
if (typeof row === 'string' || typeof row === 'number') row = { year: String(row), items: [] };
row = row && typeof row === 'object' ? row : {};
year = normalizeYear(row.year || row.key || row.label) || String(row.year || row.key || row.label || '').trim();
items = Array.isArray(row.items) ? row.items : (Array.isArray(row.events) ? row.events : []);
return {
year: year,
label: String(row.label || year).trim() || year,
items: items.map(normalizeEvent)
};
}).filter(function (row) { return !!row.year; });
if (!state.years.length) state.years = makeEmptyState(era).years;
return state;
}
function compareYears(a, b) {
var av = parseInt(a && a.year, 10);
var bv = parseInt(b && b.year, 10);
if (isFinite(av) && isFinite(bv) && av !== bv) return av - bv;
return String(a && a.year || '').localeCompare(String(b && b.year || ''), ['ko', 'en'], { numeric: true });
}
function HistoryEventManager(root) {
this.root = root;
this.eras = String(root.getAttribute('data-years') || DEFAULT_ERAS.join(' ')).split(/[\s,|]+/).filter(Boolean);
this.era = root.getAttribute('data-year') || this.eras[0] || '1950';
this.titleTemplate = root.getAttribute('data-title-template') || 'MediaWiki:{year}_History_Events.json';
this.urlTemplate = root.getAttribute('data-url-template') || '/index.php?title=MediaWiki:{year}_History_Events.json&action=raw&ctype=application/json';
this.state = null;
this.status = '';
this.query = '';
this.loading = false;
this.sourceExists = false;
}
HistoryEventManager.prototype.getTitle = function () {
return replaceYearTemplate(this.titleTemplate, this.era);
};
HistoryEventManager.prototype.getUrl = function () {
return replaceYearTemplate(this.urlTemplate, this.era);
};
HistoryEventManager.prototype.init = function () {
this.root.classList.add('history-event-manager-ready');
this.loadYear(this.era);
};
HistoryEventManager.prototype.setStatus = function (text) {
this.status = text || '';
var el = this.root.querySelector('.history-event-status');
if (el) el.textContent = this.status;
};
HistoryEventManager.prototype.renderShell = function () {
this.root.innerHTML = '<div class="history-event-loading">' + escapeHtml(this.status || 'loading...') + '</div>';
};
HistoryEventManager.prototype.loadYear = function (year) {
var self = this;
this.era = String(year || this.era || '1950');
this.loading = true;
this.status = this.era + '년 역사적 사건 데이터 불러오는 중...';
this.renderShell();
return fetch(this.getUrl(), { 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.era);
self.sourceExists = true;
self.status = self.getTitle() + ' 불러옴';
}).catch(function () {
self.state = makeEmptyState(self.era);
self.sourceExists = false;
self.status = self.getTitle() + ' 새 문서';
}).finally(function () {
self.loading = false;
self.render();
});
};
HistoryEventManager.prototype.getFilteredYears = function () {
var q = this.query.trim().toLowerCase();
var years = (this.state && this.state.years ? this.state.years : []).slice();
if (!q) return years;
return years.map(function (year) {
var copy = {
year: year.year,
label: year.label,
items: (year.items || []).filter(function (item) {
return [year.year, year.label, item.period, item.title, item.page, item.external_url, item.note, item.major ? 'major' : '', item.enabled === false ? 'disabled' : 'enabled'].join(' ').toLowerCase().indexOf(q) !== -1;
})
};
return copy;
}).filter(function (year) {
return year.items.length || String(year.year + ' ' + year.label).toLowerCase().indexOf(q) !== -1;
});
};
HistoryEventManager.prototype.render = function () {
var state = this.state || makeEmptyState(this.era);
var years = this.getFilteredYears();
var eventCount = (state.years || []).reduce(function (sum, year) { return sum + ((year.items || []).length); }, 0);
var enabledCount = (state.years || []).reduce(function (sum, year) {
return sum + (year.items || []).filter(function (item) { return item.enabled !== false; }).length;
}, 0);
this.root.innerHTML = '' +
'<div class="history-event-shell">' +
'<div class="history-event-head">' +
'<div class="history-event-title">History Event Manager</div>' +
'<div class="history-event-summary">' + escapeHtml(this.era) + '년 · 연도 ' + (state.years || []).length + ' · 사건 ' + eventCount + ' · 사용 ' + enabledCount + ' · ' + (this.sourceExists ? '문서 있음' : '새 문서') + '</div>' +
'</div>' +
'<div class="history-event-toolbar">' +
'<label>시대 <select class="history-event-year-select">' + this.eras.map(function (year) {
return '<option value="' + escapeHtml(year) + '"' + (year === this.era ? ' selected' : '') + '>' + escapeHtml(year) + '</option>';
}, this).join('') + '</select></label>' +
'<input type="search" class="history-event-search" placeholder="연도, 날짜, 사건명, 문서명, 메모 검색" value="' + escapeHtml(this.query) + '">' +
'<button type="button" class="history-event-add-year">연도 추가</button>' +
'<button type="button" class="history-event-reload">다시 불러오기</button>' +
'<button type="button" class="history-event-save">저장</button>' +
'<span class="history-event-status">' + escapeHtml(this.status) + '</span>' +
'</div>' +
'<div class="history-event-note">시대 문서의 역사적 사건 패널 데이터입니다. 저장 대상: ' + escapeHtml(this.getTitle()) + '</div>' +
'<div class="history-event-body">' + (years.length ? years.map(this.renderYearSection, this).join('') : '<div class="history-event-empty">검색 결과가 없습니다.</div>') + '</div>' +
'</div>';
this.bind();
};
HistoryEventManager.prototype.renderYearSection = function (year) {
var rows = (year.items || []).map(function (item, index) {
return this.renderEventRow(year, item, index);
}, this).join('');
if (!rows) rows = '<tr><td colspan="9" class="history-event-empty-cell">이 연도에는 사건이 없습니다.</td></tr>';
return '' +
'<section class="history-event-year" data-year="' + escapeHtml(year.year) + '">' +
'<div class="history-event-year-head">' +
'<div class="history-event-year-fields">' +
'<input type="text" class="history-event-year-value" data-year-field="year" value="' + escapeHtml(year.year) + '" maxlength="4" title="탭 내부 키">' +
'<input type="text" class="history-event-year-label" data-year-field="label" value="' + escapeHtml(year.label || year.year) + '" title="패널에 보이는 연도 버튼 라벨">' +
'<span class="history-event-year-count">' + (year.items || []).length + '개</span>' +
'</div>' +
'<div class="history-event-year-actions">' +
'<button type="button" class="history-event-add" data-year="' + escapeHtml(year.year) + '">사건 추가</button>' +
'<button type="button" class="history-event-year-up" data-year="' + escapeHtml(year.year) + '">위</button>' +
'<button type="button" class="history-event-year-down" data-year="' + escapeHtml(year.year) + '">아래</button>' +
'<button type="button" class="history-event-year-delete" data-year="' + escapeHtml(year.year) + '">연도 삭제</button>' +
'</div>' +
'</div>' +
'<div class="history-event-table-wrap">' +
'<table class="history-event-table">' +
'<thead><tr>' +
'<th class="history-event-order-col">순서</th>' +
'<th class="history-event-period-col">날짜/기간</th>' +
'<th class="history-event-title-col">사건명</th>' +
'<th class="history-event-page-col">문서</th>' +
'<th class="history-event-url-col">외부 URL</th>' +
'<th class="history-event-check-col">강조</th>' +
'<th class="history-event-check-col">사용</th>' +
'<th class="history-event-note-col">메모</th>' +
'<th class="history-event-action-col">작업</th>' +
'</tr></thead>' +
'<tbody data-year="' + escapeHtml(year.year) + '">' + rows + '</tbody>' +
'</table>' +
'</div>' +
'</section>';
};
HistoryEventManager.prototype.renderEventRow = function (year, item, index) {
return '' +
'<tr data-year="' + escapeHtml(year.year) + '" data-index="' + index + '">' +
'<td class="history-event-order-col"><button type="button" class="history-event-up">↑</button><button type="button" class="history-event-down">↓</button></td>' +
'<td class="history-event-period-col"><input type="text" data-field="period" value="' + escapeHtml(item.period) + '" placeholder="1936.07.17–1940..."></td>' +
'<td class="history-event-title-col"><input type="text" data-field="title" value="' + escapeHtml(item.title) + '" placeholder="사건명"></td>' +
'<td class="history-event-page-col"><input type="text" data-field="page" value="' + escapeHtml(item.page) + '" placeholder="문서명"></td>' +
'<td class="history-event-url-col"><input type="text" data-field="external_url" value="' + escapeHtml(item.external_url) + '" placeholder="https://..."></td>' +
'<td class="history-event-check-col"><input type="checkbox" data-field="major"' + (item.major ? ' checked' : '') + '></td>' +
'<td class="history-event-check-col"><input type="checkbox" data-field="enabled"' + (item.enabled !== false ? ' checked' : '') + '></td>' +
'<td class="history-event-note-col"><input type="text" data-field="note" value="' + escapeHtml(item.note) + '"></td>' +
'<td class="history-event-action-col"><button type="button" class="history-event-delete">삭제</button></td>' +
'</tr>';
};
HistoryEventManager.prototype.bind = function () {
var self = this;
var select = this.root.querySelector('.history-event-year-select');
var search = this.root.querySelector('.history-event-search');
var save = this.root.querySelector('.history-event-save');
var reload = this.root.querySelector('.history-event-reload');
var addYear = this.root.querySelector('.history-event-add-year');
if (select) select.addEventListener('change', function () { self.query = ''; self.loadYear(select.value); });
if (search) search.addEventListener('input', function () {
self.query = search.value;
self.render();
var next = self.root.querySelector('.history-event-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.era); });
if (addYear) addYear.addEventListener('click', function () { self.addYear(); });
this.root.querySelectorAll('.history-event-year').forEach(function (section) {
var yearKey = section.getAttribute('data-year') || '';
section.querySelectorAll('[data-year-field]').forEach(function (input) {
input.addEventListener('input', function () { self.updateYear(section, input); });
input.addEventListener('change', function () { self.updateYear(section, input); self.render(); });
});
section.querySelectorAll('.history-event-add').forEach(function (button) {
button.addEventListener('click', function () { self.addEvent(yearKey); });
});
section.querySelectorAll('.history-event-year-delete').forEach(function (button) {
button.addEventListener('click', function () { self.deleteYear(yearKey); });
});
section.querySelectorAll('.history-event-year-up').forEach(function (button) {
button.addEventListener('click', function () { self.moveYear(yearKey, -1); });
});
section.querySelectorAll('.history-event-year-down').forEach(function (button) {
button.addEventListener('click', function () { self.moveYear(yearKey, 1); });
});
});
this.root.querySelectorAll('.history-event-table tbody tr[data-index]').forEach(function (tr) {
tr.querySelectorAll('input[data-field]').forEach(function (input) {
input.addEventListener('input', function () { self.updateEvent(tr, input); });
input.addEventListener('change', function () { self.updateEvent(tr, input); });
});
var del = tr.querySelector('.history-event-delete');
var up = tr.querySelector('.history-event-up');
var down = tr.querySelector('.history-event-down');
if (del) del.addEventListener('click', function () { self.deleteEvent(tr); });
if (up) up.addEventListener('click', function () { self.moveEvent(tr, -1); });
if (down) down.addEventListener('click', function () { self.moveEvent(tr, 1); });
});
};
HistoryEventManager.prototype.findYear = function (yearKey) {
return (this.state.years || []).find(function (row) { return row.year === yearKey; }) || null;
};
HistoryEventManager.prototype.findEvent = function (tr) {
var year = this.findYear(tr.getAttribute('data-year'));
var index = parseInt(tr.getAttribute('data-index') || '-1', 10);
if (!year || index < 0 || !year.items || !year.items[index]) return null;
return { year: year, index: index, item: year.items[index] };
};
HistoryEventManager.prototype.updateYear = function (section, input) {
var oldKey = section.getAttribute('data-year') || '';
var row = this.findYear(oldKey);
var field = input.getAttribute('data-year-field');
var value = input.value;
var newKey;
if (!row) return;
if (field === 'year') {
newKey = normalizeYear(value) || String(value || '').trim();
row.year = newKey;
if (!row.label || row.label === oldKey) row.label = newKey;
section.setAttribute('data-year', newKey);
} else if (field === 'label') {
row.label = String(value || '').trim();
}
this.setStatus('수정됨');
};
HistoryEventManager.prototype.updateEvent = function (tr, input) {
var row = this.findEvent(tr);
var field = input.getAttribute('data-field');
var value;
if (!row) return;
value = input.type === 'checkbox' ? input.checked : input.value;
if (field === 'period') row.item.period = String(value || '').trim();
else if (field === 'title') row.item.title = String(value || '').trim();
else if (field === 'page') row.item.page = String(value || '').trim();
else if (field === 'external_url') row.item.external_url = String(value || '').trim();
else if (field === 'major') row.item.major = !!value;
else if (field === 'enabled') row.item.enabled = !!value;
else if (field === 'note') row.item.note = String(value || '').trim();
this.setStatus('수정됨');
};
HistoryEventManager.prototype.addYear = function () {
var value = window.prompt('추가할 연도를 입력하세요.', this.era);
var year = normalizeYear(value) || String(value || '').trim();
if (!year) return;
if (this.findYear(year)) {
this.setStatus('이미 있는 연도입니다: ' + year);
return;
}
this.state.years.push({ year: year, label: year, items: [] });
this.setStatus('연도 추가됨');
this.render();
};
HistoryEventManager.prototype.deleteYear = function (yearKey) {
var index = (this.state.years || []).findIndex(function (row) { return row.year === yearKey; });
if (index < 0) return;
this.state.years.splice(index, 1);
this.setStatus('연도 삭제됨');
this.render();
};
HistoryEventManager.prototype.moveYear = function (yearKey, direction) {
var list = this.state.years || [];
var index = list.findIndex(function (row) { return row.year === yearKey; });
var next = index + direction;
var item;
if (index < 0 || next < 0 || next >= list.length) return;
item = list.splice(index, 1)[0];
list.splice(next, 0, item);
this.setStatus('연도 순서 변경됨');
this.render();
};
HistoryEventManager.prototype.addEvent = function (yearKey) {
var year = this.findYear(yearKey);
if (!year) return;
year.items = year.items || [];
year.items.push(normalizeEvent({ enabled: true }));
this.setStatus('사건 추가됨');
this.render();
};
HistoryEventManager.prototype.deleteEvent = function (tr) {
var row = this.findEvent(tr);
if (!row) return;
row.year.items.splice(row.index, 1);
this.setStatus('사건 삭제됨');
this.render();
};
HistoryEventManager.prototype.moveEvent = function (tr, direction) {
var row = this.findEvent(tr);
var list;
var next;
var item;
if (!row) return;
list = row.year.items || [];
next = row.index + direction;
if (next < 0 || next >= list.length) return;
item = list.splice(row.index, 1)[0];
list.splice(next, 0, item);
this.setStatus('사건 순서 변경됨');
this.render();
};
HistoryEventManager.prototype.toText = function () {
var out = normalizeState(this.state, this.era);
out.era = parseInt(this.era, 10) || this.era;
out.description = this.state.description || ('시대 ' + this.era + '년 역사적 사건 패널 데이터. Manage:History_Events에서 편집한다.');
out.years = (out.years || []).map(function (year) {
return {
year: year.year,
label: year.label || year.year,
items: (year.items || []).map(function (item) {
var row = {
period: item.period || '',
title: item.title || '',
page: item.page || '',
external_url: item.external_url || '',
major: !!item.major,
enabled: item.enabled !== false
};
if (item.note) row.note = item.note;
return row;
})
};
});
return JSON.stringify(out, null, 2);
};
HistoryEventManager.prototype.save = function () {
var self = this;
var title = this.getTitle();
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.era + ' history events',
format: 'json'
}).done(function () {
self.sourceExists = true;
self.setStatus(title + ' 저장 완료');
self.render();
if (window.NationsPanel && typeof window.NationsPanel.refresh === 'function') {
window.NationsPanel.refresh(document);
}
}).fail(function (code, data) {
self.setStatus('저장 실패: ' + formatMwApiFailure(code, data));
});
};
function init(root) {
(root || document).querySelectorAll('.history-event-manager').forEach(function (node) {
if (node.getAttribute('data-history-event-manager-ready') === '1') return;
node.setAttribute('data-history-event-manager-ready', '1');
new HistoryEventManager(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);
});
}
window.HistoryEventManager = {
build: HISTORY_EVENT_MANAGER_BUILD,
init: init
};
}());