참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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);
});
}
}());