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: "업데이트 하기", battery: "배터리", batteryState: "배터리 상태", batteryLevel: "배터리 잔량", watchbatteryState: "워치 배터리 상태", watchbatteryLevel: "워치 배터리 잔량", activity: "활동", currentActivity: "현재 활동", activityAccuracy: "활동 정확도", distance: "도보로 움직인 거리", steps: "걸음 수", ascended: "오른 계단", descended: "내려간 계단", etc: "기타", connection: "연결 방법", editPage: "페이지 수정", pwa: "눌러 홈 화면에 추가해 바로 보기", history: "기록", startTime: "시작", endTime: "종료", }, 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", watchbatteryState: "Watch Battery State", watchbatteryLevel: "Watch Battery Level", activity: "Activity", currentActivity: "Current Activity", activityAccuracy: "Activity Confidence", distance: "Distance walked", steps: "Steps", ascended: "Floors Ascended", descended: "Floors Descended", etc: "Others", connection: "Network Type", editPage: "Edit Page", pwa: "Add to Home Screen for quick access!", history: "History", startTime: "Start", endTime: "End", }, }, mapState = { map: null, marker: null, circle: null, trackLine: null, smallMarkers: [], openInfoWindow: null, mapClickHandlerAdded: false, initialInfoOpened: false, }; let langCode = localStorage.getItem("currentLang") || deviceLang, lastAutoSwitchChecked, lastUpdateSwitchChecked, lastHash, lastLat, lastLng, lastUpdateISOTime, map, marker, circle, radiusInterval, _overrideRange, wakeLock = null; applyLanguage(langCode); document.querySelectorAll('.locateRow').forEach(row => { row.addEventListener('pointerdown', () => { row.style.background = '#d3d3d3'; }); row.addEventListener('pointerup', () => { row.style.background = '#fff'; }); row.addEventListener('pointerleave', () => { row.style.background = '#fff'; }); }); document.getElementById('langToggle').addEventListener('click', () => { langCode = langCode === 'ko' ? 'en' : 'ko'; localStorage.setItem('currentLang', langCode); applyLanguage(langCode); 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()); document.getElementById('blogBackLink').addEventListener('click', (e) => { e.preventDefault(); location.href = '/blog'; }); // PWA 상태인지 확인 (iOS 및 기타 브라우저 지원) function isInStandaloneMode() { return (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone === true); } // 조건: /findmydevice로 시작하고 access 쿠키가 없으며, PWA가 아닌 경우에만 리디렉션 if (location.pathname.startsWith('/findmydevice') && getCookie('access') !== '9KxH6236' && !isInStandaloneMode()) { location.replace('/blog'); return; // 이하 코드 실행 방지 } 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(); 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) }; // ✅ 사용자가 변경했거나 URL에 기간이 있으면 히스토리 모드 if (userTriggered || hasQS) { _overrideRange = range; resetHistoryLayer(); } else { _overrideRange = null; // 라이브 모드 유지 } try { const data = await fetchDeviceData(true, range); updateDOMWithData(data); const last = data.history && data.history[data.history.length - 1]; if (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)); }); 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(); const isChecked = document.getElementById('autoUpdateSwitch')?.checked; if (isChecked) { fetch('https://ha.chaegeon.com/api/webhook/5P1HgWiDpaAK8E0B', { method: 'POST' }); } else { fetch('https://ha.chaegeon.com/api/webhook/z2nHt75jRxWKo2zF', { method: 'POST' }); } }); ['pagehide', 'visibilitychange', 'blur', 'beforeunload'].forEach(eventType => { window.addEventListener(eventType, function (e) { const switchEl = document.getElementById('autoUpdateSwitch'); const isAutoUpdateOn = switchEl?.checked; if (isAutoUpdateOn) { navigator.sendBeacon('https://ha.chaegeon.com/api/webhook/z2nHt75jRxWKo2zF'); // 스위치 UI 강제 끔 switchEl.checked = false; syncUpdateBtn(); } }); }); setInterval(() => { if (document.getElementById('autoUpdateSwitch')?.checked) { fetch('https://ha.chaegeon.com/api/webhook/870ZNzB20WRkzjnh', { method: 'POST' }); } }, 10000); setInterval(() => { 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}`; } 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, }; document.getElementById('blogBackLink').innerHTML = `‹ ${t.blog}`; document.querySelector('.headerTitle').textContent = t.title; 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]; } } 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, 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` : "-" }; for (const [id, value] of Object.entries(textMap)) { const el = document.getElementById(id); if (el) el.textContent = 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); } } } } function initMap(latlng, accuracy = 50) { mapState.map = new naver.maps.Map('map', { center: latlng, zoom: 12 }); createMainMarker(latlng); createAccuracyCircle(latlng, accuracy); } 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', fillOpacity: 0.2 }); } 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() { if (mapState.trackLine) { mapState.trackLine.setMap(null); mapState.trackLine = null; } if (mapState.smallMarkers?.length) { mapState.smallMarkers.forEach(m => m.setMap?.(null)); mapState.smallMarkers = []; } if (mapState.openInfoWindow) { mapState.openInfoWindow.close(); mapState.openInfoWindow = null; } } function updateHistoryMarkers(history) { // 안전가드 if (!Array.isArray(history) || history.length === 0) { // 표시된 마커를 기준으로 라인만 갱신(필요 시) renderTrackFromDisplayedMarkers(); return; } // time 파싱 헬퍼 const toTime = (v) => { if (v instanceof Date) return v.getTime(); if (typeof v === 'number' && isFinite(v)) { // 초 단위처럼 보이면 ms로 변환 return v < 2e10 ? v * 1000 : v; } const t = Date.parse(v); return isNaN(t) ? 0 : t; }; // ★ history는 스몰마커 생성만에 사용(정렬은 생성 안정성용) history.sort((a, b) => toTime(a.time) - toTime(b.time)); // 1) 스몰마커 생성/중복 방지 history.forEach(pos => { const pointLatLng = new naver.maps.LatLng(pos.lat, pos.lng); const exist = mapState.smallMarkers.some(marker => marker.getPosition().equals(pointLatLng) && marker.userTime === pos.time ); if (!exist) { const smallMarker = new naver.maps.Marker({ position: pointLatLng, map: mapState.map, icon: { content: `