From eeef4c008dc355d1469f659940a907a1fe3521dc Mon Sep 17 00:00:00 2001 From: seo Date: Wed, 1 Jul 2026 00:55:05 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=81=EB=8B=A8=20=EB=B3=B5=EA=B7=80=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=EB=A5=BC=20=EC=9D=B4=EC=A0=84=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- js/main.js | 131 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 99 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9352ff4..5803100 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태, - 강제 위치/센서 업데이트 요청 - 한국어/영어 전환 - Wake Lock 지원 +- 상단 복귀 링크는 이전 페이지가 있으면 해당 페이지 제목과 URL을 사용하고, 직접 접속이면 `https://chaegeon.com/blog`로 이동합니다. ## 구조 @@ -41,7 +42,7 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태, 표시할 모바일 앱 센서 목록은 `/custom/common/config.php`의 `findmydevice_sensor_sections`에서 섹션 단위로 관리합니다. 화면 CSS 원본은 `/custom/common/css/findmydevice.css`를 사용합니다. 기존 운영 HTML이 `/custom/findmydevice/css/style.css`를 참조해도 호환 파일이 공용 CSS를 import합니다. -페이지 본문에는 `
`만 두면 됩니다. `main.js`는 이 컨테이너 안에 지도, 헤더, 설정/기록/위치/업데이트/배터리/활동 섹션과 iOS PWA 안내 영역을 항상 자동 생성합니다. +페이지 본문에는 `
`만 두면 됩니다. `main.js`는 이 컨테이너 안에 지도, 헤더, 설정/기록/위치/업데이트/배터리/활동 섹션과 iOS PWA 안내 영역을 항상 자동 생성합니다. 상단 복귀 링크는 같은 출처의 이전 페이지라면 문서 제목을 읽어 표시하고, 다른 `chaegeon.com` 하위 도메인이나 직접 접속에서는 경로 이름과 기본 블로그 주소를 사용합니다. 위치 갱신 명령은 `/custom/common/config.php`의 `findmydevice_location_refresh` 설정을 사용합니다. `seoul`, `main` Home Assistant 프로필의 `notify.mobile_app_seocaegeonyi_z_fold7` 서비스로 Companion App notification command를 보냅니다. 자동 업데이트가 켜지면 Android high accuracy mode를 5초 간격으로 설정하고 `force_on`을 보낸 뒤 `request_location_update`와 `command_update_sensors`를 함께 호출합니다. 자동 업데이트 중에는 10초마다 위치 요청과 센서 업데이트 요청을 반복하고, 자동 업데이트를 끄거나 페이지를 벗어나면 `force_off`를 보냅니다. diff --git a/js/main.js b/js/main.js index d670512..03d745f 100644 --- a/js/main.js +++ b/js/main.js @@ -459,6 +459,83 @@ let langCode = localStorage.getItem("currentLang") || deviceLang, _overrideRange, wakeLock = null; +const FALLBACK_BACK_TARGET = 'https://chaegeon.com/blog'; +const BACK_LINK_PATH_TITLE = { + '/': '채건닷컴', + '/home': '홈', + '/blog': '블로그', +}; + +function cleanPageTitle(title) { + return String(title || '') + .replace(/^채건닷컴\s*-\s*/i, '') + .replace(/\s*-\s*채건닷컴$/i, '') + .trim(); +} + +function sameSiteUrl(url) { + try { + const parsed = new URL(url, location.href); + return parsed.hostname === location.hostname || parsed.hostname.endsWith('.chaegeon.com') || parsed.hostname === 'chaegeon.com' + ? parsed + : null; + } catch (e) { + return null; + } +} + +function titleFromBackUrl(url) { + const parsed = sameSiteUrl(url); + if (!parsed) return ''; + return BACK_LINK_PATH_TITLE[parsed.pathname.replace(/\/$/, '') || '/'] || cleanPageTitle(parsed.pathname.split('/').filter(Boolean).pop() || ''); +} + +async function resolveBackTarget() { + const ref = sameSiteUrl(document.referrer || ''); + const current = new URL(location.href); + const target = ref && ref.href !== current.href ? ref.href : FALLBACK_BACK_TARGET; + let title = titleFromBackUrl(target); + + if (ref && ref.origin === location.origin) { + try { + const res = await fetch(ref.href, { credentials: 'same-origin', cache: 'force-cache' }); + if (res.ok) { + const html = await res.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + title = cleanPageTitle(doc.querySelector('title')?.textContent) || title; + } + } catch (e) {} + } + + return { href: target, label: title || '블로그' }; +} + +function updateBackLinkLabel(label) { + const blogBackLink = document.getElementById('blogBackLink'); + if (!blogBackLink) return; + blogBackLink.innerHTML = ` ${label}`; +} + +function setupBackLink() { + const blogBackLink = document.getElementById('blogBackLink'); + if (!blogBackLink) return; + + blogBackLink.href = FALLBACK_BACK_TARGET; + blogBackLink.dataset.backLabel = '블로그'; + updateBackLinkLabel(blogBackLink.dataset.backLabel); + + resolveBackTarget().then(({ href, label }) => { + blogBackLink.href = href; + blogBackLink.dataset.backLabel = label; + updateBackLinkLabel(label); + }); + + blogBackLink.addEventListener('click', (e) => { + e.preventDefault(); + location.href = blogBackLink.href || FALLBACK_BACK_TARGET; + }); +} + function ensureFindMyDeviceShell() { let mount = document.getElementById('findmydevice'); if (!mount) { @@ -535,16 +612,9 @@ ensureFindMyDeviceShell(); 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'; - }); -}); + row.setAttribute('role', 'button'); + row.tabIndex = 0; +}); document.getElementById('langToggle').addEventListener('click', () => { langCode = langCode === 'ko' ? 'en' : 'ko'; @@ -607,10 +677,7 @@ document.addEventListener('DOMContentLoaded', () => { container.style.webkitTouchCallout = 'none'; container.addEventListener('dragstart', e => e.preventDefault()); - document.getElementById('blogBackLink').addEventListener('click', (e) => { - e.preventDefault(); - location.href = '/blog'; - }); + setupBackLink(); function renderAuthErrorPage() { document.body.innerHTML = ` @@ -971,7 +1038,7 @@ function applyLanguage(langCode) { }; const blogBackLink = document.getElementById('blogBackLink'); - if (blogBackLink) blogBackLink.innerHTML = ` ${t.blog}`; + if (blogBackLink) updateBackLinkLabel(blogBackLink.dataset.backLabel || t.blog); const headerTitle = document.querySelector('.headerTitle'); if (headerTitle) headerTitle.textContent = t.title; @@ -2279,22 +2346,20 @@ function syncLocateBtn() { exist.remove(); } else if (!isChecked && !exist && parent) { const newDiv = document.createElement('div'); - newDiv.className = 'settingRow locateRow'; - newDiv.id = 'locateBtn'; - newDiv.innerHTML = `${langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location'}`; + 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('pointerdown', () => newDiv.style.background = '#d3d3d3'); - newDiv.addEventListener('pointerup', () => newDiv.style.background = '#fff'); - newDiv.addEventListener('pointerleave', () => newDiv.style.background = '#fff'); - - parent.parentNode.insertBefore(newDiv, parent.nextSibling); - } -} + parent.parentNode.insertBefore(newDiv, parent.nextSibling); + } +} function syncUpdateBtn() { const isChecked = document.getElementById('autoUpdateSwitch').checked; @@ -2305,15 +2370,13 @@ function syncUpdateBtn() { exist.remove(); } else if (!isChecked && !exist && parent) { const newDiv = document.createElement('div'); - newDiv.className = 'settingRow locateRow'; - newDiv.id = 'updateBtn'; - newDiv.innerHTML = `${langCode === 'ko' ? '업데이트 하기' : 'Force Update'}`; - - newDiv.addEventListener('pointerdown', () => newDiv.style.background = '#d3d3d3'); - newDiv.addEventListener('pointerup', () => newDiv.style.background = '#fff'); - newDiv.addEventListener('pointerleave', () => newDiv.style.background = '#fff'); - - newDiv.addEventListener('click', triggerForceUpdate); + newDiv.className = 'settingRow locateRow'; + newDiv.id = 'updateBtn'; + newDiv.setAttribute('role', 'button'); + newDiv.tabIndex = 0; + newDiv.innerHTML = `${langCode === 'ko' ? '업데이트 하기' : 'Force Update'}`; + + newDiv.addEventListener('click', triggerForceUpdate); parent.parentNode.insertBefore(newDiv, parent.nextSibling); }