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

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
(function () {
    'use strict';

    var NATION_LIST_MANAGER_BUILD = '20260710-symbol-details-list-001';

    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 formatMwApiFailure(code, data) {
        var parts = [];
        var xhr = data && (data.xhr || data.jqXHR);
        var status;
        var text;

        if (data && data.error && data.error.info) parts.push(data.error.info);
        if (data && data.exception) parts.push(String(data.exception));
        if (xhr) {
            status = xhr.status || 0;
            text = xhr.statusText || '';
            if (status) parts.push('HTTP ' + status + (text ? ' ' + text : ''));
            if (xhr.responseJSON && xhr.responseJSON.error && xhr.responseJSON.error.info) {
                parts.push(xhr.responseJSON.error.info);
            }
            if (!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 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) {
        var text = String(value || '').trim();
        var titleMatch;
        var redirectMatch;
        var previous;
        var i;
        var j;

        if (!text) return '';

        text = text
            .replace(/^\[\[:?\s*/i, '')
            .replace(/\]\]$/i, '')
            .trim();

        for (i = 0; i < 8; i += 1) {
            previous = text;

            for (j = 0; j < 3; j += 1) {
                try {
                    if (/%[0-9a-f]{2}/i.test(text)) {
                        text = decodeURIComponent(text);
                    }
                } catch (err) {
                    break;
                }
            }

            text = text.replace(/\\/g, '/').trim();

            titleMatch = text.match(/[?&]title=([^&#]+)/i);
            if (titleMatch) {
                text = titleMatch[1];
                continue;
            }

            text = text
                .replace(/[?#].*$/g, '')
                .replace(/^https?:\/\/[^/]+/i, '')
                .replace(/^\/+/, '')
                .replace(/^index\.php\/?/i, '')
                .replace(/^wiki\/?/i, '')
                .trim();

            for (j = 0; j < 6; j += 1) {
                var stripped = text
                    .replace(/^\s*파일\s*:/i, '')
                    .replace(/^\s*File\s*:/i, '')
                    .replace(/^\s*이미지\s*:/i, '')
                    .replace(/^\s*Image\s*:/i, '')
                    .replace(/^:+/, '')
                    .trim();
                if (stripped === text) break;
                text = stripped;
            }

            redirectMatch = text.match(/^(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
                text.match(/^(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
                text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);

            if (redirectMatch) {
                text = redirectMatch[1].trim();
                continue;
            }

            if (text === previous) break;
        }

        text = text.replace(/^\/+/, '').trim();

        return text;
    }

    function buildFileRedirectUrl(value) {
        var file = normalizeFileTitle(value);
        return file ? '/index.php?title=Special:Redirect/file/' + encodeWikiTitle(file) : '';
    }

    function fileDisplayTitle(value) {
        var file = normalizeFileTitle(value);
        return file ? '파일:' + file : '';
    }

    function getWikiSortBucket(value) {
        var text = String(value || '').trim();
        var first = text.charAt(0);

        if (/^[A-Za-z0-9]/.test(first)) return 0;
        if (/^[가-힣ㄱ-ㅎㅏ-ㅣ]/.test(first)) return 1;
        return 2;
    }

    function compareWikiLabels(a, b) {
        var av = String(a == null ? '' : a);
        var bv = String(b == null ? '' : b);
        var ab = getWikiSortBucket(av);
        var bb = getWikiSortBucket(bv);

        if (ab !== bb) return ab - bb;

        return av.localeCompare(bv, ['en', 'ko'], { numeric: true, sensitivity: 'base' });
    }

    function normalizeTagInput(value) {
        return String(value || '')
            .trim()
            .toUpperCase()
            .replace(/\s+/g, '')
            .replace(/[^A-Z0-9]/g, '')
            .slice(0, 3);
    }

    function normalizeStoredTag(value) {
        var text = String(value || '')
            .trim()
            .toUpperCase()
            .replace(/\s+/g, '')
            .replace(/[^A-Z0-9]/g, '');

        return /^[A-Z0-9]{3}$/.test(text) ? text : '';
    }

    function isValidTag(value) {
        return !!normalizeStoredTag(value);
    }


    var CANONICAL_TAG_FIXES_1950 = [];

    function getItemNameCandidates(item) {
        return [item && item.label, item && item.page, item && item.globe_label]
            .map(function (value) { return String(value || '').trim(); })
            .filter(Boolean);
    }

    function getCanonicalTagFixForItem(year, item) {
        var yearText = String(year || '').trim();
        var tag = normalizeStoredTag(item && item.tag);
        var names = getItemNameCandidates(item);
        var i;

        if (yearText !== '1950') return '';

        for (i = 0; i < CANONICAL_TAG_FIXES_1950.length; i += 1) {
            var rule = CANONICAL_TAG_FIXES_1950[i];
            var nameMatches = names.some(function (name) { return rule.names.indexOf(name) !== -1; });
            var fromMatches = !tag || rule.from.indexOf(tag) !== -1;

            if (nameMatches && fromMatches) return rule.to;
        }

        return '';
    }

    function getItemTag(item) {
        var tag = normalizeStoredTag(item && (item.tag || item.code || item.map_tag || item.mapTag || ''));
        return tag;
    }

    function getEntryTag(entry, fallback) {
        var sources = [];
        var i;
        var tag;

        if (entry && typeof entry === 'object') {
            sources.push(entry.tag, entry.code, entry.map_tag, entry.mapTag, entry.page);
        }
        sources.push(fallback);

        for (i = 0; i < sources.length; i += 1) {
            tag = normalizeStoredTag(sources[i]);
            if (tag) return tag;
        }

        return '';
    }

    function normalizeFileKey(value) {
        return normalizeFileTitle(value).replace(/_/g, ' ').trim().toLowerCase();
    }

    function collapseDuplicateFileExtension(value) {
        var file = normalizeFileTitle(value);
        return file.replace(/(\.(?:svg|png|jpe?g|gif|webp|avif|ico|pdf|geojson|json))\1$/i, '$1');
    }

    function isFlagFileCandidate(value) {
        var file = normalizeFileTitle(value).replace(/_/g, ' ').trim();
        return /^Flag-/i.test(file);
    }

    function isSybFileCandidate(value) {
        var file = normalizeFileTitle(value).replace(/_/g, ' ').trim();
        return /^Syb-/i.test(file);
    }

    function isManagedImageFileCandidate(value) {
        return isFlagFileCandidate(value) || isSybFileCandidate(value);
    }

    function isPickerFileCandidate(kind, value) {
        if (kind === 'symbol') return isManagedImageFileCandidate(value);
        return kind === 'syb' ? isSybFileCandidate(value) : isFlagFileCandidate(value);
    }

    function cleanupFlagFileSearchList(list) {
        var fileSet = {};
        var seen = {};
        var cleaned = [];

        (list || []).forEach(function (item) {
            var key = normalizeFileKey(item && item.file);
            if (key) fileSet[key] = true;
        });

        (list || []).forEach(function (item) {
            var file = normalizeFileTitle(item && item.file);
            var key = normalizeFileKey(file);
            var collapsed = collapseDuplicateFileExtension(file);
            var collapsedKey = normalizeFileKey(collapsed);

            if (!file || !key) return;
            if (!isManagedImageFileCandidate(file)) return;
            if (item && item.isRedirect) return;
            if (collapsedKey && collapsedKey !== key && fileSet[collapsedKey]) return;
            if (seen[key]) return;

            seen[key] = true;
            cleaned.push(item);
        });

        return cleaned;
    }

    function sortFlagPickerFiles(list, query) {
        var q = normalizeFileTitle(query).toLowerCase();

        return (list || []).slice().sort(function (a, b) {
            var af = normalizeFileTitle(a && a.file).toLowerCase();
            var bf = normalizeFileTitle(b && b.file).toLowerCase();
            var ar = a && a.isRecentUpload ? 0 : 1;
            var br = b && b.isRecentUpload ? 0 : 1;
            var ai;
            var bi;

            if (!q) {
                if (ar !== br) return ar - br;
                return compareWikiLabels(a.file, b.file);
            }

            ai = af === q ? 0 : (af.indexOf(q) === 0 ? 1 : (af.indexOf(q) !== -1 ? 2 : 3));
            bi = bf === q ? 0 : (bf.indexOf(q) === 0 ? 1 : (bf.indexOf(q) !== -1 ? 2 : 3));

            if (ai !== bi) return ai - bi;
            if (ar !== br) return ar - br;
            return compareWikiLabels(a.file, b.file);
        });
    }

    function clampNumber(value, min, max) {
        value = parseFloat(value);
        if (!isFinite(value)) value = 0;
        return Math.max(min, Math.min(max, value));
    }

    function normalizeOffsetMode(value) {
        value = String(value == null ? '' : value).toLowerCase().trim();
        return value === 'manual' ? 'manual' : 'auto';
    }

    function getFlagOffsetY(flag) {
        var value;

        if (!flag || typeof flag !== 'object') return 0;

        value = flag.offsetY;
        if (value == null) value = flag.offset_y;
        if (value == null) value = flag.flagOffsetY;

        if (value == null && flag.decorator === 'top') value = -1;

        return Math.round(clampNumber(value, -3, 3));
    }

    function getFlagOffsetMode(flag) {
        if (!flag || typeof flag !== 'object') return 'auto';
        if (flag.offsetMode || flag.offset_mode) return normalizeOffsetMode(flag.offsetMode || flag.offset_mode);
        if (flag.decorator === 'top') return 'manual';
        return 'auto';
    }

    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에서 편집한다.',
            relation_options: [],
            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) {
        return normalizeSymbol(flag, 'flag');
    }

    function normalizeSyb(syb) {
        return normalizeSymbol(syb, 'syb');
    }

    function normalizeSymbol(symbol, legacyKind) {
        var file;
        var explicitBorder;
        var border;

        if (typeof symbol === 'string') symbol = { file: symbol };
        if (!symbol || typeof symbol !== 'object') symbol = {};

        file = normalizeFileTitle(symbol.file || symbol.flag_file || symbol.flag || symbol.flag_title || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '');
        explicitBorder = symbol.border;
        if (explicitBorder == null && legacyKind === 'syb') explicitBorder = symbol.syb_border;
        if (explicitBorder == null) explicitBorder = symbol.symbol_border;
        if (explicitBorder == null && legacyKind === 'flag') explicitBorder = symbol.flag_border;

        if (explicitBorder == null) {
            border = !isSybFileCandidate(file);
        } else {
            border = explicitBorder !== false;
        }

        return {
            file: file,
            width: String(symbol.width || symbol.flag_width || symbol.syb_width || symbol.symbol_width || '16px').trim() || '16px',
            border: border,
            offsetMode: getFlagOffsetMode(symbol),
            offsetY: getFlagOffsetY(symbol)
        };
    }

    function collectItemSymbols(item) {
        var symbols = [];
        var legacySeen = {};
        var hasExplicitFilled = false;

        item = item && typeof item === 'object' ? item : {};

        function pushNormalized(symbol, legacyKind, dedupeLegacy) {
            var normalized = normalizeSymbol(symbol, legacyKind);
            var key = normalizeFileKey(normalized.file);

            if (dedupeLegacy && normalized.file) {
                if (legacySeen[key]) return;
                legacySeen[key] = true;
            }

            if (normalized.file) hasExplicitFilled = true;
            symbols.push(normalized);
        }

        if (Array.isArray(item.symbols)) {
            item.symbols.slice(0, 3).forEach(function (symbol) {
                pushNormalized(symbol, 'symbol', false);
            });
            if (symbols.length && hasExplicitFilled) {
                while (symbols.length < 3) symbols.push(normalizeSymbol({}));
                return symbols.slice(0, 3);
            }
            symbols = [];
            hasExplicitFilled = false;
        }

        if (Array.isArray(item.flags)) item.flags.forEach(function (symbol) { pushNormalized(symbol, 'flag', true); });
        if (Array.isArray(item.sybs)) item.sybs.forEach(function (symbol) { pushNormalized(symbol, 'syb', true); });

        if (item.flag_file || item.flag || item.flag_title) {
            pushNormalized({ file: item.flag_file || item.flag || item.flag_title, width: item.flag_width || '16px', border: item.border !== false, decorator: item.decorator || '' }, 'flag', true);
        }
        if (item.syb_file || item.syb || item.symbol_file || item.symbol) {
            pushNormalized({ file: item.syb_file || item.syb || item.symbol_file || item.symbol, width: item.syb_width || item.symbol_width || '16px', border: item.syb_border === true || item.symbol_border === true }, 'syb', true);
        }

        while (symbols.length < 3) symbols.push(normalizeSymbol({}));
        return symbols.slice(0, 3);
    }

    function collectFilledSymbols(item) {
        return collectItemSymbols(item).filter(function (symbol) {
            return !!normalizeFileTitle(symbol && symbol.file);
        }).slice(0, 3).map(function (symbol) {
            return normalizeSymbol(symbol);
        });
    }

    function symbolSignature(symbols) {
        return JSON.stringify((symbols || []).map(function (symbol) {
            symbol = normalizeSymbol(symbol);
            return {
                file: symbol.file,
                width: symbol.width,
                border: symbol.border !== false,
                offsetMode: symbol.offsetMode,
                offsetY: symbol.offsetY
            };
        }).filter(function (symbol) {
            return !!symbol.file;
        }).slice(0, 3));
    }

    function normalizeItem(item) {
        var symbols;

        item = item && typeof item === 'object' ? item : {};
        symbols = collectItemSymbols(item);

        return {
            tag: normalizeStoredTag(item.tag || item.code || item.map_tag || item.mapTag || ''),
            color: normalizeColor(item.color || item.polygon_color || item.polygonColor || ''),
            globe_label: String(item.globe_label || item.globeLabel || item.name_ko || item.globe_name || '').trim(),
            page: String(item.page || item.wiki_title || item.title || '').trim(),
            label: String(item.label || item.panel_label || item.panelLabel || item.name || item.page || item.wiki_title || '').trim(),
            external_url: String(item.external_url || item.url || '').trim(),
            symbols: symbols,
            hover_parent: normalizeTagInput(item.hover_parent || item.hoverParent || item.parent_tag || item.parentTag || item.parent || ''),
            hover_relation: normalizeRelation(item.hover_relation || item.hoverRelation || item.relation || ''),
            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.relation_options = normalizeRelationOptions(payload.relation_options || payload.relationOptions || payload.relations || state.relation_options);
        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 getItemSymbol(item, index) {
        index = Math.max(0, Math.min(2, parseInt(index, 10) || 0));
        if (!item.symbols || !Array.isArray(item.symbols)) item.symbols = collectItemSymbols(item);
        while (item.symbols.length < 3) item.symbols.push(normalizeSymbol({}));
        item.symbols[index] = normalizeSymbol(item.symbols[index]);
        return item.symbols[index];
    }

    function getItemPrimaryFlag(item) {
        return getItemSymbol(item, 0);
    }

    function getItemPrimarySyb(item) {
        return getItemSymbol(item, 1);
    }

    function normalizeParentPage(value, page) {
        var parent = String(value || '').trim().replace(/\s+/g, ' ');
        var current = String(page || '').trim();

        if (!parent || parent === current) return '';
        return parent;
    }

    function normalizeRelation(value) {
        return String(value || '').trim().replace(/\s+/g, ' ');
    }

    function pushUniqueRelationOption(list, value) {
        var relation = normalizeRelation(value);
        if (!relation || list.indexOf(relation) !== -1) return;
        list.push(relation);
    }

    function normalizeRelationOptions(value) {
        var list = [];

        if (Array.isArray(value)) {
            value.forEach(function (item) {
                pushUniqueRelationOption(list, item);
            });
        } else if (value && typeof value === 'object') {
            Object.keys(value).forEach(function (key) {
                pushUniqueRelationOption(list, key);
                pushUniqueRelationOption(list, value[key]);
            });
        } else if (typeof value === 'string') {
            value.split(/[\n,|]+/).forEach(function (item) {
                pushUniqueRelationOption(list, item);
            });
        }

        return list.sort(compareWikiLabels);
    }

    function mergeRelationOptions() {
        var list = [];

        Array.prototype.slice.call(arguments).forEach(function (source) {
            normalizeRelationOptions(source).forEach(function (item) {
                pushUniqueRelationOption(list, item);
            });
        });

        return list.sort(compareWikiLabels);
    }

    function normalizeColor(value) {
        var text = String(value || '').trim();
        if (!text) return '';
        if (/^#[0-9a-f]{3}$/i.test(text) || /^#[0-9a-f]{6}$/i.test(text) || /^#[0-9a-f]{8}$/i.test(text)) return text.toUpperCase();
        return text;
    }

    function normalizeLinkMap(payload, year) {
        var source = payload && payload.items ? payload.items : {};
        var items = {};

        Object.keys(source).sort(compareWikiLabels).forEach(function (key) {
            var entry = source[key] || {};
            var rawPage = String(entry.page || key || '').trim();
            var tag = getEntryTag(entry, key);
            var mapKey = tag || rawPage;
            var symbols = collectFilledSymbols(entry);
            var primaryFlag = normalizeFileTitle(entry.flag_file || entry.flag || entry.flag_title || (symbols[0] && symbols[0].file) || '');

            if (!mapKey) return;
            if (!symbols.length && primaryFlag) symbols = [normalizeSymbol({ file: primaryFlag })];

            items[mapKey] = {
                tag: tag || normalizeTagInput(entry.tag || entry.code || key || ''),
                page: tag || rawPage,
                name_ko: String(entry.name_ko || entry.name || '').trim(),
                wiki_title: String(entry.wiki_title || entry.title || '').trim(),
                flag_file: primaryFlag,
                symbols: symbols,
                hover_parent: normalizeTagInput(
                    entry.hover_parent || entry.hoverParent || entry.parent_page || entry.parentPage || entry.parent ||
                    ((entry.hover_group || entry.hoverGroup || entry.group || '') && (entry.hover_group || entry.hoverGroup || entry.group || '') !== rawPage ? (entry.hover_group || entry.hoverGroup || entry.group || '') : '')
                ),
                hover_relation: normalizeRelation(entry.hover_relation || entry.hoverRelation || entry.relation || ''),
                enabled: entry.enabled !== false,
                color: normalizeColor(entry.color || entry.polygon_color || entry.polygonColor || entry.fill || entry.fill_color || entry.fillColor || entry.hex || entry.colour || ''),
                note: String(entry.note || '').trim()
            };
        });

        return {
            version: payload && payload.version ? payload.version : 1,
            year: payload && payload.year ? payload.year : parseInt(year || '1950', 10),
            description: payload && payload.description ? payload.description : (year || '1950') + ' nation hover/click link map.',
            relation_options: normalizeRelationOptions(payload && (payload.relation_options || payload.relationOptions || payload.relations)),
            items: items
        };
    }

    function getLinkMapTags(map) {
        var tags = {};
        var items = map && map.items ? map.items : {};

        Object.keys(items).forEach(function (key) {
            var tag = getEntryTag(items[key], key);
            if (tag) tags[tag] = true;
        });

        return tags;
    }

    function normalizePolygonColorIndex(payload) {
        var index = {};
        var features = payload && Array.isArray(payload.features) ? payload.features : [];

        features.forEach(function (feature) {
            var props = feature && feature.properties ? feature.properties : {};
            var tag = getEntryTag(props, props.page || props.tag || props.code || props.TAG || props.map_tag || props.mapTag || '');
            var color = normalizeColor(
                props.color || props.polygon_color || props.polygonColor || props.fill ||
                props.fill_color || props.fillColor || props.hex || props.colour || ''
            );
            var area = parseFloat(props.area_hint || props.area || props.shape_area || props.Shape_Area || '0') || 0;
            var previous;

            if (!tag || !color) return;
            previous = index[tag];

            if (!previous || area >= previous.area) {
                index[tag] = { color: color, area: area };
            }
        });

        return index;
    }

    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.polygonHitUrlTemplate = root.getAttribute('data-polygon-hit-url-template') || '/index.php/Special:Redirect/file/{year}_Polygon_Hit.geojson';
        this.baseUrl = root.getAttribute('data-link-base') || '/index.php/';
        this.state = null;
        this.linkMap = null;
        this.linkMapExists = false;
        this.linkMapDirty = false;
        this.mapTags = null;
        this.polygonColorMap = null;
        this.polygonColorMapLoaded = false;
        this.activeContinent = 'america';
        this.query = '';
        this.status = '';
        this.sourceExists = false;
        this.loading = false;
        this.dragSource = null;
        this.flagSearchKeyword = root.getAttribute('data-flag-search-keyword') || 'flag';
        this.sybSearchKeyword = root.getAttribute('data-syb-search-keyword') || 'syb';
        this.flagSearchLimit = Math.max(10, Math.min(200, parseInt(root.getAttribute('data-flag-search-limit') || '80', 10) || 80));
        this.flagFiles = null;
        this.flagFilesLoading = false;
        this.flagPicker = null;
        this.activeFlagInput = null;
        this.relationDialogOpen = false;
    }

    NationListManager.prototype.getListTitle = function () {
        return replaceYearTemplate(this.titleTemplate, this.year);
    };

    NationListManager.prototype.getListUrl = function () {
        return replaceYearTemplate(this.urlTemplate, this.year);
    };

    NationListManager.prototype.getMapTitle = function () {
        return replaceYearTemplate(this.mapTitleTemplate, this.year);
    };

    NationListManager.prototype.getMapUrl = function () {
        return replaceYearTemplate(this.mapUrlTemplate, this.year);
    };

    NationListManager.prototype.getPolygonHitUrl = function () {
        return replaceYearTemplate(this.polygonHitUrlTemplate, 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.applyKnownTagCorrections();
                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();
            })
            .then(function () {
                return self.loadPolygonColorMap();
            })
            .then(function () {
                self.mergePolygonColorsIntoItems();
            })
            .finally(function () {
                self.loading = false;
                self.render();
            });
    };

    NationListManager.prototype.loadMapWarnings = function () {
        var self = this;

        this.mapTags = null;
        this.linkMap = null;
        this.linkMapExists = false;
        this.linkMapDirty = false;

        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.linkMap = trimmed ? normalizeLinkMap(JSON.parse(trimmed), self.year) : normalizeLinkMap({}, self.year);
                self.linkMapExists = true;
                self.importRelationOptionsFromLinkMap();
                self.inferMissingTagsFromMap();
                self.mergeLinkMapIntoItems();
                self.pruneLinkMapToList();
                self.mapTags = getLinkMapTags(self.linkMap);
            })
            .catch(function () {
                self.linkMap = null;
                self.linkMapExists = false;
                self.linkMapDirty = false;
                self.mapTags = null;
            });
    };

    NationListManager.prototype.loadPolygonColorMap = function (force) {
        var self = this;

        if (!force && this.polygonColorMapLoaded) return Promise.resolve(this.polygonColorMap || {});

        this.polygonColorMap = null;
        this.polygonColorMapLoaded = false;

        return fetch(this.getPolygonHitUrl(), { 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.polygonColorMap = trimmed ? normalizePolygonColorIndex(JSON.parse(trimmed)) : {};
                self.polygonColorMapLoaded = true;
                return self.polygonColorMap;
            })
            .catch(function (err) {
                if (window.console && console.warn) console.warn('nation list polygon color load failed:', err);
                self.polygonColorMap = {};
                self.polygonColorMapLoaded = true;
                return self.polygonColorMap;
            });
    };

    NationListManager.prototype.loadFlagFiles = function (force) {
        var self = this;
        var apiUrl;
        var recentLimit;

        if (!force && this.flagFiles) return Promise.resolve(this.flagFiles);
        if (this.flagFilesLoading) {
            return new Promise(function (resolve) {
                var timer = setInterval(function () {
                    if (!self.flagFilesLoading) {
                        clearInterval(timer);
                        resolve(self.flagFiles || []);
                    }
                }, 80);
            });
        }

        this.flagFilesLoading = true;
        apiUrl = (window.mw && mw.util && mw.util.wikiScript) ? mw.util.wikiScript('api') : '/api.php';
        recentLimit = Math.max(250, Math.min(500, parseInt(this.flagSearchLimit || 80, 10) * 4 || 250));

        function fetchApi(params) {
            return fetch(apiUrl + '?' + new URLSearchParams(params).toString(), { credentials: 'same-origin', cache: 'no-store' })
                .then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status);
                    return res.json();
                });
        }

        function fromSearchPages(data) {
            var pages = data && data.query && data.query.pages ? data.query.pages : {};
            return Object.keys(pages).map(function (id) {
                var page = pages[id];
                var info = page.imageinfo && page.imageinfo[0] ? page.imageinfo[0] : {};
                var file = normalizeFileTitle(page.title || '');
                return {
                    title: page.title || fileDisplayTitle(file),
                    file: file,
                    thumb: info.thumburl || info.url || buildFileRedirectUrl(file),
                    url: info.url || buildFileRedirectUrl(file),
                    isRedirect: !!page.redirect
                };
            });
        }

        function fromAllImages(data, options) {
            var items = data && data.query && data.query.allimages ? data.query.allimages : [];
            options = options || {};
            return items.map(function (image) {
                var file = normalizeFileTitle(image.title || image.name || '');
                return {
                    title: image.title || fileDisplayTitle(file),
                    file: file,
                    thumb: image.thumburl || image.url || buildFileRedirectUrl(file),
                    url: image.url || buildFileRedirectUrl(file),
                    isRedirect: false,
                    isRecentUpload: !!options.recent,
                    source: options.source || 'allimages',
                    timestamp: image.timestamp || ''
                };
            }).filter(function (item) {
                return item.file && isManagedImageFileCandidate(item.file) && /\.(svg|png|jpe?g|gif|webp|avif|ico)$/i.test(item.file);
            });
        }

        function fetchAllImages(params, maxItems) {
            var collected = [];

            function step(extra) {
                var query = Object.assign({}, params, extra || {});
                return fetchApi(query).then(function (data) {
                    var items = data && data.query && data.query.allimages ? data.query.allimages : [];
                    collected = collected.concat(items);

                    if (data && data.continue && collected.length < maxItems) {
                        return step(data.continue);
                    }

                    return { query: { allimages: collected.slice(0, maxItems) } };
                });
            }

            return step();
        }

        return Promise.all([
            fetchApi({
                action: 'query',
                generator: 'search',
                gsrsearch: this.flagSearchKeyword,
                gsrnamespace: '6',
                gsrlimit: String(this.flagSearchLimit),
                prop: 'info|imageinfo',
                redirects: '1',
                iiprop: 'url|mime',
                iiurlwidth: '32',
                format: 'json'
            }).then(fromSearchPages).catch(function (err) {
                if (window.console && console.warn) console.warn('nation list flag indexed search failed:', err);
                return [];
            }),
            fetchApi({
                action: 'query',
                generator: 'search',
                gsrsearch: this.sybSearchKeyword,
                gsrnamespace: '6',
                gsrlimit: String(this.flagSearchLimit),
                prop: 'info|imageinfo',
                redirects: '1',
                iiprop: 'url|mime',
                iiurlwidth: '32',
                format: 'json'
            }).then(fromSearchPages).catch(function (err) {
                if (window.console && console.warn) console.warn('nation list syb indexed search failed:', err);
                return [];
            }),
            fetchApi({
                action: 'query',
                list: 'allimages',
                aisort: 'timestamp',
                aidir: 'descending',
                ailimit: String(recentLimit),
                aiprop: 'url|mime|timestamp',
                aiurlwidth: '32',
                format: 'json'
            }).then(function (data) {
                return fromAllImages(data, { recent: true, source: 'recent' });
            }).catch(function (err) {
                if (window.console && console.warn) console.warn('nation list recent file scan failed:', err);
                return [];
            }),
            fetchAllImages({
                action: 'query',
                list: 'allimages',
                aiprefix: 'Flag',
                aisort: 'name',
                aidir: 'ascending',
                ailimit: '500',
                aiprop: 'url|mime|timestamp',
                aiurlwidth: '32',
                format: 'json'
            }, 3000).then(function (data) {
                return fromAllImages(data, { recent: false, source: 'flag-prefix' });
            }).catch(function (err) {
                if (window.console && console.warn) console.warn('nation list flag prefix scan failed:', err);
                return [];
            }),
            fetchAllImages({
                action: 'query',
                list: 'allimages',
                aiprefix: 'Syb',
                aisort: 'name',
                aidir: 'ascending',
                ailimit: '500',
                aiprop: 'url|mime|timestamp',
                aiurlwidth: '32',
                format: 'json'
            }, 3000).then(function (data) {
                return fromAllImages(data, { recent: false, source: 'syb-prefix' });
            }).catch(function (err) {
                if (window.console && console.warn) console.warn('nation list syb prefix scan failed:', err);
                return [];
            })
        ]).then(function (parts) {
            var list = cleanupFlagFileSearchList([].concat(parts[2] || [], parts[3] || [], parts[4] || [], parts[0] || [], parts[1] || []));
            self.flagFiles = list;
            return list;
        }).catch(function (err) {
            if (window.console && console.warn) console.warn('nation list flag file search failed:', err);
            self.flagFiles = [];
            return [];
        }).finally(function () {
            self.flagFilesLoading = false;
        });
    };

    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.getManagedRelationOptions = function () {
        return normalizeRelationOptions(this.state && this.state.relation_options);
    };

    NationListManager.prototype.setManagedRelationOptions = function (options) {
        if (!this.state) this.state = makeEmptyState(this.year);
        this.state.relation_options = normalizeRelationOptions(options || []);
        if (this.linkMap) {
            this.linkMap.relation_options = this.state.relation_options.slice();
            this.linkMapDirty = true;
        }
    };

    NationListManager.prototype.getUsedRelationOptions = function () {
        var used = [];

        this.getAllItems().forEach(function (row) {
            pushUniqueRelationOption(used, row.item && row.item.hover_relation);
        });

        if (this.linkMap && this.linkMap.items) {
            Object.keys(this.linkMap.items).forEach(function (key) {
                pushUniqueRelationOption(used, this.linkMap.items[key] && this.linkMap.items[key].hover_relation);
            }, this);
        }

        return used.sort(compareWikiLabels);
    };

    NationListManager.prototype.getRelationOptions = function () {
        return mergeRelationOptions(
            this.getManagedRelationOptions(),
            this.linkMap && this.linkMap.relation_options,
            this.getUsedRelationOptions()
        );
    };

    NationListManager.prototype.importRelationOptionsFromLinkMap = function () {
        var before = this.getManagedRelationOptions();
        var merged = mergeRelationOptions(before, this.linkMap && this.linkMap.relation_options, this.getUsedRelationOptions());

        if (!this.state) this.state = makeEmptyState(this.year);
        this.state.relation_options = merged;
        return merged.length - before.length;
    };

    NationListManager.prototype.getParentOptions = function () {
        var used = {};
        var list = [];

        this.getAllItems().forEach(function (row) {
            var tag = getItemTag(row.item);
            if (tag && !used[tag]) {
                used[tag] = { tag: tag, label: row.item.label || row.item.page || row.item.globe_label || '' };
                list.push(tag);
            }
        });

        return list.sort(compareWikiLabels).map(function (tag) {
            return used[tag];
        });
    };

    NationListManager.prototype.addRelationOption = function (value) {
        var relation = normalizeRelation(value);
        var options;

        if (!relation) {
            this.setStatus('관계 선택지 값이 비어 있습니다.');
            return;
        }

        options = this.getManagedRelationOptions();
        if (options.indexOf(relation) !== -1) {
            this.setStatus('이미 있는 관계 선택지입니다.');
            return;
        }

        pushUniqueRelationOption(options, relation);
        this.setManagedRelationOptions(options);
        this.relationDialogOpen = true;
        this.setStatus('관계 선택지 추가됨: ' + relation);
        this.render();
    };

    NationListManager.prototype.deleteRelationOption = function (value) {
        var relation = normalizeRelation(value);
        var options;

        if (!relation) return;

        options = this.getManagedRelationOptions().filter(function (item) {
            return item !== relation;
        });

        this.setManagedRelationOptions(options);
        this.relationDialogOpen = true;
        this.setStatus('관계 선택지 삭제됨: ' + relation);
        this.render();
    };

    NationListManager.prototype.openRelationDialog = function () {
        this.relationDialogOpen = true;
        this.render();
    };

    NationListManager.prototype.closeRelationDialog = function () {
        this.relationDialogOpen = false;
        this.render();
    };

    NationListManager.prototype.findUniqueLinkTagForItem = function (item) {
        var page = String(item && item.page || '').trim();
        var label = String(item && item.label || '').trim();
        var items = this.linkMap && this.linkMap.items ? this.linkMap.items : {};
        var candidates = [];

        if (!page && !label) return '';

        Object.keys(items).forEach(function (key) {
            var entry = items[key] || {};
            var tag = getEntryTag(entry, key);
            var wikiTitle = String(entry.wiki_title || '').trim();
            var nameKo = String(entry.name_ko || '').trim();

            if (!tag) return;
            if ((page && wikiTitle && wikiTitle === page) ||
                    (page && nameKo && nameKo === page) ||
                    (label && wikiTitle && wikiTitle === label) ||
                    (label && nameKo && nameKo === label)) {
                candidates.push(tag);
            }
        });

        candidates = candidates.filter(function (tag, index) {
            return tag && candidates.indexOf(tag) === index;
        });

        return candidates.length === 1 ? candidates[0] : '';
    };

    NationListManager.prototype.inferMissingTagsFromMap = function () {
        var count = 0;

        if (!this.linkMap || !this.linkMap.items || !this.state) return 0;

        this.getAllItems().forEach(function (row) {
            var item = row.item;
            var tag;

            if (!item || getItemTag(item)) return;
            tag = this.findUniqueLinkTagForItem(item);
            if (!tag) return;

            item.tag = tag;
            count += 1;
        }, this);

        return count;
    };

    NationListManager.prototype.mergeLinkMapIntoItems = function () {
        var map = this.linkMap && this.linkMap.items ? this.linkMap.items : null;
        var count = 0;

        if (!map || !this.state) return 0;

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var tag = getItemTag(item);
            var entry = tag ? map[tag] : null;
            var flag;

            if (!entry) return;

            if (!item.globe_label && entry.name_ko) { item.globe_label = String(entry.name_ko || '').trim(); count += 1; }
            if (!item.page && entry.wiki_title) { item.page = String(entry.wiki_title || '').trim(); count += 1; }
            if (!item.label && (item.page || entry.wiki_title || entry.name_ko)) { item.label = String(item.page || entry.wiki_title || entry.name_ko || '').trim(); count += 1; }
            var entryColor = normalizeColor(entry.color || entry.polygon_color || entry.polygonColor || entry.fill || entry.fill_color || entry.fillColor || entry.hex || entry.colour || '');
            if (entryColor && normalizeColor(item.color || '') !== entryColor) { item.color = entryColor; count += 1; }
            if (!item.hover_parent && entry.hover_parent) { item.hover_parent = normalizeTagInput(entry.hover_parent); count += 1; }
            if (!item.hover_relation && entry.hover_relation) { item.hover_relation = normalizeRelation(entry.hover_relation); count += 1; }

            flag = getItemPrimaryFlag(item);
            if (collectFilledSymbols(item).length === 0 && collectFilledSymbols(entry).length) {
                item.symbols = collectFilledSymbols(entry);
                count += 1;
            } else if (flag && !flag.file && entry.flag_file) {
                flag.file = normalizeFileTitle(entry.flag_file);
                count += 1;
            }
        });

        return count;
    };

    NationListManager.prototype.mergePolygonColorsIntoItems = function () {
        var colors = this.polygonColorMap || {};
        var count = 0;

        if (!this.state) return 0;

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var tag = getItemTag(item);
            var match = tag ? colors[tag] : null;
            var color = match && match.color ? normalizeColor(match.color) : '';

            if (!color) return;
            if (normalizeColor(item.color || '') === color) return;

            item.color = color;
            count += 1;
        });

        return count;
    };

    NationListManager.prototype.commitVisibleInputs = function () {
        var self = this;
        var count = 0;

        this.root.querySelectorAll('.nation-list-table tbody tr[data-index]').forEach(function (tr) {
            tr.querySelectorAll('input[data-field], select[data-field]').forEach(function (input) {
                self.updateRow(tr, input);
                count += 1;
            });
        });

        return count;
    };


    NationListManager.prototype.applyKnownTagCorrections = function () {
        return 0;
    };

    NationListManager.prototype.getListTagSet = function () {
        var set = {};

        if (!this.state) return set;

        this.getAllItems().forEach(function (row) {
            var tag = getItemTag(row.item);
            if (tag) set[tag] = true;
        });

        return set;
    };

    NationListManager.prototype.pruneLinkMapToList = function () {
        var map = this.linkMap && this.linkMap.items ? this.linkMap.items : null;
        var listTags = this.getListTagSet();
        var next = {};
        var removed = 0;

        if (!map) return 0;

        Object.keys(map).forEach(function (key) {
            var entry = map[key] || {};
            var tag = getEntryTag(entry, key);

            if (tag && listTags[tag]) {
                entry.tag = tag;
                next[tag] = entry;
            } else {
                removed += 1;
            }
        });

        if (removed) {
            this.linkMap.items = next;
            this.linkMapDirty = true;
            this.mapTags = getLinkMapTags(this.linkMap);
        }

        return removed;
    };

    NationListManager.prototype.validateTags = function () {
        var seen = {};
        var duplicate = [];
        var invalid = [];

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var source = String(item.tag || '').trim();
            var raw = normalizeStoredTag(source);
            var name = item.label || item.page || '(이름 없음)';

            if (!source) return;
            if (!raw) {
                invalid.push(name + ' = ' + source);
                return;
            }
            if (seen[raw]) {
                duplicate.push(raw + ' (' + seen[raw] + ' / ' + name + ')');
                return;
            }
            seen[raw] = name;
        });

        return { duplicate: duplicate, invalid: invalid };
    };

    NationListManager.prototype.renameLinkMapTag = function (oldTag, newTag) {
        var map = this.linkMap;
        var entry;

        oldTag = normalizeTagInput(oldTag);
        newTag = normalizeTagInput(newTag);

        if (!map || !map.items || !isValidTag(oldTag) || !isValidTag(newTag) || oldTag === newTag) return false;
        if (!map.items[oldTag] || map.items[newTag]) return false;

        entry = map.items[oldTag];
        delete map.items[oldTag];
        entry.tag = newTag;
        entry.page = newTag;
        if (normalizeTagInput(entry.hover_parent) === oldTag) entry.hover_parent = newTag;

        Object.keys(map.items).forEach(function (key) {
            if (normalizeTagInput(map.items[key] && map.items[key].hover_parent) === oldTag) {
                map.items[key].hover_parent = newTag;
            }
        });

        map.items[newTag] = entry;
        this.linkMapDirty = true;
        this.mapTags = getLinkMapTags(map);
        return true;
    };

    NationListManager.prototype.getListSymbols = function (item) {
        return collectFilledSymbols(item);
    };

    NationListManager.prototype.getListFlagFile = function (item) {
        var symbols = this.getListSymbols(item);
        return normalizeFileTitle(symbols[0] && symbols[0].file);
    };

    NationListManager.prototype.getLinkEntry = function (item) {
        var tag = getItemTag(item);
        return tag && this.linkMap && this.linkMap.items ? this.linkMap.items[tag] || null : null;
    };

    NationListManager.prototype.getLinkStatus = function (item) {
        var tag = getItemTag(item);
        var listSymbols = this.getListSymbols(item);
        var listFlag = normalizeFileTitle(listSymbols[0] && listSymbols[0].file);
        var entry;
        var mapSymbols;
        var mapFlag;

        if (item && item.external_url) {
            return { type: 'soft', text: '외부 URL' };
        }

        if (!tag) {
            return { type: 'missing', text: 'TAG 없음' };
        }

        if (!String(item && item.page || '').trim()) {
            return { type: 'incomplete', text: '문서 없음' };
        }

        if (!this.linkMap) {
            return { type: 'soft', text: '저장 시 생성' };
        }

        entry = this.getLinkEntry(item);
        if (!entry) {
            return { type: 'soft', text: '저장 시 생성' };
        }

        mapSymbols = collectFilledSymbols(entry);
        mapFlag = normalizeFileTitle(entry.flag_file || (mapSymbols[0] && mapSymbols[0].file) || '');
        if (symbolSignature(listSymbols) !== symbolSignature(mapSymbols)) {
            return {
                type: 'flag-mismatch',
                text: '저장 시 보정',
                mapFlag: mapSymbols.map(function (symbol) { return symbol.file; }).join(' / ') || mapFlag,
                listFlag: listSymbols.map(function (symbol) { return symbol.file; }).join(' / ') || listFlag
            };
        }

        return { type: 'ok', text: '연결됨' };
    };

    NationListManager.prototype.renderLinkStatus = function (item) {
        var status = this.getLinkStatus(item);
        var tag = getItemTag(item);
        var title = '';

        if (status.type === 'flag-mismatch') {
            title = 'Nation List: ' + status.listFlag + ' / Link Map: ' + status.mapFlag;
        }

        return '' +
            '<div class="nation-list-link-status is-' + escapeHtml(status.type) + '" title="' + escapeHtml(title) + '">' +
                '<span>' + escapeHtml(status.text) + '</span>' +
            '</div>';
    };

    NationListManager.prototype.ensureLinkMap = function () {
        if (!this.linkMap) {
            this.linkMap = normalizeLinkMap({}, this.year);
            this.linkMapExists = false;
        }
        if (!this.linkMap.items) this.linkMap.items = {};
        return this.linkMap;
    };

    NationListManager.prototype.makeLinkEntryFromItem = function (item) {
        var tag = getItemTag(item);
        var page = String(item && item.page || '').trim();
        var externalUrl = String(item && item.external_url || '').trim();
        var globeLabel = String(item && (item.globe_label || item.globeLabel) || page || item && item.label || '').trim();
        var symbols = this.getListSymbols(item);
        var flag = normalizeFileTitle(symbols[0] && symbols[0].file);

        return {
            tag: tag,
            page: tag,
            name_ko: globeLabel,
            wiki_title: externalUrl || page,
            external_url: externalUrl,
            flag_file: flag,
            symbols: symbols,
            hover_parent: normalizeTagInput(item && item.hover_parent || ''),
            hover_relation: normalizeRelation(item && item.hover_relation || ''),
            enabled: item && item.enabled !== false,
            color: normalizeColor(item && item.color || ''),
            note: String(item && item.note || '').trim()
        };
    };

    NationListManager.prototype.createLinkEntryForItem = function (item) {
        var map = this.ensureLinkMap();
        var tag = getItemTag(item);

        if (!tag || map.items[tag]) return false;

        map.items[tag] = this.makeLinkEntryFromItem(item);
        this.linkMapDirty = true;
        this.mapTags = getLinkMapTags(map);
        return true;
    };

    NationListManager.prototype.syncLinkMapFlagForItem = function (item) {
        var entry = this.getLinkEntry(item);
        var symbols = this.getListSymbols(item);
        var flag = normalizeFileTitle(symbols[0] && symbols[0].file);
        var currentSymbols;

        if (!entry) return false;
        currentSymbols = collectFilledSymbols(entry);
        if (symbolSignature(currentSymbols) === symbolSignature(symbols) &&
                normalizeFileKey(entry.flag_file || '') === normalizeFileKey(flag)) return false;

        entry.flag_file = flag;
        entry.symbols = symbols;
        this.linkMapDirty = true;
        return true;
    };

    NationListManager.prototype.syncLinkMapColorForItem = function (item) {
        var entry = this.getLinkEntry(item);
        var color = normalizeColor(item && item.color || '');

        if (!entry || !color) return false;
        if (normalizeColor(entry.color || '') === color) return false;

        entry.color = color;
        this.linkMapDirty = true;
        return true;
    };

    NationListManager.prototype.createMissingLinkEntries = function () {
        var count = 0;

        this.getAllItems().forEach(function (row) {
            if (row.item && row.item.enabled === false) return;
            if (this.createLinkEntryForItem(row.item)) count += 1;
        }, this);

        this.setStatus(count ? '링크맵 누락 항목 ' + count + '개 생성됨. 저장을 누르면 목록과 링크맵이 함께 저장됩니다.' : '생성할 링크맵 누락 항목이 없습니다.');
        this.render();
    };

    NationListManager.prototype.runLinkMapSync = function () {
        var imported = 0;
        var inferred = 0;
        var polygonColors = 0;
        var pushedFlags = 0;
        var pushedColors = 0;
        var relationOptions = 0;
        var pruned = 0;

        relationOptions = this.importRelationOptionsFromLinkMap();
        inferred = this.inferMissingTagsFromMap();
        imported = this.mergeLinkMapIntoItems();
        polygonColors = this.mergePolygonColorsIntoItems();

        this.getAllItems().forEach(function (row) {
            if (this.syncLinkMapFlagForItem(row.item)) pushedFlags += 1;
            if (this.syncLinkMapColorForItem(row.item)) pushedColors += 1;
        }, this);
        pruned = this.pruneLinkMapToList();

        this.setStatus(
            (relationOptions || inferred || imported || polygonColors || pushedFlags || pushedColors || pruned)
                ? '연동 재계산 완료: 관계 선택지 흡수 ' + relationOptions + '개, TAG 추정 ' + inferred + '개, Link Map 값 불러오기 ' + imported + '개, Polygon 색상 불러오기 ' + polygonColors + '개, 상징물 반영 ' + pushedFlags + '개, 색상 반영 ' + pushedColors + '개, 목록 밖 링크맵 TAG 제거 ' + pruned + '개.'
                : '연동 재계산 결과 변경할 항목이 없습니다.'
        );
        this.render();
    };

    NationListManager.prototype.syncAllLinkMapFlags = function () {
        var self = this;

        this.commitVisibleInputs();

        if (!this.polygonColorMapLoaded) {
            this.setStatus('연동 재계산: Polygon 색상 데이터를 불러오는 중...');
            return this.loadPolygonColorMap().then(function () {
                self.runLinkMapSync();
            });
        }

        this.runLinkMapSync();
        return Promise.resolve();
    };

    NationListManager.prototype.rebuildLinkMapFromList = function () {
        var map = this.ensureLinkMap();
        var existing = map.items || {};
        var next = {};

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var tag = getItemTag(item);
            var entry;

            if (!tag) return;
            entry = existing[tag] || {};
            next[tag] = Object.assign({}, entry, this.makeLinkEntryFromItem(item));
        }, this);

        map.items = next;
        map.relation_options = this.getManagedRelationOptions();
        this.linkMapDirty = true;
        this.mapTags = getLinkMapTags(map);
        return map;
    };

    NationListManager.prototype.linkMapToText = function () {
        var ordered = {};
        var map = this.ensureLinkMap();

        Object.keys(map.items || {}).sort(compareWikiLabels).forEach(function (key) {
            var item = Object.assign({}, map.items[key]);
            delete item._orphan;
            var tag = getEntryTag(item, key) || key;
            item.tag = normalizeTagInput(tag);
            item.page = item.tag || String(item.page || key || '').trim();
            item.symbols = collectFilledSymbols(item);
            item.flag_file = normalizeFileTitle(item.flag_file || (item.symbols[0] && item.symbols[0].file) || '');
            item.hover_parent = normalizeTagInput(item.hover_parent || '');
            item.hover_relation = normalizeRelation(item.hover_relation || '');
            ordered[tag] = item;
        });

        return JSON.stringify({
            version: map.version || 1,
            year: map.year || parseInt(this.year, 10),
            description: map.description || (this.year + ' nation hover/click link map.'),
            relation_options: this.getManagedRelationOptions(),
            items: ordered
        }, null, 2);
    };

    NationListManager.prototype.saveLinkMap = function () {
        var self = this;
        var title = this.getMapTitle();

        if (!window.mw || !mw.Api) {
            return Promise.reject(new Error('mw.Api를 사용할 수 없습니다.'));
        }

        return new Promise(function (resolve, reject) {
            new mw.Api().postWithToken('csrf', {
                action: 'edit',
                title: title,
                text: self.linkMapToText(),
                summary: 'Update ' + self.year + ' nation link map from Nation List Manager',
                format: 'json'
            }).done(function () {
                self.linkMapDirty = false;
                self.linkMapExists = true;
                self.inferMissingTagsFromMap();
                self.mapTags = getLinkMapTags(self.linkMap);
                resolve();
            }).fail(function (code, data) {
                reject(new Error(formatMwApiFailure(code, data)));
            });
        });
    };

    NationListManager.prototype.getWarnings = function () {
        var mapTags = this.linkMap ? getLinkMapTags(this.linkMap) : null;
        var listTags = {};
        var missingTag = [];
        var missingInMap = [];
        var missingInList = [];
        var flagMismatch = [];
        var tagValidation = this.validateTags();

        if (mapTags === null) {
            return [{ type: 'soft', text: this.year + '년 링크맵을 불러오지 못했습니다. 링크맵 문서가 없으면 이 경고는 정상입니다.' }];
        }

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var tag = getItemTag(item);
            var label = item.label || item.page || '(이름 없음)';
            var status;

            if (item.enabled === false || item.external_url) return;

            if (!tag) {
                missingTag.push(label);
                return;
            }

            listTags[tag] = true;
            if (!mapTags[tag]) {
                missingInMap.push(tag + ':' + label + ' (저장 시 자동 생성)');
                return;
            }

            status = this.getLinkStatus(item);
            if (status.type === 'flag-mismatch') flagMismatch.push(tag);
        }, this);

        Object.keys(mapTags).forEach(function (tag) {
            if (!listTags[tag]) missingInList.push(tag);
        });

        if (!missingTag.length && !missingInMap.length && !missingInList.length && !flagMismatch.length && !tagValidation.duplicate.length && !tagValidation.invalid.length) {
            return [{ type: 'ok', text: '국가 목록과 링크맵이 TAG 기준으로 맞습니다. 상징물 1은 Nation List 기준으로 연동됩니다.' }];
        }

        return [
            tagValidation.duplicate.length ? { type: 'warn', text: '중복 TAG: ' + tagValidation.duplicate.slice(0, 40).join(', ') + (tagValidation.duplicate.length > 40 ? ' 외 ' + (tagValidation.duplicate.length - 40) + '개' : '') } : null,
            tagValidation.invalid.length ? { type: 'warn', text: '3글자가 아닌 TAG: ' + tagValidation.invalid.slice(0, 40).join(', ') + (tagValidation.invalid.length > 40 ? ' 외 ' + (tagValidation.invalid.length - 40) + '개' : '') } : null,
            missingTag.length ? { type: 'warn', text: '목록 항목에 TAG 없음: ' + missingTag.slice(0, 80).join(', ') + (missingTag.length > 80 ? ' 외 ' + (missingTag.length - 80) + '개' : '') } : null,
            missingInMap.length ? { type: 'soft', text: '링크맵 자동 생성 대기: ' + missingInMap.slice(0, 80).join(', ') + (missingInMap.length > 80 ? ' 외 ' + (missingInMap.length - 80) + '개' : '') } : null,
            missingInList.length ? { type: 'warn', text: '링크맵에는 있으나 목록에 없는 TAG: ' + missingInList.slice(0, 80).join(', ') + (missingInList.length > 80 ? ' 외 ' + (missingInList.length - 80) + '개' : '') } : null,
            flagMismatch.length ? { type: 'warn', text: 'Nation List 상징물 1과 링크맵 국기가 다른 TAG: ' + flagMismatch.slice(0, 80).join(', ') + (flagMismatch.length > 80 ? ' 외 ' + (flagMismatch.length - 80) + '개' : '') } : null
        ].filter(Boolean);
    };

    NationListManager.prototype.toText = function () {
        this.applyKnownTagCorrections();
        var out = normalizeState(this.state, this.year);
        out.year = parseInt(this.year, 10);
        out.description = this.state.description || ('시대 ' + this.year + '년 당대 존재 국가 패널 데이터. Manage:Nation_List에서 편집한다.');
        out.relation_options = this.getManagedRelationOptions();
        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.commitVisibleInputs();
        this.applyKnownTagCorrections();

        var tagValidation = this.validateTags();
        var tagWarning = tagValidation.duplicate.length || tagValidation.invalid.length;

        if (tagWarning) {
            this.setStatus('TAG 경고가 남아 있지만 저장은 계속합니다. 중복/비정상 TAG 항목은 연동에서 제외됩니다.');
        }

        this.rebuildLinkMapFromList();
        this.setStatus('상징물 보정값 계산 중...');

        this.computeFlagOffsetsForSave().then(function () {
            self.setStatus(title + ' 저장 중...');

            new mw.Api().postWithToken('csrf', {
                action: 'edit',
                title: title,
                text: self.toText(),
                summary: 'Update ' + self.year + ' nation list',
                format: 'json'
            }).done(function () {
                self.sourceExists = true;

                if (!self.linkMapDirty) {
                    self.setStatus(title + ' 저장 완료');
                    self.loadMapWarnings().then(function () { self.render(); });
                    return;
                }

                self.setStatus(title + ' 저장 완료 · 통합 링크맵 저장 중...');
                self.saveLinkMap().then(function () {
                    self.setStatus(title + ' 및 ' + self.getMapTitle() + ' 저장 완료');
                    return self.loadMapWarnings();
                }).then(function () {
                    self.render();
                }).catch(function (err) {
                    self.setStatus('목록 저장 완료 / 링크맵 저장 실패: ' + (err && err.message ? err.message : err));
                });
            }).fail(function (code, data) {
                self.setStatus('저장 실패: ' + formatMwApiFailure(code, data));
            });
        }).catch(function (err) {
            self.setStatus('상징물 보정 계산 실패: ' + (err && err.message ? err.message : err));
        });
    };

    NationListManager.prototype.computeFlagOffsetsForSave = function () {
        var self = this;
        var rows = [];
        var fontsReady;

        this.getAllItems().forEach(function (row) {
            var item = row.item || {};
            var label = item.label || item.page || item.external_url || '';
            var symbols = collectItemSymbols(item);

            symbols.forEach(function (flag) {
                if (!flag.file) return;

                if (flag.offsetMode === 'manual') {
                    flag.offsetY = getFlagOffsetY(flag);
                    return;
                }

                rows.push({ flag: flag, label: label });
            });

            item.symbols = symbols;
        });

        if (!rows.length) return Promise.resolve();

        fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready.catch(function () {}) : Promise.resolve();

        return fontsReady.then(function () {
            return self.measureFlagOffsets(rows);
        });
    };

    NationListManager.prototype.measureFlagOffsets = function (rows) {
        var mount = document.createElement('div');
        var waiters = [];
        var timeout;

        mount.className = 'nation-list-measure-stage';
        mount.setAttribute('aria-hidden', 'true');
        document.body.appendChild(mount);

        rows.forEach(function (row, index) {
            var item = document.createElement('span');
            var img = document.createElement('img');
            var label = document.createElement('span');
            var width = String(row.flag.width || '16px').trim() || '16px';

            item.className = 'clbi-nations-list-item nation-list-measure-item';
            item.setAttribute('data-measure-index', String(index));

            img.className = 'clbi-nations-list-flag' + (row.flag.border !== false ? ' has-border' : '');
            img.alt = '';
            img.src = buildFileRedirectUrl(row.flag.file);
            img.style.width = width;
            img.style.setProperty('--flag-y', '0px');

            label.className = 'clbi-nations-list-label nation-list-measure-label';
            label.textContent = row.label || '미지정';

            item.appendChild(img);
            item.appendChild(label);
            mount.appendChild(item);

            if (!img.complete) {
                waiters.push(new Promise(function (resolve) {
                    img.addEventListener('load', resolve, { once: true });
                    img.addEventListener('error', resolve, { once: true });
                }));
            }
        });

        timeout = new Promise(function (resolve) {
            window.setTimeout(resolve, 2200);
        });

        return Promise.race([Promise.all(waiters), timeout]).then(function () {
            rows.forEach(function (row, index) {
                var item = mount.querySelector('[data-measure-index="' + index + '"]');
                var img = item ? item.querySelector('.clbi-nations-list-flag') : null;
                var label = item ? item.querySelector('.nation-list-measure-label') : null;
                var imgRect;
                var labelRect;
                var offset = 0;

                if (img && label) {
                    imgRect = img.getBoundingClientRect();
                    labelRect = label.getBoundingClientRect();

                    if (imgRect.height && labelRect.height) {
                        offset = Math.round(((labelRect.top + labelRect.height / 2) - (imgRect.top + imgRect.height / 2)) * 10) / 10;
                    }
                }

                row.flag.offsetMode = 'auto';
                row.flag.offsetY = Math.round(clampNumber(offset, -3, 3));
            });
        }).finally(function () {
            mount.remove();
        });
    };

    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 relationCount = this.getManagedRelationOptions().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 + ' · 관계 ' + relationCount + ' · ' + (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="TAG, 지구본명, 문서명, 패널명, 상징물 검색" value="' + escapeHtml(this.query) + '">' +
                    '<button type="button" class="nation-list-map-sync-flags" title="TAG 기준으로 Link Map의 색상/표시명/문서명/관계 선택지를 불러오고, Nation List 기준 상징물 전체/색상/관계 선택지를 Link Map에 반영합니다.">연동 재계산</button>' +
                    '<button type="button" class="nation-list-relation-manage">관계 선택지 관리</button>' +
                    '<button type="button" class="nation-list-reload">다시 불러오기</button>' +
                    '<button type="button" class="nation-list-save">저장</button>' +
                    '<span class="nation-list-status">' + escapeHtml(this.status) + (this.linkMapDirty ? ' · 링크맵 수정됨' : '') + '</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>' +
                this.renderParentDatalist() +
                this.renderRelationDatalist() +
                this.renderRelationOptionDialog() +
                '<div class="nation-list-flag-picker" hidden></div>' +
            '</div>';

        this.bind();
        this.bindBrokenFlagFallbacks(this.root);
    };

    NationListManager.prototype.bindBrokenFlagFallbacks = function (scope) {
        var root = scope || this.root;
        if (!root) return;

        Array.prototype.slice.call(root.querySelectorAll('img.nation-list-flag-preview, .nation-list-flag-option img')).forEach(function (img) {
            if (img.getAttribute('data-missing-handler') === '1') return;
            img.setAttribute('data-missing-handler', '1');
            img.addEventListener('error', function () {
                var field;
                img.classList.add('is-missing');
                img.removeAttribute('src');
                if (img.closest) {
                    field = img.closest('.nation-list-flag-field');
                    if (field) {
                        field.classList.remove('has-preview');
                        field.classList.add('is-flag-missing');
                    }
                }
            });
        });
    };

    NationListManager.prototype.renderParentDatalist = function () {
        var id = 'nation-list-' + this.year + '-hover-parents';
        var items = this.getParentOptions();

        return '<datalist id="' + escapeHtml(id) + '">' + items.map(function (item) {
            var label = item.label ? ' label="' + escapeHtml(item.label) + '"' : '';
            return '<option value="' + escapeHtml(item.tag) + '"' + label + '></option>';
        }).join('') + '</datalist>';
    };

    NationListManager.prototype.renderRelationDatalist = function () {
        var id = 'nation-list-' + this.year + '-relations';
        var list = this.getRelationOptions();

        return '<datalist id="' + escapeHtml(id) + '">' + list.map(function (item) {
            return '<option value="' + escapeHtml(item) + '"></option>';
        }).join('') + '</datalist>';
    };

    NationListManager.prototype.renderRelationOptionDialog = function () {
        var options = this.getManagedRelationOptions();
        var used = {};
        var rows = '';

        if (!this.relationDialogOpen) return '';

        this.getAllItems().forEach(function (row) {
            var value = normalizeRelation(row.item && row.item.hover_relation);
            if (!value) return;
            used[value] = (used[value] || 0) + 1;
        });

        rows = options.map(function (item) {
            var count = used[item] || 0;
            return '' +
                '<div class="nation-list-relation-dialog-row" data-relation="' + escapeHtml(item) + '">' +
                    '<span class="nation-list-relation-dialog-name">' + escapeHtml(item) + '</span>' +
                    '<span class="nation-list-relation-dialog-count">' + (count ? escapeHtml(count + '행 사용 중') : '미사용') + '</span>' +
                    '<button type="button" class="nation-list-relation-delete" data-relation="' + escapeHtml(item) + '">삭제</button>' +
                '</div>';
        }).join('');

        if (!rows) rows = '<div class="nation-list-relation-dialog-empty">관계 선택지가 없습니다.</div>';

        return '' +
            '<div class="nation-list-dialog-backdrop">' +
                '<div class="nation-list-relation-dialog" role="dialog" aria-modal="true" aria-label="관계 선택지 관리">' +
                    '<div class="nation-list-relation-dialog-head">' +
                        '<div>' +
                            '<div class="nation-list-relation-dialog-title">관계 선택지 관리</div>' +
                            '<div class="nation-list-relation-dialog-note">Nation List Manager가 관리하는 관계 템플릿입니다. 저장하면 Nation List와 Link Map에 같이 반영됩니다.</div>' +
                        '</div>' +
                        '<button type="button" class="nation-list-relation-dialog-close" aria-label="닫기">×</button>' +
                    '</div>' +
                    '<div class="nation-list-relation-dialog-add">' +
                        '<input type="text" class="nation-list-relation-option-input" placeholder="관계 선택지 추가">' +
                        '<button type="button" class="nation-list-relation-option-add">추가</button>' +
                    '</div>' +
                    '<div class="nation-list-relation-dialog-list">' + rows + '</div>' +
                    '<div class="nation-list-relation-dialog-foot">입력칸의 선택 목록은 관리 선택지와 현재 사용 중인 관계값을 함께 보여줍니다.</div>' +
                '</div>' +
            '</div>';
    };

    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 symbols = collectItemSymbols(item).map(function (symbol) { return symbol.file; }).join(' ');
            var haystack = [item.tag, item.color, item.globe_label, item.page, item.label, item.external_url, symbols, item.hover_parent, item.hover_relation, 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-drag-col">이동</th>' +
                            '<th class="nation-list-tag-col">TAG</th>' +
                            '<th class="nation-list-color-col">색상</th>' +
                            '<th class="nation-list-name-col">지구본 표시명</th>' +
                            '<th class="nation-list-name-col">문서 이름</th>' +
                            '<th class="nation-list-name-col">패널 이름</th>' +
                            '<th class="nation-list-link-col">상태</th>' +
                            '<th class="nation-list-symbols-col">상징물</th>' +
                            '<th class="nation-list-parent-col">상위 국가</th>' +
                            '<th class="nation-list-relation-col">관계</th>' +
                            '<th class="nation-list-note-col">메모</th>' +
                            '<th class="nation-list-check-col">사용</th>' +
                            '<th class="nation-list-action-col">작업</th>' +
                        '</tr></thead>' +
                        '<tbody data-continent="' + escapeHtml(continent.key) + '" data-region="' + escapeHtml(region.key) + '">' + (items.length ? items.map(function (item) {
                            return this.renderItemRow(continent, region, item, region.items.indexOf(item));
                        }, this).join('') : '<tr><td colspan="13" class="nation-list-empty">이 지역에는 항목이 없습니다.</td></tr>') + '</tbody>' +
                    '</table>' +
                '</div>' +
            '</section>';
    };

    NationListManager.prototype.renderOffsetSelect = function (flag, symbolIndex) {
        var mode = getFlagOffsetMode(flag);
        var offset = getFlagOffsetY(flag);
        var values = [
            { value: 'auto', label: '자동' },
            { value: '-3', label: '-3px' },
            { value: '-2', label: '-2px' },
            { value: '-1', label: '-1px' },
            { value: '0', label: '0px' },
            { value: '1', label: '+1px' },
            { value: '2', label: '+2px' },
            { value: '3', label: '+3px' }
        ];
        var selected = mode === 'manual' ? String(offset) : 'auto';

        return '<select data-field="offset" data-symbol-index="' + escapeHtml(String(symbolIndex || 0)) + '" title="시대 목록에서 상징물과 텍스트 중심선을 맞추는 세로 보정값입니다. 자동은 저장할 때 계산됩니다.">' + values.map(function (item) {
            return '<option value="' + escapeHtml(item.value) + '"' + (item.value === selected ? ' selected' : '') + '>' + escapeHtml(item.label) + '</option>';
        }).join('') + '</select>';
    };

    NationListManager.prototype.renderSymbolField = function (symbol, index) {
        var file = normalizeFileTitle(symbol && symbol.file);
        var url = buildFileRedirectUrl(file);
        var hasPreview = isManagedImageFileCandidate(file) && !!url;
        var sybClass = isSybFileCandidate(file) ? ' is-syb' : '';

        return '' +
            '<div class="nation-list-flag-field nation-list-symbol-field' + sybClass + (hasPreview ? ' has-preview' : '') + '" data-file-kind="symbol" data-symbol-index="' + escapeHtml(String(index)) + '" data-flag-file="' + escapeHtml(file) + '">' +
                '<span class="nation-list-flag-thumb nation-list-symbol-thumb" aria-hidden="true">' + (hasPreview ? '<img class="nation-list-flag-preview nation-list-symbol-preview' + sybClass + '" src="' + escapeHtml(url) + '" alt="" data-flag-file="' + escapeHtml(file) + '">' : '') + '</span>' +
                '<input type="text" data-field="symbol_file" data-file-kind="symbol" data-symbol-index="' + escapeHtml(String(index)) + '" class="nation-list-flag-input nation-list-symbol-input" value="' + escapeHtml(file) + '" placeholder="Flag- 또는 Syb-파일명.svg">' +
            '</div>';
    };

    NationListManager.prototype.renderSymbolDetails = function (item) {
        var symbols = [getItemSymbol(item, 0), getItemSymbol(item, 1), getItemSymbol(item, 2)];
        var filled = symbols.filter(function (symbol) { return normalizeFileTitle(symbol && symbol.file); });
        var preview = filled.map(function (symbol, index) {
            var file = normalizeFileTitle(symbol && symbol.file);
            var url = buildFileRedirectUrl(file);
            var sybClass = isSybFileCandidate(file) ? ' is-syb' : '';
            if (!url) return '<span class="nation-list-symbol-summary-missing" title="' + escapeHtml(file) + '"></span>';
            return '<img class="nation-list-symbol-summary-img' + sybClass + '" src="' + escapeHtml(url) + '" alt="" data-flag-file="' + escapeHtml(file) + '">';
        }).join('');

        if (!preview) preview = '<span class="nation-list-symbol-summary-empty">없음</span>';

        return '' +
            '<details class="nation-list-symbol-details">' +
                '<summary class="nation-list-symbol-summary">' +
                    '<span class="nation-list-symbol-summary-preview">' + preview + '</span>' +
                    '<span class="nation-list-symbol-summary-text">' + filled.length + '/3</span>' +
                '</summary>' +
                '<div class="nation-list-symbol-list">' + symbols.map(function (symbol, index) {
                    return '' +
                        '<div class="nation-list-symbol-row" data-symbol-index="' + index + '">' +
                            '<div class="nation-list-symbol-row-head">상징물 ' + (index + 1) + '</div>' +
                            '<div class="nation-list-symbol-row-main">' + this.renderSymbolField(symbol, index) + '</div>' +
                            '<label class="nation-list-symbol-size">크기 <input type="text" data-field="symbol_width" data-symbol-index="' + index + '" value="' + escapeHtml(symbol.width) + '"></label>' +
                            '<label class="nation-list-symbol-border">테두리 <input type="checkbox" data-field="symbol_border" data-symbol-index="' + index + '"' + (symbol.border !== false ? ' checked' : '') + '></label>' +
                            '<label class="nation-list-symbol-offset">보정 ' + this.renderOffsetSelect(symbol, index) + '</label>' +
                        '</div>';
                }, this).join('') + '</div>' +
            '</details>';
    };

    NationListManager.prototype.renderItemRow = function (continent, region, item, index) {
        var symbol0 = getItemSymbol(item, 0);
        var symbol1 = getItemSymbol(item, 1);
        var symbol2 = getItemSymbol(item, 2);
        var pageValue = item.external_url ? item.external_url : item.page;
        var globeLabel = item.globe_label || item.page || item.label || '';
        var panelLabel = item.label || item.page || '';

        return '' +
            '<tr draggable="true" data-continent="' + escapeHtml(continent.key) + '" data-region="' + escapeHtml(region.key) + '" data-index="' + index + '">' +
                '<td class="nation-list-drag-col"><span class="nation-list-drag-handle" title="드래그해서 순서 변경">↕</span></td>' +
                '<td class="nation-list-tag-col"><input type="text" data-field="tag" value="' + escapeHtml(item.tag || '') + '" placeholder="AAA" maxlength="3"></td>' +
                '<td class="nation-list-color-col"><div class="nation-list-color-field"><span style="background:' + escapeHtml(item.color || '#333') + '"></span><input type="text" data-field="color" value="' + escapeHtml(item.color || '') + '" placeholder="#333333"></div></td>' +
                '<td class="nation-list-name-col"><input type="text" data-field="globe_label" value="' + escapeHtml(globeLabel) + '" placeholder="지구본 표시명"></td>' +
                '<td class="nation-list-name-col"><input type="text" data-field="page" value="' + escapeHtml(pageValue) + '" placeholder="문서명 또는 https://..."></td>' +
                '<td class="nation-list-name-col"><input type="text" data-field="label" value="' + escapeHtml(panelLabel) + '" placeholder="비우면 문서 이름"></td>' +
                '<td class="nation-list-link-col">' + this.renderLinkStatus(item) + '</td>' +
                '<td class="nation-list-symbols-col">' + this.renderSymbolDetails(item) + '</td>' +
                '<td class="nation-list-parent-col"><input type="text" data-field="hover_parent" list="nation-list-' + escapeHtml(this.year) + '-hover-parents" value="' + escapeHtml(item.hover_parent || '') + '" placeholder="비우면 최상위" maxlength="3"></td>' +
                '<td class="nation-list-relation-col"><input type="text" data-field="hover_relation" list="nation-list-' + escapeHtml(this.year) + '-relations" value="' + escapeHtml(item.hover_relation || '') + '" placeholder="본국/속국/식민지"></td>' +
                '<td class="nation-list-note-col"><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');
        var mapSyncFlags = this.root.querySelector('.nation-list-map-sync-flags');
        var relationManage = this.root.querySelector('.nation-list-relation-manage');
        var relationDialogClose = this.root.querySelector('.nation-list-relation-dialog-close');
        var relationOptionInput = this.root.querySelector('.nation-list-relation-option-input');
        var relationOptionAdd = this.root.querySelector('.nation-list-relation-option-add');

        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); });
        if (mapSyncFlags) mapSyncFlags.addEventListener('click', function () { self.syncAllLinkMapFlags(); });
        if (relationManage) relationManage.addEventListener('click', function () { self.openRelationDialog(); });
        if (relationDialogClose) relationDialogClose.addEventListener('click', function () { self.closeRelationDialog(); });
        if (relationOptionAdd && relationOptionInput) {
            relationOptionAdd.addEventListener('click', function () {
                self.addRelationOption(relationOptionInput.value);
            });
            relationOptionInput.addEventListener('keydown', function (event) {
                if (event.key === 'Enter') {
                    event.preventDefault();
                    self.addRelationOption(relationOptionInput.value);
                }
                if (event.key === 'Escape') {
                    event.preventDefault();
                    self.closeRelationDialog();
                }
            });
        }

        this.root.querySelectorAll('.nation-list-relation-delete').forEach(function (button) {
            button.addEventListener('click', function () {
                var relation = button.getAttribute('data-relation') || '';
                if (!relation) return;
                self.deleteRelationOption(relation);
            });
        });

        this.root.querySelectorAll('.nation-list-dialog-backdrop').forEach(function (backdrop) {
            backdrop.addEventListener('click', function (event) {
                if (event.target === backdrop) self.closeRelationDialog();
            });
        });

        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], select[data-field]').forEach(function (input) {
                input.addEventListener('input', function () {
                    self.updateRow(tr, input);
                    if (input.getAttribute('data-field') === 'symbol_file' || input.getAttribute('data-field') === 'flag' || input.getAttribute('data-field') === 'syb') {
                        self.updateFlagInputPreview(input);
                        self.showFlagPicker(input);
                    }
                });
                input.addEventListener('change', function () {
                    var field = input.getAttribute('data-field');
                    self.updateRow(tr, input);
                    if (field === 'symbol_file' || field === 'flag' || field === 'syb') self.updateFlagInputPreview(input);
                    if (field === 'hover_parent' || field === 'hover_relation') self.render();
                });
                if (input.getAttribute('data-field') === 'symbol_file' || input.getAttribute('data-field') === 'flag' || input.getAttribute('data-field') === 'syb') {
                    input.addEventListener('focus', function () { self.showFlagPicker(input); });
                    input.addEventListener('keydown', function (event) {
                        if (event.key === 'Escape') self.hideFlagPicker();
                    });
                }
            });

            tr.addEventListener('dragstart', function (event) {
                self.startDrag(tr, event);
            });

            tr.addEventListener('dragover', function (event) {
                self.dragOverRow(tr, event);
            });

            tr.addEventListener('dragleave', function () {
                tr.classList.remove('is-drop-before', 'is-drop-after');
            });

            tr.addEventListener('drop', function (event) {
                self.dropOnRow(tr, event);
            });

            tr.addEventListener('dragend', function () {
                self.clearDragState();
            });

            var createLink = tr.querySelector('.nation-list-link-create');
            if (createLink) {
                createLink.addEventListener('click', function () {
                    var row = self.getRowItem(tr);
                    if (row && self.createLinkEntryForItem(row.item)) {
                        self.setStatus('링크맵 항목 생성됨. 저장을 누르면 목록과 링크맵이 함께 저장됩니다.');
                        self.render();
                    }
                });
            }

            var syncOneFlag = tr.querySelector('.nation-list-link-sync-one');
            if (syncOneFlag) {
                syncOneFlag.addEventListener('click', function () {
                    var row = self.getRowItem(tr);
                    if (row && self.syncLinkMapFlagForItem(row.item)) {
                        self.setStatus('이 항목의 링크맵 상징물을 Nation List 기준으로 동기화했습니다.');
                        self.render();
                    }
                });
            }

            var del = tr.querySelector('.nation-list-delete');
            if (del) {
                del.addEventListener('click', function () {
                    self.deleteItem(tr);
                });
            }
        });


        this.root.querySelectorAll('.nation-list-table tbody[data-continent][data-region]').forEach(function (tbody) {
            tbody.addEventListener('dragover', function (event) {
                self.dragOverRegion(tbody, event);
            });

            tbody.addEventListener('drop', function (event) {
                self.dropOnRegion(tbody, event);
            });
        });

        document.addEventListener('pointerdown', function onDocPointerDown(event) {
            if (!self.root.contains(event.target)) {
                self.hideFlagPicker();
                document.removeEventListener('pointerdown', onDocPointerDown);
            }
        });
    };

    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 syb;
        var symbol;
        var symbolIndex;
        var value;

        if (!row) return;
        flag = getItemPrimaryFlag(row.item);
        syb = getItemPrimarySyb(row.item);
        symbolIndex = Math.max(0, Math.min(2, parseInt(input.getAttribute('data-symbol-index') || '0', 10) || 0));
        symbol = getItemSymbol(row.item, symbolIndex);
        value = input.type === 'checkbox' ? input.checked : input.value;

        if (field === 'tag') {
            var oldTag = getItemTag(row.item);
            var newTag = normalizeTagInput(value);
            input.value = newTag;
            row.item.tag = newTag;
            if (oldTag && isValidTag(newTag) && oldTag !== newTag) this.renameLinkMapTag(oldTag, newTag);
        } else if (field === 'color') {
            row.item.color = normalizeColor(value);
            input.value = row.item.color;
            if (input.previousSibling && input.previousSibling.style) input.previousSibling.style.background = row.item.color || '#333';
            this.syncLinkMapColorForItem(row.item);
        } else if (field === 'globe_label') {
            row.item.globe_label = String(value || '').trim();
        } else if (field === 'page') {
            value = String(value || '').trim();
            var oldPage = String(row.item.page || '').trim();
            if (/^https?:\/\//i.test(value)) {
                row.item.external_url = value;
                row.item.page = '';
            } else {
                row.item.page = value;
                row.item.external_url = '';
                if (!row.item.label || row.item.label === oldPage) {
                    row.item.label = value;
                    var labelInput = tr.querySelector('[data-field="label"]');
                    if (labelInput) labelInput.value = value;
                }
                if (!row.item.globe_label || row.item.globe_label === oldPage) {
                    row.item.globe_label = value;
                    var globeInput = tr.querySelector('[data-field="globe_label"]');
                    if (globeInput) globeInput.value = value;
                }
            }
        } else if (field === 'label') {
            row.item.label = String(value || '').trim();
        } else if (field === 'hover_parent') {
            row.item.hover_parent = normalizeTagInput(value);
            input.value = row.item.hover_parent;
        } else if (field === 'hover_relation') {
            row.item.hover_relation = normalizeRelation(value);
        } else if (field === 'symbol_file') {
            symbol.file = normalizeFileTitle(value);
            if (symbolIndex === 0) this.syncLinkMapFlagForItem(row.item);
        } else if (field === 'symbol_width') {
            symbol.width = String(value || '').trim() || '16px';
        } else if (field === 'symbol_border') {
            symbol.border = !!value;
        } else if (field === 'flag') {
            flag.file = normalizeFileTitle(value);
            this.syncLinkMapFlagForItem(row.item);
        } else if (field === 'width') {
            flag.width = String(value || '').trim() || '16px';
        } else if (field === 'border') {
            flag.border = !!value;
        } else if (field === 'syb') {
            syb.file = normalizeFileTitle(value);
        } else if (field === 'syb_width') {
            syb.width = String(value || '').trim() || '16px';
        } else if (field === 'syb_border') {
            syb.border = !!value;
        } else if (field === 'offset') {
            if (String(value) === 'auto') {
                symbol.offsetMode = 'auto';
            } else {
                symbol.offsetMode = 'manual';
                symbol.offsetY = Math.round(clampNumber(value, -3, 3));
            }
        } else if (field === 'note') {
            row.item.note = value;
        } else if (field === 'enabled') {
            row.item.enabled = !!value;
        }

        this.setStatus('수정됨');
    };

    NationListManager.prototype.showFlagPicker = function (input) {
        var self = this;
        var picker = this.root.querySelector('.nation-list-flag-picker');
        var query = normalizeFileTitle(input.value).toLowerCase();
        var kind = input.getAttribute('data-file-kind') || (input.getAttribute('data-field') === 'syb' ? 'syb' : 'flag');
        var label = kind === 'symbol' ? '상징물' : (kind === 'syb' ? 'SYB' : 'flag');
        var rect;
        var rootRect;

        if (!picker) return;
        this.activeFlagInput = input;
        this.flagPicker = picker;
        picker.setAttribute('data-file-kind', kind);
        picker.hidden = false;
        picker.innerHTML = '<div class="nation-list-flag-picker-loading">' + escapeHtml(label) + ' 파일 검색 중...</div>';

        rect = input.getBoundingClientRect();
        rootRect = this.root.getBoundingClientRect();
        picker.style.left = Math.max(0, rect.left - rootRect.left) + 'px';
        picker.style.top = Math.max(0, rect.bottom - rootRect.top + 2) + 'px';
        picker.style.width = Math.max(280, rect.width + 44) + 'px';

        this.loadFlagFiles().then(function (list) {
            var candidates = list.filter(function (item) {
                return isPickerFileCandidate(kind, item && item.file);
            });
            var filtered = sortFlagPickerFiles(candidates.filter(function (item) {
                var file = normalizeFileTitle(item && item.file).toLowerCase();
                return !query || file.indexOf(query) !== -1;
            }), query).slice(0, 96);

            if (!filtered.length && query) {
                filtered = sortFlagPickerFiles(candidates, '').slice(0, 96);
                picker.innerHTML = '<div class="nation-list-flag-picker-empty">일치 항목 없음 · 최근/전체 ' + escapeHtml(label) + ' 파일 표시</div>';
            } else if (!filtered.length) {
                picker.innerHTML = '<div class="nation-list-flag-picker-empty">' + escapeHtml(label) + ' 파일 없음 · 직접 입력 가능</div>';
                return;
            } else {
                picker.innerHTML = '';
            }

            picker.innerHTML += filtered.map(function (item) {
                return '' +
                    '<button type="button" class="nation-list-flag-option" data-file="' + escapeHtml(item.file) + '">' +
                        '<img src="' + escapeHtml(item.thumb || buildFileRedirectUrl(item.file)) + '" alt="" data-flag-file="' + escapeHtml(item.file) + '">' +
                        '<span>' + escapeHtml(item.file) + '</span>' +
                    '</button>';
            }).join('');

            self.bindBrokenFlagFallbacks(picker);
            picker.querySelectorAll('.nation-list-flag-option').forEach(function (btn) {
                btn.addEventListener('click', function () {
                    var file = btn.getAttribute('data-file') || '';
                    var tr;

                    if (!self.activeFlagInput) return;
                    self.activeFlagInput.value = file;
                    tr = self.activeFlagInput.closest('tr');
                    if (tr) self.updateRow(tr, self.activeFlagInput);
                    self.updateFlagInputPreview(self.activeFlagInput);
                    self.setStatus('수정됨');
                    self.hideFlagPicker();
                });
            });
        });
    };


    NationListManager.prototype.updateFlagInputPreview = function (input) {
        var field;
        var thumb;
        var file;
        var url;

        if (!input || !input.closest) return;
        field = input.closest('.nation-list-flag-field');
        if (!field) return;
        thumb = field.querySelector('.nation-list-flag-thumb');
        file = normalizeFileTitle(input.value);
        url = isPickerFileCandidate(input.getAttribute('data-file-kind') || (input.getAttribute('data-field') === 'syb' ? 'syb' : 'flag'), file) ? buildFileRedirectUrl(file) : '';

        field.setAttribute('data-flag-file', file);
        field.classList.remove('is-flag-missing');
        field.classList.toggle('is-syb', isSybFileCandidate(file));

        if (!thumb) return;
        if (!url) {
            thumb.innerHTML = '';
            field.classList.remove('has-preview');
            return;
        }

        thumb.innerHTML = '<img class="nation-list-flag-preview nation-list-symbol-preview' + (isSybFileCandidate(file) ? ' is-syb' : '') + '" src="' + escapeHtml(url) + '" alt="" data-flag-file="' + escapeHtml(file) + '">';
        field.classList.add('has-preview');
        this.bindBrokenFlagFallbacks(field);
    };

    NationListManager.prototype.hideFlagPicker = function () {
        var picker = this.flagPicker || this.root.querySelector('.nation-list-flag-picker');
        if (picker) {
            picker.hidden = true;
            picker.innerHTML = '';
        }
        this.activeFlagInput = null;
    };

    NationListManager.prototype.addItem = function (continentKey, regionKey) {
        var region = this.findRegion(continentKey, regionKey);
        if (!region) return;
        region.items = region.items || [];
        region.items.push(normalizeItem({ tag: '', page: '', label: '', symbols: [normalizeSymbol({}), normalizeSymbol({}), normalizeSymbol({})], 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.startDrag = function (tr, event) {
        var row = this.getRowItem(tr);
        if (!row) return;

        this.dragSource = {
            continent: tr.getAttribute('data-continent'),
            region: tr.getAttribute('data-region'),
            index: row.index
        };

        tr.classList.add('is-dragging');

        if (event && event.dataTransfer) {
            event.dataTransfer.effectAllowed = 'move';
            event.dataTransfer.setData('text/plain', JSON.stringify(this.dragSource));
        }
    };

    NationListManager.prototype.clearDragState = function () {
        this.dragSource = null;
        this.root.querySelectorAll('.is-dragging, .is-drop-before, .is-drop-after, .is-region-drop').forEach(function (node) {
            node.classList.remove('is-dragging', 'is-drop-before', 'is-drop-after', 'is-region-drop');
        });
    };

    NationListManager.prototype.dragOverRow = function (tr, event) {
        var rect;
        var after;

        if (!this.dragSource) return;
        event.preventDefault();
        rect = tr.getBoundingClientRect();
        after = event.clientY > rect.top + rect.height / 2;

        this.root.querySelectorAll('.is-drop-before, .is-drop-after').forEach(function (node) {
            if (node !== tr) node.classList.remove('is-drop-before', 'is-drop-after');
        });

        tr.classList.toggle('is-drop-before', !after);
        tr.classList.toggle('is-drop-after', after);

        if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
    };

    NationListManager.prototype.dragOverRegion = function (tbody, event) {
        if (!this.dragSource) return;
        event.preventDefault();
        tbody.classList.add('is-region-drop');
        if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
    };

    NationListManager.prototype.moveDraggedItem = function (targetContinent, targetRegion, targetIndex) {
        var source = this.dragSource;
        var sourceRegion;
        var destRegion;
        var item;

        if (!source) return false;

        sourceRegion = this.findRegion(source.continent, source.region);
        destRegion = this.findRegion(targetContinent, targetRegion);

        if (!sourceRegion || !destRegion || !sourceRegion.items || !destRegion.items) return false;
        if (source.index < 0 || source.index >= sourceRegion.items.length) return false;

        item = sourceRegion.items.splice(source.index, 1)[0];

        if (sourceRegion === destRegion && targetIndex > source.index) {
            targetIndex -= 1;
        }

        targetIndex = Math.max(0, Math.min(targetIndex, destRegion.items.length));
        destRegion.items.splice(targetIndex, 0, item);
        return true;
    };

    NationListManager.prototype.dropOnRow = function (tr, event) {
        var target = this.getRowItem(tr);
        var rect;
        var targetIndex;

        if (!this.dragSource || !target) return;
        event.preventDefault();
        event.stopPropagation();

        rect = tr.getBoundingClientRect();
        targetIndex = target.index + (event.clientY > rect.top + rect.height / 2 ? 1 : 0);

        if (this.moveDraggedItem(tr.getAttribute('data-continent'), tr.getAttribute('data-region'), targetIndex)) {
            this.setStatus('순서 변경됨');
            this.clearDragState();
            this.render();
        }
    };

    NationListManager.prototype.dropOnRegion = function (tbody, event) {
        var targetContinent;
        var targetRegion;
        var region;

        if (!this.dragSource) return;
        if (event.target && event.target.closest && event.target.closest('tr[data-index]')) return;
        event.preventDefault();

        targetContinent = tbody.getAttribute('data-continent');
        targetRegion = tbody.getAttribute('data-region');
        region = this.findRegion(targetContinent, targetRegion);

        if (!region) return;

        if (this.moveDraggedItem(targetContinent, targetRegion, region.items.length)) {
            this.setStatus('순서 변경됨');
            this.clearDragState();
            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);
        });
    }
}());