미디어위키:Nation List Manager.js

Nxdsxn (토론 | 기여)님의 2026년 7월 5일 (일) 04:04 판 (새 문서: (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: ['북유럽', '서유럽', '중앙유럽', '동유럽', '동남유럽', '남유럽', '남...)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: 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, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

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