Feat: Improve marker tooltips and Excel parsing

- Add toggle button for marker tooltips (Show All / Show Active)
- Update tooltip to use HTML formatting (bold name, secondary phrase)
- Improve Excel column parsing using nullish coalescing operator
- Add support for '사진' (photo) column in Excel import
This commit is contained in:
unknown
2026-05-31 13:37:36 +09:00
parent 35c2b4bbf7
commit 3b16bfbf29

View File

@@ -253,6 +253,7 @@ body { font-family: 'Segoe UI', sans-serif; display: flex; height: 100vh; color:
<div class="stats-bar">
<span id="stats">0 / 0 placed</span>
<div class="filter-btns">
<button id="toggle-tooltips" class="active" title="말풍선 켜기/끄기">💬 켜짐</button>
<button id="filter-all" class="active">All</button>
<button id="filter-unplaced">To Place</button>
</div>
@@ -458,6 +459,37 @@ let flags = [];
let nextId = 1;
let selectedFlagId = null;
let filterMode = 'all';
let showAllTooltips = true;
let activeTooltipId = null;
function refreshMarkerTooltip(flag) {
if (!flag.marker) return;
flag.marker.unbindTooltip();
const text = getFlagTooltipHtml(flag);
if (text) {
const isPermanent = showAllTooltips || activeTooltipId === flag.id;
flag.marker.bindTooltip(text, {
permanent: isPermanent,
direction: 'top',
offset: [0, -17],
className: 'flag-marker-tooltip'
});
}
}
function setActiveTooltip(flagId) {
if (showAllTooltips) return;
const prevId = activeTooltipId;
activeTooltipId = flagId;
if (prevId) {
const prevFlag = flags.find(f => f.id === prevId);
if (prevFlag) refreshMarkerTooltip(prevFlag);
}
if (flagId) {
const flag = flags.find(f => f.id === flagId);
if (flag) refreshMarkerTooltip(flag);
}
}
// ═══════════════════════════════════════════════════════════
// ─── API Key UI ──────────────────────────────────────────
@@ -495,9 +527,13 @@ function esc(s) {
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]);
}
function getFlagTooltipText(flag) {
if (!flag.name) return flag.phrase || '';
return flag.phrase ? `${flag.name} · ${flag.phrase}` : flag.name;
function getFlagTooltipHtml(flag) {
const n = esc(flag.name || '');
const p = esc(flag.phrase || '');
if (!n && !p) return '';
if (!n) return `<div style="text-align:center; line-height:1.3;">${p}</div>`;
if (!p) return `<div style="text-align:center; line-height:1.3;"><strong>${n}</strong></div>`;
return `<div style="text-align:center; line-height:1.3;"><strong>${n}</strong><br><span style="font-size:0.9em;color:#555;">${p}</span></div>`;
}
// ═══════════════════════════════════════════════════════════
@@ -578,6 +614,7 @@ function renderList() {
header.addEventListener('click', (e) => {
if (e.target.closest('.actions')) return;
flag.expanded = !flag.expanded;
if (flag.expanded && flag.marker) setActiveTooltip(flag.id);
renderList();
});
li.appendChild(header);
@@ -616,7 +653,10 @@ function renderList() {
header.querySelector('.idx')?.addEventListener('click', (e) => {
e.stopPropagation();
if (flag.marker) map.flyTo(flag.marker.getLatLng(), 17);
if (flag.marker) {
setActiveTooltip(flag.id);
map.flyTo(flag.marker.getLatLng(), 17);
}
});
header.querySelector('[data-action="edit-name"]')?.addEventListener('click', (e) => {
e.stopPropagation(); openNameEdit(flag.id);
@@ -638,6 +678,7 @@ function removeFlag(id) {
const flag = flags.find(f => f.id === id);
if (!flag || !flag.placed) return;
if (flag.marker) map.removeLayer(flag.marker);
if (activeTooltipId === id) setActiveTooltip(null);
flag.placed = false; flag.lat = null; flag.lng = null;
flag.address = null; flag.marker = null;
if (selectedFlagId === id) selectedFlagId = null;
@@ -674,13 +715,7 @@ document.getElementById('name-edit-apply').addEventListener('click', () => {
if (nameVal) {
flag.name = nameVal;
flag.phrase = phraseVal;
if (flag.marker) {
flag.marker.unbindTooltip();
const tooltipText = esc(getFlagTooltipText(flag));
if (tooltipText) {
flag.marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
}
}
if (flag.marker) refreshMarkerTooltip(flag);
renderList();
}
document.getElementById('name-edit-modal').classList.remove('open');
@@ -721,38 +756,70 @@ document.getElementById('photo-overlay').addEventListener('click', () => {
// ─── Map Click ──────────────────────────────────────────
// ═══════════════════════════════════════════════════════════
map.on('click', (e) => {
if (!selectedFlagId) { alert('Select a flag from the list first.'); return; }
let clearedTooltip = false;
if (!showAllTooltips && activeTooltipId) {
setActiveTooltip(null);
clearedTooltip = true;
}
if (!selectedFlagId) {
if (!clearedTooltip && flags.length > 0) alert('Select a flag from the list first.');
return;
}
const flag = flags.find(f => f.id === selectedFlagId);
if (!flag || flag.placed) return;
const { lat, lng } = e.latlng;
const displayNum = flag.number || flags.indexOf(flag) + 1;
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
const tooltipText = esc(getFlagTooltipText(flag));
if (tooltipText) marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
marker.on('dragend', () => {
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
scheduleGeocode(flag); renderList();
});
marker.on('contextmenu', () => { removeFlag(flag.id); });
marker.on('click', () => { setActiveTooltip(flag.id); });
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
refreshMarkerTooltip(flag);
selectedFlagId = null;
renderList();
scheduleGeocode(flag);
});
// ═══════════════════════════════════════════════════════════
// ─── Filters ────────────────────────────────────────────
// ─── Filters & Tooltip Toggle ───────────────────────────
// ═══════════════════════════════════════════════════════════
document.getElementById('toggle-tooltips').addEventListener('click', (e) => {
showAllTooltips = !showAllTooltips;
const btn = e.target;
if (showAllTooltips) {
btn.classList.add('active');
btn.textContent = '💬 켜짐';
activeTooltipId = null;
} else {
btn.classList.remove('active');
btn.textContent = '💬 꺼짐';
}
flags.forEach(f => {
if (f.placed) refreshMarkerTooltip(f);
});
});
document.getElementById('filter-all').addEventListener('click', () => {
filterMode = 'all';
document.querySelectorAll('.filter-btns button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.filter-btns button').forEach(b => {
if (b.id !== 'toggle-tooltips') b.classList.remove('active');
});
document.getElementById('filter-all').classList.add('active');
renderList();
});
document.getElementById('filter-unplaced').addEventListener('click', () => {
filterMode = 'unplaced';
document.querySelectorAll('.filter-btns button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.filter-btns button').forEach(b => {
if (b.id !== 'toggle-tooltips') b.classList.remove('active');
});
document.getElementById('filter-unplaced').classList.add('active');
renderList();
});
@@ -764,36 +831,55 @@ document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
for (const f of flags) { if (f.marker) map.removeLayer(f.marker); }
flags = []; nextId = 1; selectedFlagId = null;
flags = []; nextId = 1; selectedFlagId = null; activeTooltipId = null;
const data = await file.arrayBuffer();
const wb = XLSX.read(data, { type: 'array' });
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '' });
for (const row of rows) {
const name = row.Name || row.name || row.이름 || row['Flag Name'] || Object.values(row)[0] || '';
const number = row.Number || row.number || row.숫자 || row['Flag Number'] || Object.values(row)[1] || '';
const phrase = row.Phrase || row.phrase || row.문구 || row['Flag Phrase'] || row['Flag Text'] || row.Text || '';
if (!String(name).trim()) continue;
const flag = addFlag(String(name).trim(), String(number).trim(), String(phrase).trim());
let name = row.Name ?? row.name ?? row.이름 ?? row['Flag Name'];
let number = row.Number ?? row.number ?? row.숫자 ?? row['Flag Number'];
let phrase = row.Phrase ?? row.phrase ?? row.문구 ?? row['Flag Phrase'] ?? row['Flag Text'] ?? row.Text;
const photo = row.Photo || row.photo || '';
if (name === undefined && number === undefined) {
const vals = Object.values(row);
name = vals[0];
number = vals[1];
if (phrase === undefined) phrase = vals[2];
}
name = String(name ?? '').trim();
number = String(number ?? '').trim();
phrase = String(phrase ?? '').trim();
if (!name) continue;
const flag = addFlag(name, number, phrase);
const photo = row.Photo ?? row.photo ?? row.사진;
if (photo) flag.photoPath = String(photo);
let lat = parseFloat(row.Latitude || row.latitude || row.위도);
let lng = parseFloat(row.Longitude || row.longitude || row.경도);
let latRaw = row.Latitude ?? row.latitude ?? row.위도;
let lngRaw = row.Longitude ?? row.longitude ?? row.경도;
let lat = parseFloat(latRaw);
let lng = parseFloat(lngRaw);
if (!isNaN(lat) && !isNaN(lng)) {
const displayNum = flag.number || flags.indexOf(flag) + 1;
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
const tooltipText = esc(getFlagTooltipText(flag));
if (tooltipText) marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
marker.on('dragend', () => {
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
scheduleGeocode(flag); renderList();
});
marker.on('contextmenu', () => { removeFlag(flag.id); });
marker.on('click', () => { setActiveTooltip(flag.id); });
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
if (row.Address || row.address || row.주소) flag.address = String(row.Address || row.address || row.주소);
refreshMarkerTooltip(flag);
let addr = row.Address ?? row.address ?? row.주소;
if (addr) flag.address = String(addr);
else scheduleGeocode(flag);
}
}