From 3b16bfbf2901731f0de54018d6210cf9ece5ac80 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 31 May 2026 13:37:36 +0900 Subject: [PATCH] Feat: Improve marker tooltips and Excel parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- index.html | 146 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 116 insertions(+), 30 deletions(-) diff --git a/index.html b/index.html index d3702e7..27d0848 100644 --- a/index.html +++ b/index.html @@ -253,6 +253,7 @@ body { font-family: 'Segoe UI', sans-serif; display: flex; height: 100vh; color:
0 / 0 placed
+
@@ -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) { ({'&':'&','<':'<','>':'>','"':'"'})[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 `
${p}
`; + if (!p) return `
${n}
`; + return `
${n}
${p}
`; } // ═══════════════════════════════════════════════════════════ @@ -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); } } @@ -1090,4 +1176,4 @@ document.getElementById('boundary-toggle').addEventListener('click', () => { }); - + \ No newline at end of file