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"> <div class="stats-bar">
<span id="stats">0 / 0 placed</span> <span id="stats">0 / 0 placed</span>
<div class="filter-btns"> <div class="filter-btns">
<button id="toggle-tooltips" class="active" title="말풍선 켜기/끄기">💬 켜짐</button>
<button id="filter-all" class="active">All</button> <button id="filter-all" class="active">All</button>
<button id="filter-unplaced">To Place</button> <button id="filter-unplaced">To Place</button>
</div> </div>
@@ -458,6 +459,37 @@ let flags = [];
let nextId = 1; let nextId = 1;
let selectedFlagId = null; let selectedFlagId = null;
let filterMode = 'all'; 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 ────────────────────────────────────────── // ─── API Key UI ──────────────────────────────────────────
@@ -495,9 +527,13 @@ function esc(s) {
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]); ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]);
} }
function getFlagTooltipText(flag) { function getFlagTooltipHtml(flag) {
if (!flag.name) return flag.phrase || ''; const n = esc(flag.name || '');
return flag.phrase ? `${flag.name} · ${flag.phrase}` : 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) => { header.addEventListener('click', (e) => {
if (e.target.closest('.actions')) return; if (e.target.closest('.actions')) return;
flag.expanded = !flag.expanded; flag.expanded = !flag.expanded;
if (flag.expanded && flag.marker) setActiveTooltip(flag.id);
renderList(); renderList();
}); });
li.appendChild(header); li.appendChild(header);
@@ -616,7 +653,10 @@ function renderList() {
header.querySelector('.idx')?.addEventListener('click', (e) => { header.querySelector('.idx')?.addEventListener('click', (e) => {
e.stopPropagation(); 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) => { header.querySelector('[data-action="edit-name"]')?.addEventListener('click', (e) => {
e.stopPropagation(); openNameEdit(flag.id); e.stopPropagation(); openNameEdit(flag.id);
@@ -638,6 +678,7 @@ function removeFlag(id) {
const flag = flags.find(f => f.id === id); const flag = flags.find(f => f.id === id);
if (!flag || !flag.placed) return; if (!flag || !flag.placed) return;
if (flag.marker) map.removeLayer(flag.marker); if (flag.marker) map.removeLayer(flag.marker);
if (activeTooltipId === id) setActiveTooltip(null);
flag.placed = false; flag.lat = null; flag.lng = null; flag.placed = false; flag.lat = null; flag.lng = null;
flag.address = null; flag.marker = null; flag.address = null; flag.marker = null;
if (selectedFlagId === id) selectedFlagId = null; if (selectedFlagId === id) selectedFlagId = null;
@@ -674,13 +715,7 @@ document.getElementById('name-edit-apply').addEventListener('click', () => {
if (nameVal) { if (nameVal) {
flag.name = nameVal; flag.name = nameVal;
flag.phrase = phraseVal; flag.phrase = phraseVal;
if (flag.marker) { if (flag.marker) refreshMarkerTooltip(flag);
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' });
}
}
renderList(); renderList();
} }
document.getElementById('name-edit-modal').classList.remove('open'); document.getElementById('name-edit-modal').classList.remove('open');
@@ -721,38 +756,70 @@ document.getElementById('photo-overlay').addEventListener('click', () => {
// ─── Map Click ────────────────────────────────────────── // ─── Map Click ──────────────────────────────────────────
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
map.on('click', (e) => { 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); const flag = flags.find(f => f.id === selectedFlagId);
if (!flag || flag.placed) return; if (!flag || flag.placed) return;
const { lat, lng } = e.latlng; const { lat, lng } = e.latlng;
const displayNum = flag.number || flags.indexOf(flag) + 1; const displayNum = flag.number || flags.indexOf(flag) + 1;
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map); 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', () => { marker.on('dragend', () => {
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng; const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
scheduleGeocode(flag); renderList(); scheduleGeocode(flag); renderList();
}); });
marker.on('contextmenu', () => { removeFlag(flag.id); }); marker.on('contextmenu', () => { removeFlag(flag.id); });
marker.on('click', () => { setActiveTooltip(flag.id); });
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker; flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
refreshMarkerTooltip(flag);
selectedFlagId = null; selectedFlagId = null;
renderList(); renderList();
scheduleGeocode(flag); 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', () => { document.getElementById('filter-all').addEventListener('click', () => {
filterMode = 'all'; 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'); document.getElementById('filter-all').classList.add('active');
renderList(); renderList();
}); });
document.getElementById('filter-unplaced').addEventListener('click', () => { document.getElementById('filter-unplaced').addEventListener('click', () => {
filterMode = 'unplaced'; 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'); document.getElementById('filter-unplaced').classList.add('active');
renderList(); renderList();
}); });
@@ -764,36 +831,55 @@ document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
for (const f of flags) { if (f.marker) map.removeLayer(f.marker); } 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 data = await file.arrayBuffer();
const wb = XLSX.read(data, { type: 'array' }); const wb = XLSX.read(data, { type: 'array' });
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '' }); const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '' });
for (const row of rows) { for (const row of rows) {
const name = row.Name || row.name || row.이름 || row['Flag Name'] || Object.values(row)[0] || ''; let name = row.Name ?? row.name ?? row.이름 ?? row['Flag Name'];
const number = row.Number || row.number || row.숫자 || row['Flag Number'] || Object.values(row)[1] || ''; let number = row.Number ?? row.number ?? row.숫자 ?? row['Flag Number'];
const phrase = row.Phrase || row.phrase || row.문구 || row['Flag Phrase'] || row['Flag Text'] || row.Text || ''; let 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());
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); if (photo) flag.photoPath = String(photo);
let lat = parseFloat(row.Latitude || row.latitude || row.위도); let latRaw = row.Latitude ?? row.latitude ?? row.위도;
let lng = parseFloat(row.Longitude || row.longitude || row.경도); let lngRaw = row.Longitude ?? row.longitude ?? row.경도;
let lat = parseFloat(latRaw);
let lng = parseFloat(lngRaw);
if (!isNaN(lat) && !isNaN(lng)) { if (!isNaN(lat) && !isNaN(lng)) {
const displayNum = flag.number || flags.indexOf(flag) + 1; const displayNum = flag.number || flags.indexOf(flag) + 1;
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map); 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', () => { marker.on('dragend', () => {
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng; const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
scheduleGeocode(flag); renderList(); scheduleGeocode(flag); renderList();
}); });
marker.on('contextmenu', () => { removeFlag(flag.id); }); marker.on('contextmenu', () => { removeFlag(flag.id); });
marker.on('click', () => { setActiveTooltip(flag.id); });
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker; 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); else scheduleGeocode(flag);
} }
} }
@@ -1090,4 +1176,4 @@ document.getElementById('boundary-toggle').addEventListener('click', () => {
}); });
</script> </script>
</body> </body>
</html> </html>