diff --git a/js/main.js b/js/main.js index 9c79b89..35e91e9 100644 --- a/js/main.js +++ b/js/main.js @@ -10,21 +10,21 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").startsWith("ko") ? "ko" : "en", translations = { - ko: { - blog: "블로그", - title: "나의 찾기", - keepScreenOn: "화면 켜짐 유지", - settings: "설정", - location: "위치", - currentPosition: "현재 위치", - gpsAccuracy: "GPS 정확도", - moveToCurrent: "현재 위치로 이동하기", - update: "업데이트", - lastUpdate: "마지막 업데이트", - updateTrigger: "업데이트 트리거", - autoCurrentUpdate: "현재 위치 자동 이동", - autoUpdate: "자동 업데이트", - forceUpdate: "업데이트 하기", + ko: { + blog: "블로그", + title: "나의 찾기", + keepScreenOn: "화면 켜짐 유지", + settings: "설정", + location: "위치", + currentPosition: "현재 위치", + gpsAccuracy: "GPS 정확도", + moveToCurrent: "현재 위치로 이동하기", + update: "업데이트", + lastUpdate: "마지막 업데이트", + updateTrigger: "업데이트 트리거", + autoCurrentUpdate: "현재 위치 자동 이동", + autoUpdate: "자동 업데이트", + forceUpdate: "업데이트 하기", battery: "배터리", batteryState: "배터리 상태", batteryLevel: "배터리 잔량", @@ -40,10 +40,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts activity: "활동", currentActivity: "현재 활동", activityAccuracy: "활동 정확도", - distance: "도보로 움직인 거리", - steps: "걸음 수", - ascended: "오른 계단", - descended: "내려간 계단", + distance: "도보로 움직인 거리", + steps: "걸음 수", + ascended: "오른 계단", + descended: "내려간 계단", etc: "기타", connection: "연결 방법", wifiConnection: "Wi-Fi", @@ -217,21 +217,21 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts openNaverMap: "Naver 지도", selectedPoint: "선택 지점", }, - en: { - blog: "Blog", - title: "Find My Device", - keepScreenOn: "Keep Screen On", - settings: "Settings", - location: "Location", - currentPosition: "Current Location", - gpsAccuracy: "GPS Accuracy", - moveToCurrent: "Move to Current Location", - update: "Update", - lastUpdate: "Last Update", - updateTrigger: "Update Trigger", - autoCurrentUpdate: "Current Location Auto Update", - autoUpdate: "Auto Update", - forceUpdate: "Force Update", + en: { + blog: "Blog", + title: "Find My Device", + keepScreenOn: "Keep Screen On", + settings: "Settings", + location: "Location", + currentPosition: "Current Location", + gpsAccuracy: "GPS Accuracy", + moveToCurrent: "Move to Current Location", + update: "Update", + lastUpdate: "Last Update", + updateTrigger: "Update Trigger", + autoCurrentUpdate: "Current Location Auto Update", + autoUpdate: "Auto Update", + forceUpdate: "Force Update", battery: "Battery", batteryState: "Battery State", batteryLevel: "Battery Level", @@ -245,12 +245,12 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts remainingChargeTime: "Remaining Charge", batteryCycleCount: "Battery Cycles", activity: "Activity", - currentActivity: "Current Activity", - activityAccuracy: "Activity Confidence", - distance: "Distance walked", - steps: "Steps", - ascended: "Floors Ascended", - descended: "Floors Descended", + currentActivity: "Current Activity", + activityAccuracy: "Activity Confidence", + distance: "Distance walked", + steps: "Steps", + ascended: "Floors Ascended", + descended: "Floors Descended", etc: "Others", connection: "Network Type", wifiConnection: "Wi-Fi", @@ -424,9 +424,9 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts openNaverMap: "Naver Map", selectedPoint: "Selected Point", }, - }, - mapState = { - map: null, + }, + mapState = { + map: null, marker: null, circle: null, trackLine: null, @@ -457,14 +457,14 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts }; let langCode = localStorage.getItem("currentLang") || deviceLang, lastAutoSwitchChecked, - lastUpdateSwitchChecked, - lastHash, - lastLat, - lastLng, - lastUpdateISOTime, - map, - marker, - circle, + lastUpdateSwitchChecked, + lastHash, + lastLat, + lastLng, + lastUpdateISOTime, + map, + marker, + circle, radiusInterval, _overrideRange, wakeLock = null; @@ -675,247 +675,247 @@ document.querySelectorAll('.locateRow').forEach(row => { row.setAttribute('role', 'button'); row.tabIndex = 0; }); - -document.getElementById('langToggle').addEventListener('click', () => { - langCode = langCode === 'ko' ? 'en' : 'ko'; - localStorage.setItem('currentLang', langCode); - applyLanguage(langCode); - + +document.getElementById('langToggle').addEventListener('click', () => { + langCode = langCode === 'ko' ? 'en' : 'ko'; + localStorage.setItem('currentLang', langCode); + applyLanguage(langCode); + runAfterFindAuth(() => fetchDeviceData(true).then(data => updateDOMWithData(data))); - - // locateBtn 즉시 재렌더링 - if (document.getElementById('locateBtn')) { - document.getElementById('labelMoveToCurrent').textContent = - langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location'; - } - - if (document.getElementById('autoUpdateBtn')) { - document.getElementById('labelAutoUpdate').textContent = - langCode === 'ko' ? '자동 업데이트' : 'Auto Update'; - } - - if (document.getElementById('HistoryResetBtn')) { - document.getElementById('labelHistoryReset').textContent = - langCode === 'ko' ? '초기화' : 'Reset'; - } - - if (document.getElementById('updateBtn')) { - document.getElementById('labelForceUpdate').textContent = - langCode === 'ko' ? '업데이트 하기' : 'Force Update'; - } -}); - -document.getElementById('locateBtn').addEventListener('click', () => { - if (_overrideRange) return; - if (hasValidCoords(lastLat, lastLng)) { - const latlng = new naver.maps.LatLng(lastLat, lastLng); - mapState.map.panTo(latlng); - } -}); - -// [REPLACE] 기존 초기 updateBtn 리스너 -document.getElementById('updateBtn').addEventListener('click', triggerForceUpdate); - -document.getElementById('autoPositionSwitch').addEventListener('change', () => { - syncLocateBtn(); - lastAutoSwitchChecked = document.getElementById('autoPositionSwitch').checked; -}); - -document.addEventListener('DOMContentLoaded', () => { - const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); - - if (!isMobile) { - const el = document.getElementById('mobileonly'); - if (el) el.remove(); - } - - const container = document.getElementById('mySettingContainer'); - container.style.userSelect = 'none'; - container.style.webkitUserSelect = 'none'; - container.style.msUserSelect = 'none'; - container.style.MozUserSelect = 'none'; - container.style.webkitTouchCallout = 'none'; - container.addEventListener('dragstart', e => e.preventDefault()); - + + // locateBtn 즉시 재렌더링 + if (document.getElementById('locateBtn')) { + document.getElementById('labelMoveToCurrent').textContent = + langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location'; + } + + if (document.getElementById('autoUpdateBtn')) { + document.getElementById('labelAutoUpdate').textContent = + langCode === 'ko' ? '자동 업데이트' : 'Auto Update'; + } + + if (document.getElementById('HistoryResetBtn')) { + document.getElementById('labelHistoryReset').textContent = + langCode === 'ko' ? '초기화' : 'Reset'; + } + + if (document.getElementById('updateBtn')) { + document.getElementById('labelForceUpdate').textContent = + langCode === 'ko' ? '업데이트 하기' : 'Force Update'; + } +}); + +document.getElementById('locateBtn').addEventListener('click', () => { + if (_overrideRange) return; + if (hasValidCoords(lastLat, lastLng)) { + const latlng = new naver.maps.LatLng(lastLat, lastLng); + mapState.map.panTo(latlng); + } +}); + +// [REPLACE] 기존 초기 updateBtn 리스너 +document.getElementById('updateBtn').addEventListener('click', triggerForceUpdate); + +document.getElementById('autoPositionSwitch').addEventListener('change', () => { + syncLocateBtn(); + lastAutoSwitchChecked = document.getElementById('autoPositionSwitch').checked; +}); + +document.addEventListener('DOMContentLoaded', () => { + const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); + + if (!isMobile) { + const el = document.getElementById('mobileonly'); + if (el) el.remove(); + } + + const container = document.getElementById('mySettingContainer'); + container.style.userSelect = 'none'; + container.style.webkitUserSelect = 'none'; + container.style.msUserSelect = 'none'; + container.style.MozUserSelect = 'none'; + container.style.webkitTouchCallout = 'none'; + container.addEventListener('dragstart', e => e.preventDefault()); + setupBackLink(); - + if (FINDMYDEVICE_AUTH_REQUIRED) { findmydeviceAuthPromise.then(ok => { if (!ok) renderAuthErrorPage(); }); } - - const btnArea = document.querySelector('body > div.btnArea'); - const pageModifyLink = btnArea?.querySelectorAll('a')[1]?.getAttribute('href') || ''; - if (btnArea) btnArea.remove(); - - // 페이지 수정 버튼 요소 생성 - const editRow = document.createElement('div'); - editRow.className = 'settingRow locateRow'; - editRow.style.cursor = 'pointer'; - editRow.innerHTML = `${langCode === 'ko' ? '페이지 수정' : 'Edit Page'}`; - editRow.addEventListener('click', () => { - window.location.href = pageModifyLink; - }); - - const settingTitle = [...document.querySelectorAll('.settingTitle')].find(el => el.textContent.trim() === (langCode === 'ko' ? '설정' : 'Settings')); - let settingSection = settingTitle?.nextElementSibling; - - // 삽입 기준: #mapContainer 다음 - const mapContainer = document.getElementById('mapContainer'); - const toggleContent = document.getElementById('toggleContent'); - - if (pageModifyLink) { - // 설정 섹션이 없으면 새로 생성 (mapContainer 아래) - if (!settingSection || !settingSection.classList.contains('settingSection')) { - const newTitle = document.createElement('div'); - newTitle.className = 'settingTitle'; - newTitle.innerHTML = `${langCode === 'ko' ? '설정' : 'Settings'}`; - - const newSection = document.createElement('div'); - newSection.className = 'settingSection'; - newSection.appendChild(editRow); - - if (mapContainer) { - toggleContent.insertBefore(newTitle, mapContainer.nextSibling); - toggleContent.insertBefore(newSection, newTitle.nextSibling); - } - } else { - // 설정 섹션이 이미 존재하는 경우 - const keepRow = settingSection.querySelector('#labelKeepScreenOn')?.closest('.settingRow'); - - if (keepRow) { - if (!isMobile) keepRow.remove(), settingSection.prepend(editRow); - else keepRow.insertAdjacentElement('afterend', editRow); - } else { - settingSection.appendChild(editRow); - } - } - } else { - // pageModifyLink 없음 → 셀렉터 존재 안 함 - if (!isMobile && settingSection && settingTitle) { - settingTitle.remove(); - settingSection.remove(); - } - } - - const tooltip = document.getElementById("ios-pwa-tooltip"); - const closeBtn = document.getElementById("close-tooltip"); - const isStandalone = window.navigator.standalone || window.matchMedia('(display-mode: standalone)').matches; - const tooltipClosed = getCookie('iosPwaTooltipClosed'); // 없으면 쿠키 만료 or 미설정 - - if (tooltip && closeBtn && isIosSafari() && !isStandalone && !tooltipClosed) { - function handleScroll() { - const scrollPercent = ((window.scrollY || document.documentElement.scrollTop) / - (document.documentElement.scrollHeight - document.documentElement.clientHeight)) * 100; - if (scrollPercent > 10) { - tooltip.classList.add("visible"); - } else { - tooltip.classList.remove("visible"); - } - } - - window.addEventListener("scroll", handleScroll, { passive: true }); - - closeBtn.addEventListener("click", function () { - tooltip.classList.remove("visible"); - setCookieHours("iosPwaTooltipClosed", "1", 12); // 12시간 동안 숨김 - window.removeEventListener("scroll", handleScroll); - }); - } - - const btn = document.getElementById('HistoryResetBtn'); - if (btn) { - // 라벨 - const label = document.getElementById('labelHistoryReset'); - (label || btn).textContent = (langCode === 'ko') ? '초기화' : 'Reset'; - - btn.addEventListener('click', () => { - // 1) 히스토리 모드 해제 → 라이브 복귀 - _overrideRange = null; - - // 2) 입력 요소 가져오기 (같은 스코프에서!) - const startEl = document.getElementById('rangeStart'); - const endEl = document.getElementById('rangeEnd'); - if (startEl && endEl) { - const pad = n => String(n).padStart(2, '0'); - const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`; - - // 기준 시각(현재) - const current = new Date(); - - // 시작: 24시간 전 → 정각 버림 - const start24 = new Date(current.getTime() - 24 * 3600 * 1000); - start24.setMinutes(0, 0, 0); - - // 종료: 현재 시각의 다음 정각 올림 - const endRoundUp = new Date(current); - endRoundUp.setMinutes(0, 0, 0); - if (current.getMinutes() > 0 || current.getSeconds() > 0 || current.getMilliseconds() > 0) { - endRoundUp.setHours(endRoundUp.getHours() + 1); - } - - // 입력값 세팅 - startEl.value = toLocalHour(start24); - endEl.value = toLocalHour(endRoundUp); - } - - // 3) 지도 히스토리 초기화 & 라이브 데이터 재요청 + 현재 위치로 이동 - resetHistoryLayer(); + + const btnArea = document.querySelector('body > div.btnArea'); + const pageModifyLink = btnArea?.querySelectorAll('a')[1]?.getAttribute('href') || ''; + if (btnArea) btnArea.remove(); + + // 페이지 수정 버튼 요소 생성 + const editRow = document.createElement('div'); + editRow.className = 'settingRow locateRow'; + editRow.style.cursor = 'pointer'; + editRow.innerHTML = `${langCode === 'ko' ? '페이지 수정' : 'Edit Page'}`; + editRow.addEventListener('click', () => { + window.location.href = pageModifyLink; + }); + + const settingTitle = [...document.querySelectorAll('.settingTitle')].find(el => el.textContent.trim() === (langCode === 'ko' ? '설정' : 'Settings')); + let settingSection = settingTitle?.nextElementSibling; + + // 삽입 기준: #mapContainer 다음 + const mapContainer = document.getElementById('mapContainer'); + const toggleContent = document.getElementById('toggleContent'); + + if (pageModifyLink) { + // 설정 섹션이 없으면 새로 생성 (mapContainer 아래) + if (!settingSection || !settingSection.classList.contains('settingSection')) { + const newTitle = document.createElement('div'); + newTitle.className = 'settingTitle'; + newTitle.innerHTML = `${langCode === 'ko' ? '설정' : 'Settings'}`; + + const newSection = document.createElement('div'); + newSection.className = 'settingSection'; + newSection.appendChild(editRow); + + if (mapContainer) { + toggleContent.insertBefore(newTitle, mapContainer.nextSibling); + toggleContent.insertBefore(newSection, newTitle.nextSibling); + } + } else { + // 설정 섹션이 이미 존재하는 경우 + const keepRow = settingSection.querySelector('#labelKeepScreenOn')?.closest('.settingRow'); + + if (keepRow) { + if (!isMobile) keepRow.remove(), settingSection.prepend(editRow); + else keepRow.insertAdjacentElement('afterend', editRow); + } else { + settingSection.appendChild(editRow); + } + } + } else { + // pageModifyLink 없음 → 셀렉터 존재 안 함 + if (!isMobile && settingSection && settingTitle) { + settingTitle.remove(); + settingSection.remove(); + } + } + + const tooltip = document.getElementById("ios-pwa-tooltip"); + const closeBtn = document.getElementById("close-tooltip"); + const isStandalone = window.navigator.standalone || window.matchMedia('(display-mode: standalone)').matches; + const tooltipClosed = getCookie('iosPwaTooltipClosed'); // 없으면 쿠키 만료 or 미설정 + + if (tooltip && closeBtn && isIosSafari() && !isStandalone && !tooltipClosed) { + function handleScroll() { + const scrollPercent = ((window.scrollY || document.documentElement.scrollTop) / + (document.documentElement.scrollHeight - document.documentElement.clientHeight)) * 100; + if (scrollPercent > 10) { + tooltip.classList.add("visible"); + } else { + tooltip.classList.remove("visible"); + } + } + + window.addEventListener("scroll", handleScroll, { passive: true }); + + closeBtn.addEventListener("click", function () { + tooltip.classList.remove("visible"); + setCookieHours("iosPwaTooltipClosed", "1", 12); // 12시간 동안 숨김 + window.removeEventListener("scroll", handleScroll); + }); + } + + const btn = document.getElementById('HistoryResetBtn'); + if (btn) { + // 라벨 + const label = document.getElementById('labelHistoryReset'); + (label || btn).textContent = (langCode === 'ko') ? '초기화' : 'Reset'; + + btn.addEventListener('click', () => { + // 1) 히스토리 모드 해제 → 라이브 복귀 + _overrideRange = null; + + // 2) 입력 요소 가져오기 (같은 스코프에서!) + const startEl = document.getElementById('rangeStart'); + const endEl = document.getElementById('rangeEnd'); + if (startEl && endEl) { + const pad = n => String(n).padStart(2, '0'); + const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`; + + // 기준 시각(현재) + const current = new Date(); + + // 시작: 24시간 전 → 정각 버림 + const start24 = new Date(current.getTime() - 24 * 3600 * 1000); + start24.setMinutes(0, 0, 0); + + // 종료: 현재 시각의 다음 정각 올림 + const endRoundUp = new Date(current); + endRoundUp.setMinutes(0, 0, 0); + if (current.getMinutes() > 0 || current.getSeconds() > 0 || current.getMilliseconds() > 0) { + endRoundUp.setHours(endRoundUp.getHours() + 1); + } + + // 입력값 세팅 + startEl.value = toLocalHour(start24); + endEl.value = toLocalHour(endRoundUp); + } + + // 3) 지도 히스토리 초기화 & 라이브 데이터 재요청 + 현재 위치로 이동 + resetHistoryLayer(); runAfterFindAuth(() => fetchDeviceData(true).then(data => { updateDOMWithData(data); if (data.latitude && data.longitude && mapState.map) { mapState.map.panTo(new naver.maps.LatLng(data.latitude, data.longitude)); } })); - }); - } - - (function initInstantHistorySearch() { - const startEl = document.getElementById('rangeStart'); - const endEl = document.getElementById('rangeEnd'); - if (!startEl || !endEl) return; - - const pad = n => String(n).padStart(2, '0'); - const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`; - const toServerTime = v => - v ? (v.includes('T') ? v.replace('T', ' ') : v) + (v.length === 16 ? ':00' : '') : null; - - const now = new Date(); - - // 시작: 현재 시각에서 24시간 전, 정각 버림 - const start24 = new Date(now.getTime() - 24 * 3600 * 1000); - start24.setMinutes(0, 0, 0); - - // 종료: 현재 시각, 다음 정각 올림 - const endRoundUp = new Date(now); - endRoundUp.setMinutes(0, 0, 0); - if (now.getMinutes() > 0 || now.getSeconds() > 0 || now.getMilliseconds() > 0) { - endRoundUp.setHours(endRoundUp.getHours() + 1); - } - - const qs = new URLSearchParams(window.location.search); - startEl.value = qs.get('startTime') - ? qs.get('startTime').replace(' ', 'T').slice(0, 16) - : toLocalHour(start24); - endEl.value = qs.get('endTime') - ? qs.get('endTime').replace(' ', 'T').slice(0, 16) - : toLocalHour(endRoundUp); - - const hasQS = qs.has('startTime') || qs.has('endTime'); - - const fire = debounce(async (userTriggered = false) => { - const start = startEl.value; - const end = endEl.value; - if (!start || !end) return; - - if (start > end) endEl.value = start; - - const range = { - startTime: toServerTime(startEl.value), - endTime: toServerTime(endEl.value) - }; - + }); + } + + (function initInstantHistorySearch() { + const startEl = document.getElementById('rangeStart'); + const endEl = document.getElementById('rangeEnd'); + if (!startEl || !endEl) return; + + const pad = n => String(n).padStart(2, '0'); + const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`; + const toServerTime = v => + v ? (v.includes('T') ? v.replace('T', ' ') : v) + (v.length === 16 ? ':00' : '') : null; + + const now = new Date(); + + // 시작: 현재 시각에서 24시간 전, 정각 버림 + const start24 = new Date(now.getTime() - 24 * 3600 * 1000); + start24.setMinutes(0, 0, 0); + + // 종료: 현재 시각, 다음 정각 올림 + const endRoundUp = new Date(now); + endRoundUp.setMinutes(0, 0, 0); + if (now.getMinutes() > 0 || now.getSeconds() > 0 || now.getMilliseconds() > 0) { + endRoundUp.setHours(endRoundUp.getHours() + 1); + } + + const qs = new URLSearchParams(window.location.search); + startEl.value = qs.get('startTime') + ? qs.get('startTime').replace(' ', 'T').slice(0, 16) + : toLocalHour(start24); + endEl.value = qs.get('endTime') + ? qs.get('endTime').replace(' ', 'T').slice(0, 16) + : toLocalHour(endRoundUp); + + const hasQS = qs.has('startTime') || qs.has('endTime'); + + const fire = debounce(async (userTriggered = false) => { + const start = startEl.value; + const end = endEl.value; + if (!start || !end) return; + + if (start > end) endEl.value = start; + + const range = { + startTime: toServerTime(startEl.value), + endTime: toServerTime(endEl.value) + }; + const shouldUseHistory = userTriggered || hasQS; const shouldRequestHistory = shouldUseHistory || !userTriggered; if (shouldUseHistory) { @@ -937,43 +937,43 @@ document.addEventListener('DOMContentLoaded', () => { if (shouldUseHistory && last && mapState.map) { mapState.map.panTo(new naver.maps.LatLng(last.lat, last.lng)); } - } catch (err) { - alert(langCode === 'ko' ? '기록을 불러오지 못했습니다.' : 'Failed to load history.'); - } - }, 400); - - ['change', 'input'].forEach(ev => { - startEl.addEventListener(ev, () => fire(true)); - endEl.addEventListener(ev, () => fire(true)); - }); - + } catch (err) { + alert(langCode === 'ko' ? '기록을 불러오지 못했습니다.' : 'Failed to load history.'); + } + }, 400); + + ['change', 'input'].forEach(ev => { + startEl.addEventListener(ev, () => fire(true)); + endEl.addEventListener(ev, () => fire(true)); + }); + runAfterFindAuth(() => fire(false)); - })(); -}); - -document.getElementById('wakeLockSwitch').addEventListener('change', async function () { - if (this.checked) { - try { - wakeLock = await navigator.wakeLock.request('screen'); - - wakeLock.addEventListener('release', () => { - wakeLock = null; - document.getElementById('wakeLockSwitch').checked = false; - }); - - } catch (err) { - this.checked = false; - } - } else { - if (wakeLock) { - wakeLock.release(); - wakeLock = null; - } - } -}); - -document.getElementById('autoUpdateSwitch').addEventListener('change', () => { - syncUpdateBtn(); + })(); +}); + +document.getElementById('wakeLockSwitch').addEventListener('change', async function () { + if (this.checked) { + try { + wakeLock = await navigator.wakeLock.request('screen'); + + wakeLock.addEventListener('release', () => { + wakeLock = null; + document.getElementById('wakeLockSwitch').checked = false; + }); + + } catch (err) { + this.checked = false; + } + } else { + if (wakeLock) { + wakeLock.release(); + wakeLock = null; + } + } +}); + +document.getElementById('autoUpdateSwitch').addEventListener('change', () => { + syncUpdateBtn(); const isChecked = document.getElementById('autoUpdateSwitch')?.checked; if (isChecked) { @@ -982,11 +982,11 @@ document.getElementById('autoUpdateSwitch').addEventListener('change', () => { callDeviceWebhook('auto_off'); } }); - -['pagehide', 'visibilitychange', 'blur', 'beforeunload'].forEach(eventType => { - window.addEventListener(eventType, function (e) { - - const switchEl = document.getElementById('autoUpdateSwitch'); + +['pagehide', 'visibilitychange', 'blur', 'beforeunload'].forEach(eventType => { + window.addEventListener(eventType, function (e) { + + const switchEl = document.getElementById('autoUpdateSwitch'); const isAutoUpdateOn = switchEl?.checked; if (isAutoUpdateOn) { @@ -994,11 +994,11 @@ document.getElementById('autoUpdateSwitch').addEventListener('change', () => { // 스위치 UI 강제 끔 switchEl.checked = false; - syncUpdateBtn(); - } - }); -}); - + syncUpdateBtn(); + } + }); +}); + setInterval(() => { if (!findmydeviceAuthenticated) return; if (document.getElementById('autoUpdateSwitch')?.checked) { @@ -1009,91 +1009,91 @@ setInterval(() => { setInterval(() => { if (!findmydeviceAuthenticated) return; if (lastUpdateISOTime) { - const el = document.getElementById("lastUpdateTime"); - if (el) el.textContent = getRelativeTime(lastUpdateISOTime); - } - - // 히스토리 범위가 활성화된 경우: hash 체크 생략하고 같은 범위로 재요청(낭비 방지로 5초 권장) - if (_overrideRange) return; // 간단히 폴링 정지 (원하면 5초 주기로 같은 범위 재요청하도록 변경 가능) - - fetchDeviceData(false).then(({ hash }) => { - if (!hash || hash === lastHash) return; - lastHash = hash; - fetchDeviceData(true).then(data => updateDOMWithData(data)); - }); -}, 1000); - -function formatNumber(num) { - if (num == null || isNaN(num)) return '-'; - return new Intl.NumberFormat().format(num); -} - -function getRelativeTime(isoTime) { - const d = new Date(isoTime), n = new Date(); - const diffDays = Math.floor( - (new Date(n.getFullYear(), n.getMonth(), n.getDate()) - - new Date(d.getFullYear(), d.getMonth(), d.getDate())) / 86400000 - ); - - const isKo = (typeof langCode !== 'undefined' && langCode === 'ko'); - const pad = (x) => x.toString().padStart(2, '0'); - const h24 = d.getHours(); - const p = isKo ? (h24 < 12 ? '오전' : '오후') : (h24 < 12 ? 'AM' : 'PM'); - const h12 = pad(h24 % 12 === 0 ? 12 : h24 % 12); - const m = pad(d.getMinutes()), s = pad(d.getSeconds()); - - if (isKo) { - return diffDays === 0 - ? `${p} ${h12}:${m}:${s}` - : `${diffDays}일 전 ${p} ${h12}:${m}:${s}`; - } - return diffDays === 0 - ? `${p} ${h12}:${m}:${s}` - : `${diffDays} day${diffDays > 1 ? 's' : ''} ago ${p} ${h12}:${m}:${s}`; -} - + const el = document.getElementById("lastUpdateTime"); + if (el) el.textContent = getRelativeTime(lastUpdateISOTime); + } + + // 히스토리 범위가 활성화된 경우: hash 체크 생략하고 같은 범위로 재요청(낭비 방지로 5초 권장) + if (_overrideRange) return; // 간단히 폴링 정지 (원하면 5초 주기로 같은 범위 재요청하도록 변경 가능) + + fetchDeviceData(false).then(({ hash }) => { + if (!hash || hash === lastHash) return; + lastHash = hash; + fetchDeviceData(true).then(data => updateDOMWithData(data)); + }); +}, 1000); + +function formatNumber(num) { + if (num == null || isNaN(num)) return '-'; + return new Intl.NumberFormat().format(num); +} + +function getRelativeTime(isoTime) { + const d = new Date(isoTime), n = new Date(); + const diffDays = Math.floor( + (new Date(n.getFullYear(), n.getMonth(), n.getDate()) - + new Date(d.getFullYear(), d.getMonth(), d.getDate())) / 86400000 + ); + + const isKo = (typeof langCode !== 'undefined' && langCode === 'ko'); + const pad = (x) => x.toString().padStart(2, '0'); + const h24 = d.getHours(); + const p = isKo ? (h24 < 12 ? '오전' : '오후') : (h24 < 12 ? 'AM' : 'PM'); + const h12 = pad(h24 % 12 === 0 ? 12 : h24 % 12); + const m = pad(d.getMinutes()), s = pad(d.getSeconds()); + + if (isKo) { + return diffDays === 0 + ? `${p} ${h12}:${m}:${s}` + : `${diffDays}일 전 ${p} ${h12}:${m}:${s}`; + } + return diffDays === 0 + ? `${p} ${h12}:${m}:${s}` + : `${diffDays} day${diffDays > 1 ? 's' : ''} ago ${p} ${h12}:${m}:${s}`; +} + function applyLanguage(langCode) { document.title = langCode === 'ko' ? '나의 찾기' : 'Find My Device'; - - const t = translations[langCode] || translations.ko, - titleMap = { titleSettings: t.settings, titleLocation: t.location, titleUpdate: t.update, titleBattery: t.battery, titleActivity: t.activity, titleEtc: t.etc, titleHistory: t.history }, - labelMap = { - labelKeepScreenOn: t.keepScreenOn, - labelCurrentPosition: t.currentPosition, - labelGpsAccuracy: t.gpsAccuracy, - labelMoveToCurrent: t.moveToCurrent, - labelLastUpdate: t.lastUpdate, - labelUpdateTrigger: t.updateTrigger, - labelCurrentAutoPosition: t.autoCurrentUpdate, - labelForceUpdate: t.forceUpdate, - labelBatteryState: t.batteryState, - labelBatteryLevel: t.batteryLevel, - labelWatchBatteryState: t.watchbatteryState, - labelWatchBatteryLevel: t.watchbatteryLevel, - labelCurrentActivity: t.currentActivity, - labelActivityAccuracy: t.activityAccuracy, - labelDistance: t.distance, - labelSteps: t.steps, - labelAscended: t.ascended, - labelDescended: t.descended, - labelConnection: t.connection, - labelEditPage: t.editPage, - labelAutoUpdate: t.autoUpdate, - labelStartTime: t.startTime, - labelEndTime: t.endTime, - labelPwa: t.pwa, - }; - + + const t = translations[langCode] || translations.ko, + titleMap = { titleSettings: t.settings, titleLocation: t.location, titleUpdate: t.update, titleBattery: t.battery, titleActivity: t.activity, titleEtc: t.etc, titleHistory: t.history }, + labelMap = { + labelKeepScreenOn: t.keepScreenOn, + labelCurrentPosition: t.currentPosition, + labelGpsAccuracy: t.gpsAccuracy, + labelMoveToCurrent: t.moveToCurrent, + labelLastUpdate: t.lastUpdate, + labelUpdateTrigger: t.updateTrigger, + labelCurrentAutoPosition: t.autoCurrentUpdate, + labelForceUpdate: t.forceUpdate, + labelBatteryState: t.batteryState, + labelBatteryLevel: t.batteryLevel, + labelWatchBatteryState: t.watchbatteryState, + labelWatchBatteryLevel: t.watchbatteryLevel, + labelCurrentActivity: t.currentActivity, + labelActivityAccuracy: t.activityAccuracy, + labelDistance: t.distance, + labelSteps: t.steps, + labelAscended: t.ascended, + labelDescended: t.descended, + labelConnection: t.connection, + labelEditPage: t.editPage, + labelAutoUpdate: t.autoUpdate, + labelStartTime: t.startTime, + labelEndTime: t.endTime, + labelPwa: t.pwa, + }; + const blogBackLink = document.getElementById('blogBackLink'); if (blogBackLink) updateBackLinkLabel(blogBackLink.dataset.backLabel || t.blog); const headerTitle = document.querySelector('.headerTitle'); if (headerTitle) headerTitle.textContent = t.title; - - for (const id in titleMap) { - const el = document.getElementById(id); - if (el) el.textContent = titleMap[id]; - } - + + for (const id in titleMap) { + const el = document.getElementById(id); + if (el) el.textContent = titleMap[id]; + } + for (const id in labelMap) { const el = document.getElementById(id); if (el) el.textContent = labelMap[id]; @@ -1115,31 +1115,31 @@ function applyLanguage(langCode) { updateMapToolLabels(); renderHistorySummary(); } - -function setCookieHours(name, value, hours) { - const d = new Date(); - d.setTime(d.getTime() + hours * 60 * 60 * 1000); - document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; expires=${d.toUTCString()}; path=/; SameSite=Lax`; -} - + +function setCookieHours(name, value, hours) { + const d = new Date(); + d.setTime(d.getTime() + hours * 60 * 60 * 1000); + document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; expires=${d.toUTCString()}; path=/; SameSite=Lax`; +} + function updateDOM(data) { const textMap = { currentPosition: data.address, - lastUpdateTime: data.updated ? getRelativeTime(data.updated) : "-", - nowActivity: data.activity, - connectionType: data.connection, - updateTrigger: data.trigger, + lastUpdateTime: data.updated ? getRelativeTime(data.updated) : "-", + nowActivity: data.activity, + connectionType: data.connection, + updateTrigger: data.trigger, batteryStatus: data.state, Battery: data.level > 0 ? `${data.level}%` : "-", WbatteryStatus: data.wstate, WBattery: data.wlevel > 0 ? `${data.wlevel}%` : "-", todaySteps: data.steps > 0 ? `${formatNumber(data.steps)}${langCode === "ko" ? "걸음" : "steps"}` : "-", - todayAscended: data.ascended > 0 ? `${formatNumber(data.ascended)}${langCode === "ko" ? "층" : "floors"}` : "-", - todayDescended: data.descended > 0 ? `${formatNumber(data.descended)}${langCode === "ko" ? "층" : "floors"}` : "-", - todayDistance: data.distance > 0 ? `${formatNumber(data.distance)}m` : "-", - accuracyValue: data.accuracy > 0 ? `${formatNumber(data.accuracy)}m` : "-" - }; - + todayAscended: data.ascended > 0 ? `${formatNumber(data.ascended)}${langCode === "ko" ? "층" : "floors"}` : "-", + todayDescended: data.descended > 0 ? `${formatNumber(data.descended)}${langCode === "ko" ? "층" : "floors"}` : "-", + todayDistance: data.distance > 0 ? `${formatNumber(data.distance)}m` : "-", + accuracyValue: data.accuracy > 0 ? `${formatNumber(data.accuracy)}m` : "-" + }; + for (const [id, value] of Object.entries(textMap)) { const el = document.getElementById(id); if (el) el.textContent = value || "-"; @@ -1268,61 +1268,61 @@ function normalizeDisplayValue(value) { } function updateActivitySection(data) { - const validActivities = ["Stationary", "Walking", "Running", "In Vehicle", "Cycling", "가만히 있음", "걷는 중", "달리는 중", "차량 이동 중", "자전거 이동 중"]; - const validAccuracies = ["높음", "중간", "낮음", "High", "Middle", "Low"]; - - let existActivityEl = document.getElementById('existnowActivity'); - if (existActivityEl) { - if (validActivities.includes(data.activity || '') && validAccuracies.includes(data.aaccuracy || '')) { - existActivityEl.style.display = 'flex'; - document.getElementById('nowactivityAccuracy').textContent = data.aaccuracy || '-'; - } else { - existActivityEl.remove(); - } - } - - if (!document.getElementById('existnowActivity') && - validActivities.includes(data.activity || '') && - validAccuracies.includes(data.aaccuracy || '')) { - const titles = document.querySelectorAll('.settingTitle'); - let activitySection = null; - - for (let title of titles) { - if (title.textContent.trim() === (langCode === 'ko' ? '활동' : 'Activity')) { - const next = title.nextElementSibling; - if (next && next.classList.contains('settingSection')) { - activitySection = next; - break; - } - } - } - - if (activitySection) { - const rows = activitySection.querySelectorAll('.settingRow'); - let targetRow = null; - - for (let row of rows) { - const label = row.querySelector('span'); - if (label && label.textContent.trim() === (langCode === 'ko' ? '현재 활동' : 'Current Activity')) { - targetRow = row; - break; - } - } - - if (targetRow) { - const newRow = document.createElement('div'); - newRow.className = 'settingRow'; - newRow.id = 'existnowActivity'; - newRow.innerHTML = ` - ${langCode === 'ko' ? '활동 정확도' : 'Activity Accuracy'} - ${data.aaccuracy || '-'} - `; - activitySection.insertBefore(newRow, targetRow.nextSibling); - } - } - } -} - + const validActivities = ["Stationary", "Walking", "Running", "In Vehicle", "Cycling", "가만히 있음", "걷는 중", "달리는 중", "차량 이동 중", "자전거 이동 중"]; + const validAccuracies = ["높음", "중간", "낮음", "High", "Middle", "Low"]; + + let existActivityEl = document.getElementById('existnowActivity'); + if (existActivityEl) { + if (validActivities.includes(data.activity || '') && validAccuracies.includes(data.aaccuracy || '')) { + existActivityEl.style.display = 'flex'; + document.getElementById('nowactivityAccuracy').textContent = data.aaccuracy || '-'; + } else { + existActivityEl.remove(); + } + } + + if (!document.getElementById('existnowActivity') && + validActivities.includes(data.activity || '') && + validAccuracies.includes(data.aaccuracy || '')) { + const titles = document.querySelectorAll('.settingTitle'); + let activitySection = null; + + for (let title of titles) { + if (title.textContent.trim() === (langCode === 'ko' ? '활동' : 'Activity')) { + const next = title.nextElementSibling; + if (next && next.classList.contains('settingSection')) { + activitySection = next; + break; + } + } + } + + if (activitySection) { + const rows = activitySection.querySelectorAll('.settingRow'); + let targetRow = null; + + for (let row of rows) { + const label = row.querySelector('span'); + if (label && label.textContent.trim() === (langCode === 'ko' ? '현재 활동' : 'Current Activity')) { + targetRow = row; + break; + } + } + + if (targetRow) { + const newRow = document.createElement('div'); + newRow.className = 'settingRow'; + newRow.id = 'existnowActivity'; + newRow.innerHTML = ` + ${langCode === 'ko' ? '활동 정확도' : 'Activity Accuracy'} + ${data.aaccuracy || '-'} + `; + activitySection.insertBefore(newRow, targetRow.nextSibling); + } + } + } +} + function initMap(latlng, accuracy = 50) { mapState.map = new naver.maps.Map('map', { center: latlng, @@ -2036,29 +2036,29 @@ function moveSelectedHistoryPoint(index, pan = false, syncSlider = true) { if (slider) slider.value = String(safeIndex); } } - -function createMainMarker(latlng) { - mapState.marker = new naver.maps.Marker({ - position: latlng, - map: mapState.map, - zIndex: 100, - icon: { - content: `
`, - anchor: new naver.maps.Point(30, 30) - } - }); - applyMarkerEffects(mapState.marker); -} - + +function createMainMarker(latlng) { + mapState.marker = new naver.maps.Marker({ + position: latlng, + map: mapState.map, + zIndex: 100, + icon: { + content: ``, + anchor: new naver.maps.Point(30, 30) + } + }); + applyMarkerEffects(mapState.marker); +} + function createAccuracyCircle(latlng, accuracy = 50) { mapState.circle = new naver.maps.Circle({ map: mapState.map, center: latlng, - radius: Math.max(accuracy, 10), - strokeColor: '#007AFF', - strokeOpacity: 0.8, - strokeWeight: 2, - fillColor: '#007AFF', + radius: Math.max(accuracy, 10), + strokeColor: '#007AFF', + strokeOpacity: 0.8, + strokeWeight: 2, + fillColor: '#007AFF', fillOpacity: 0.2 }); updateCurrentRangeCircles(latlng); @@ -2088,58 +2088,58 @@ function updateCurrentRangeCircles(latlng) { } }); } - -function applyMarkerEffects(marker) { - naver.maps.Event.addListener(marker, 'mouseover', () => { - const el = marker.getElement(); - if (el) el.style.transform = `scale(1.2)`; - }); - naver.maps.Event.addListener(marker, 'mouseout', () => { - const el = marker.getElement(); - if (el) el.style.transform = `scale(1)`; - }); - naver.maps.Event.addListener(marker, 'click', () => bounceMarker(marker)); - breathingMarker(marker); -} - -function breathingMarker(marker) { - const el = marker.getElement(); - let scale = 1, direction = 1; - setInterval(() => { - scale += 0.01 * direction; - if (scale > 1.1) direction = -1; - if (scale < 1.0) direction = 1; - if (el) { - el.style.transition = 'transform 0.1s'; - el.style.transform = `scale(${scale})`; - } - }, 50); -} - -function getCookie(name) { - const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)')); - return m ? decodeURIComponent(m[1]) : null; -} - -function bounceMarker(marker) { - let scale = 1, up = true, count = 0; - const interval = setInterval(() => { - scale += up ? 0.1 : -0.1; - if (scale >= 1.3) up = false; - if (scale <= 1) { - clearInterval(interval); - scale = 1; - } - marker.setIcon({ - url: 'https://chaegeon.com/custom/findmydevice/img/30c0fc9d9.png', - size: new naver.maps.Size(60 * scale, 60 * scale), - scaledSize: new naver.maps.Size(60 * scale, 60 * scale), - anchor: new naver.maps.Point(30 * scale, 30 * scale) - }); - if (count++ > 10) clearInterval(interval); - }, 30); -} - + +function applyMarkerEffects(marker) { + naver.maps.Event.addListener(marker, 'mouseover', () => { + const el = marker.getElement(); + if (el) el.style.transform = `scale(1.2)`; + }); + naver.maps.Event.addListener(marker, 'mouseout', () => { + const el = marker.getElement(); + if (el) el.style.transform = `scale(1)`; + }); + naver.maps.Event.addListener(marker, 'click', () => bounceMarker(marker)); + breathingMarker(marker); +} + +function breathingMarker(marker) { + const el = marker.getElement(); + let scale = 1, direction = 1; + setInterval(() => { + scale += 0.01 * direction; + if (scale > 1.1) direction = -1; + if (scale < 1.0) direction = 1; + if (el) { + el.style.transition = 'transform 0.1s'; + el.style.transform = `scale(${scale})`; + } + }, 50); +} + +function getCookie(name) { + const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)')); + return m ? decodeURIComponent(m[1]) : null; +} + +function bounceMarker(marker) { + let scale = 1, up = true, count = 0; + const interval = setInterval(() => { + scale += up ? 0.1 : -0.1; + if (scale >= 1.3) up = false; + if (scale <= 1) { + clearInterval(interval); + scale = 1; + } + marker.setIcon({ + url: 'https://chaegeon.com/custom/findmydevice/img/30c0fc9d9.png', + size: new naver.maps.Size(60 * scale, 60 * scale), + scaledSize: new naver.maps.Size(60 * scale, 60 * scale), + anchor: new naver.maps.Point(30 * scale, 30 * scale) + }); + if (count++ > 10) clearInterval(interval); + }, 30); +} + function resetHistoryLayer() { stopHistoryPlayback(); clearTrackLines(); @@ -2310,26 +2310,26 @@ function renderHistoryPoints(rawPoints) { if (!mapState.mapClickHandlerAdded) { naver.maps.Event.addListener(mapState.map, "click", () => { if (mapState.openInfoWindow) { - mapState.openInfoWindow.close(); - mapState.openInfoWindow = null; - } - }); - mapState.mapClickHandlerAdded = true; - } -} - + mapState.openInfoWindow.close(); + mapState.openInfoWindow = null; + } + }); + mapState.mapClickHandlerAdded = true; + } +} + function updateDOMWithData(data) { - // ⚑ 히스토리 모드: 값은 "-"로, 지도엔 히스토리만 반영 - if (typeof _overrideRange !== 'undefined' && _overrideRange) { - resetDisplayValues(); - - // 히스토리 기준 중심점 선택 - const last = data.history && data.history[data.history.length - 1]; - const baseLatLng = last - ? new naver.maps.LatLng(last.lat, last.lng) - : (data.latitude && data.longitude ? new naver.maps.LatLng(data.latitude, data.longitude) : null); - - // 지도 초기화 (최초 1회) + // ⚑ 히스토리 모드: 값은 "-"로, 지도엔 히스토리만 반영 + if (typeof _overrideRange !== 'undefined' && _overrideRange) { + resetDisplayValues(); + + // 히스토리 기준 중심점 선택 + const last = data.history && data.history[data.history.length - 1]; + const baseLatLng = last + ? new naver.maps.LatLng(last.lat, last.lng) + : (data.latitude && data.longitude ? new naver.maps.LatLng(data.latitude, data.longitude) : null); + + // 지도 초기화 (최초 1회) if (!mapState.map && baseLatLng) { initMap(baseLatLng, data.accuracy || 50); } @@ -2340,23 +2340,23 @@ function updateDOMWithData(data) { applyHistoryCounts(data); updateHistoryMarkers(data.history); } - - // 상대시간 갱신 중단 - lastUpdateISOTime = null; - - // 히스토리 모드에서는 메인 마커/원, auto pan 업데이트 스킵 - return; - } - + + // 상대시간 갱신 중단 + lastUpdateISOTime = null; + + // 히스토리 모드에서는 메인 마커/원, auto pan 업데이트 스킵 + return; + } + // ── 라이브 모드 기존 로직 ── const latlng = new naver.maps.LatLng(data.latitude, data.longitude); updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude); // DOM 업데이트 - updateDOM(data); - updateActivitySection(data); - - // 지도 초기화 + updateDOM(data); + updateActivitySection(data); + + // 지도 초기화 if (!mapState.map) { initMap(latlng, data.accuracy); } @@ -2367,74 +2367,74 @@ function updateDOMWithData(data) { applyHistoryCounts(data); updateHistoryMarkers(data.history); } - - // 메인 마커 / 정확도 원 업데이트 + + // 메인 마커 / 정확도 원 업데이트 mapState.marker.setPosition(latlng); mapState.circle.setCenter(latlng); animateCircleRadius(Math.max(data.accuracy || 50, 10)); updateCurrentRangeCircles(latlng); - + // autoPosition - const isChecked = document.getElementById('autoPositionSwitch').checked; - if (isChecked && (lastLat !== data.latitude || lastLng !== data.longitude)) { - mapState.map.panTo(latlng); - } - lastLat = data.latitude; - lastLng = data.longitude; - lastUpdateISOTime = data.updated; -} - -function animateCircleRadius(targetRadius) { - if (!mapState.circle) return; - if (window.radiusInterval) clearInterval(window.radiusInterval); - - let currentRadius = mapState.circle.getRadius(); - const step = (targetRadius - currentRadius) / 100; - let count = 0; - - window.radiusInterval = setInterval(() => { - currentRadius += step; - mapState.circle.setRadius(currentRadius); - if (count++ >= 100) { - mapState.circle.setRadius(targetRadius); - clearInterval(window.radiusInterval); - } - }, 30); -} - -function syncLocateBtn() { - const isChecked = document.getElementById('autoPositionSwitch').checked; - const parent = document.getElementById('autoLocateBtn'); - const exist = document.getElementById('locateBtn'); - - if (isChecked && exist) { - exist.remove(); - } else if (!isChecked && !exist && parent) { - const newDiv = document.createElement('div'); + const isChecked = document.getElementById('autoPositionSwitch').checked; + if (isChecked && (lastLat !== data.latitude || lastLng !== data.longitude)) { + mapState.map.panTo(latlng); + } + lastLat = data.latitude; + lastLng = data.longitude; + lastUpdateISOTime = data.updated; +} + +function animateCircleRadius(targetRadius) { + if (!mapState.circle) return; + if (window.radiusInterval) clearInterval(window.radiusInterval); + + let currentRadius = mapState.circle.getRadius(); + const step = (targetRadius - currentRadius) / 100; + let count = 0; + + window.radiusInterval = setInterval(() => { + currentRadius += step; + mapState.circle.setRadius(currentRadius); + if (count++ >= 100) { + mapState.circle.setRadius(targetRadius); + clearInterval(window.radiusInterval); + } + }, 30); +} + +function syncLocateBtn() { + const isChecked = document.getElementById('autoPositionSwitch').checked; + const parent = document.getElementById('autoLocateBtn'); + const exist = document.getElementById('locateBtn'); + + if (isChecked && exist) { + exist.remove(); + } else if (!isChecked && !exist && parent) { + const newDiv = document.createElement('div'); newDiv.className = 'settingRow locateRow'; newDiv.id = 'locateBtn'; newDiv.setAttribute('role', 'button'); newDiv.tabIndex = 0; newDiv.innerHTML = `${langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location'}`; - - newDiv.addEventListener('click', () => { - if (hasValidCoords(lastLat, lastLng)) { - mapState.map.panTo(new naver.maps.LatLng(lastLat, lastLng)); - } - }); + + newDiv.addEventListener('click', () => { + if (hasValidCoords(lastLat, lastLng)) { + mapState.map.panTo(new naver.maps.LatLng(lastLat, lastLng)); + } + }); parent.parentNode.insertBefore(newDiv, parent.nextSibling); } } - -function syncUpdateBtn() { - const isChecked = document.getElementById('autoUpdateSwitch').checked; - const parent = document.getElementById('autoUpdateBtn'); - const exist = document.getElementById('updateBtn'); - - if (isChecked && exist) { - exist.remove(); - } else if (!isChecked && !exist && parent) { - const newDiv = document.createElement('div'); + +function syncUpdateBtn() { + const isChecked = document.getElementById('autoUpdateSwitch').checked; + const parent = document.getElementById('autoUpdateBtn'); + const exist = document.getElementById('updateBtn'); + + if (isChecked && exist) { + exist.remove(); + } else if (!isChecked && !exist && parent) { + const newDiv = document.createElement('div'); newDiv.className = 'settingRow locateRow'; newDiv.id = 'updateBtn'; newDiv.setAttribute('role', 'button'); @@ -2442,11 +2442,11 @@ function syncUpdateBtn() { newDiv.innerHTML = `${langCode === 'ko' ? '업데이트 하기' : 'Force Update'}`; newDiv.addEventListener('click', triggerForceUpdate); - - parent.parentNode.insertBefore(newDiv, parent.nextSibling); - } -} - + + parent.parentNode.insertBefore(newDiv, parent.nextSibling); + } +} + function readHistoryRangeInputs() { const toServerTime = value => value ? (value.includes('T') ? value.replace('T', ' ') : value) + (value.length === 16 ? ':00' : '') @@ -2490,22 +2490,22 @@ function fetchDeviceData(includeLang = true, range = null) { body: JSON.stringify({}) }).then(res => res.json()); } - -function isIosSafari() { - const ua = window.navigator.userAgent; - const isIOS = /iPad|iPhone|iPod/.test(ua); - const isSafari = /^((?!chrome|android).)*safari/i.test(ua); - return isIOS && isSafari; -} - -function debounce(fn, delay = 400) { - let t; - return (...args) => { - clearTimeout(t); - t = setTimeout(() => fn(...args), delay); - }; -} - + +function isIosSafari() { + const ua = window.navigator.userAgent; + const isIOS = /iPad|iPhone|iPod/.test(ua); + const isSafari = /^((?!chrome|android).)*safari/i.test(ua); + return isIOS && isSafari; +} + +function debounce(fn, delay = 400) { + let t; + return (...args) => { + clearTimeout(t); + t = setTimeout(() => fn(...args), delay); + }; +} + function resetDisplayValues() { const ids = [ 'currentPosition', 'accuracyValue', 'lastUpdateTime', 'updateTrigger', @@ -2519,28 +2519,28 @@ function resetDisplayValues() { }); document.querySelectorAll('[data-sensor-row="1"], [data-sensor-title="1"], [id^="sensorSection_"]').forEach(el => el.remove()); } - + function triggerForceUpdate() { callDeviceWebhook('force_update'); - - const el = document.getElementById('updateBtn'); - if (!el) return; - el.style.pointerEvents = 'none'; - el.style.color = '#8e8e93'; - el.textContent = (langCode === 'ko' ? '갱신중...' : 'Updating...'); - - setTimeout(() => { - el.style.pointerEvents = 'auto'; - el.style.color = '#007AFF'; - el.textContent = (langCode === 'ko' ? '업데이트 하기' : 'Force Update'); - }, 5000); -} - -function hasValidCoords(lat, lng) { - return typeof lat === 'number' && isFinite(lat) && - typeof lng === 'number' && isFinite(lng); -} - + + const el = document.getElementById('updateBtn'); + if (!el) return; + el.style.pointerEvents = 'none'; + el.style.color = '#8e8e93'; + el.textContent = (langCode === 'ko' ? '갱신중...' : 'Updating...'); + + setTimeout(() => { + el.style.pointerEvents = 'auto'; + el.style.color = '#007AFF'; + el.textContent = (langCode === 'ko' ? '업데이트 하기' : 'Force Update'); + }, 5000); +} + +function hasValidCoords(lat, lng) { + return typeof lat === 'number' && isFinite(lat) && + typeof lng === 'number' && isFinite(lng); +} + function callDeviceWebhook(name, keepalive = false) { return fetch('https://chaegeon.com/custom/findmydevice/php/api.php?action=webhook', { method: 'POST',