지도 복귀 링크과 경로 마커 정리

This commit is contained in:
seo
2026-07-12 06:34:00 +09:00
parent b34cd5e17f
commit 6302030d4d
2 changed files with 51 additions and 53 deletions
+3 -3
View File
@@ -27,7 +27,7 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
- 한국어/영어 전환
- Wake Lock 지원
- 나의 찾기와 공통 파일 변경 시 열린 화면 자동 새로고침
- 상단 복귀 링크는 이전 페이지가 있으면 해당 페이지 제목과 URL을 사용고, 직접 접속이면 `https://chaegeon.com/`로 이동합니다.
- 상단 복귀 링크는 이전 페이지가 있으면 해당 페이지 제목과 URL을 사용합니다. 쿼리만 다른 같은 나의 찾기 화면은 건너뛰고, 직접 접속이면 `https://chaegeon.com/`로 이동합니다.
## 구조
@@ -48,7 +48,7 @@ PWA standalone으로 진입한 경우에는 설치형 앱 사용성을 우선해
페이지 본문은 `/custom/common/js/asset-reload.js``findmydevice` 서비스로 먼저 불러옵니다. 감시 대상은 나의 찾기 JS/CSS/API와 공통 인증, 공통 CSS, 공통 설정 파일입니다. 열린 나의 찾기 화면에서 대상 파일 버전이 달라지면 브라우저 캐시를 비우고 현재 화면을 다시 열어 새 파일을 받습니다.
페이지 본문에는 `<div id="findmydevice"></div>`만 두면 됩니다. `main.js`는 이 컨테이너 안에 지도, 헤더, 설정/기록/위치/업데이트/배터리/활동 섹션과 iOS PWA 안내 영역을 항상 자동 생성합니다. 상단 복귀 링크는 `chaegeon.com`이면 채건닷컴, `/blog`이면 블로그처럼 알려진 경로명을 우선 표시하고, 같은 출처의 다른 이전 페이지는 문서 제목을 읽어 표시합니다. 직접 접속처럼 이전 페이지 확인이 어려우면 채건닷컴으로 표시하고 `https://chaegeon.com/`로 이동합니다.
페이지 본문에는 `<div id="findmydevice"></div>`만 두면 됩니다. `main.js`는 이 컨테이너 안에 지도, 헤더, 설정/기록/위치/업데이트/배터리/활동 섹션과 iOS PWA 안내 영역을 항상 자동 생성합니다. 상단 복귀 링크는 `chaegeon.com`이면 채건닷컴, `/blog`이면 블로그처럼 알려진 경로명을 우선 표시하고, 같은 출처의 다른 이전 페이지는 문서 제목을 읽어 표시합니다. 같은 경로에서 쿼리만 바뀐 화면은 같은 페이지로 보고 세션 방문 기록에서 다른 이전 페이지를 찾습니다. 직접 접속처럼 이전 페이지 확인이 어려우면 채건닷컴으로 표시하고 `https://chaegeon.com/`로 이동합니다.
위치 갱신 명령은 `/custom/common/config.php``findmydevice_location_refresh` 설정을 사용합니다. `seoul`, `main` Home Assistant 프로필의 `notify.mobile_app_seocaegeonyi_z_fold7` 서비스로 Companion App notification command를 보냅니다. 자동 업데이트는 서울 집과 목포 집 150m 반경 밖이고 충전 중이거나 `Niro` 블루투스가 연결되어 있을 때만 Android high accuracy mode를 5초 간격으로 설정하고 `force_on`, `request_location_update`, `command_update_sensors`를 호출합니다. 화면이 열려 있으면 5초마다 위치/센서 갱신 명령을 반복하고, HA 자동화도 30초마다 같은 조건을 확인합니다. 집 안이거나 충전 중도 아니고 `Niro`도 연결되어 있지 않으면 자동 명령은 `force_off`로 고정밀 위치 갱신을 끕니다.
@@ -62,7 +62,7 @@ PWA standalone으로 진입한 경우에는 설치형 앱 사용성을 우선해
히스토리 조회도 `seoul`, `main` Home Assistant history를 모두 읽습니다. 서버는 시간과 좌표를 기준으로 중복 지점을 제거하고 시간순으로 병합합니다. 응답에는 병합 전 출처별 지점 수인 `historySourceCounts`도 포함합니다.
Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그리고 시작/종료 마커와 체류 지점 마커를 표시합니다. 중간 점 마커는 긴 기간 조회에서 브라우저 부하가 커지므로 표시하지 않습니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다.
Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그리고 체류 지점 마커를 표시합니다. 시작/종료 마커와 자동 정보창은 표시하지 않습니다. 중간 점 마커는 긴 기간 조회에서 브라우저 부하가 커지므로 표시하지 않습니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞니다.
지도 기능은 NAVER 지도 API v3의 기본 컨트롤, `Polyline`, `PointingIcon`, `Circle`, `fitBounds`를 활용합니다. 지도 아래 도구 패널에서 경로 전체 보기, 경로 재생, GPS 튐 보정, Naver 지도 열기를 제어합니다.
+46 -48
View File
@@ -477,6 +477,7 @@ const PLAYBACK_MIN_VISIBLE_STEPS = 80;
const PLAYBACK_TICK_MS = 160;
const FALLBACK_BACK_TARGET = 'https://chaegeon.com/';
const BACK_HISTORY_KEY = 'chaegeon.backHistory.v1';
const BACK_LINK_PATH_TITLE = {
'/': '채건닷컴',
'/home': '홈',
@@ -556,16 +557,52 @@ function titleFromBackUrl(url) {
return BACK_LINK_PATH_TITLE[parsed.pathname.replace(/\/$/, '') || '/'] || cleanPageTitle(parsed.pathname.split('/').filter(Boolean).pop() || '');
}
function backPageKey(url) {
const parsed = sameSiteUrl(url);
if (!parsed) return '';
const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
return `${parsed.hostname.replace(/^www\./, '')}${pathname}`;
}
function rememberBackHistory() {
try {
const current = new URL(location.href);
const list = JSON.parse(sessionStorage.getItem(BACK_HISTORY_KEY) || '[]')
.filter(item => item && item.href && item.key);
const entry = { href: current.href, key: backPageKey(current.href), time: Date.now() };
const last = list[list.length - 1];
const next = last && last.href === entry.href ? list : [...list, entry];
sessionStorage.setItem(BACK_HISTORY_KEY, JSON.stringify(next.slice(-20)));
} catch (e) {}
}
function storedBackTarget(currentKey) {
try {
const list = JSON.parse(sessionStorage.getItem(BACK_HISTORY_KEY) || '[]');
for (let i = list.length - 1; i >= 0; i--) {
const item = list[i];
if (item?.href && item.key && item.key !== currentKey && sameSiteUrl(item.href)) {
return item.href;
}
}
} catch (e) {}
return '';
}
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;
const currentKey = backPageKey(current.href);
rememberBackHistory();
const refIsDifferentPage = ref && backPageKey(ref.href) !== currentKey;
const target = refIsDifferentPage ? ref.href : (storedBackTarget(currentKey) || FALLBACK_BACK_TARGET);
const knownTitle = titleFromBackUrl(target);
let title = knownTitle;
const targetUrl = sameSiteUrl(target);
if (!knownTitle && ref && ref.origin === location.origin) {
if (!knownTitle && targetUrl && targetUrl.origin === location.origin) {
try {
const res = await fetch(ref.href, { credentials: 'same-origin', cache: 'force-cache' });
const res = await fetch(targetUrl.href, { credentials: 'same-origin', cache: 'force-cache' });
if (res.ok) {
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
@@ -2300,47 +2337,14 @@ function renderHistoryPoints(rawPoints) {
mapState.stayMarkers.forEach(marker => marker.setMap(null));
mapState.stayMarkers = [];
}
if (mapState.historyStartMarker) mapState.historyStartMarker.setMap(null);
if (mapState.historyEndMarker) mapState.historyEndMarker.setMap(null);
const makeMarker = (pos, type = 'point') => {
const isEndpoint = type === 'start' || type === 'end';
const color = type === 'start' ? '#34C759' : (type === 'end' ? '#FF3B30' : '#007AFF');
const label = type === 'start' ? (langCode === 'ko' ? '시작' : 'Start') : (type === 'end' ? (langCode === 'ko' ? '종료' : 'End') : '');
const marker = new naver.maps.Marker({
position: pos.latlng,
map: mapState.map,
zIndex: isEndpoint ? 90 : 50,
icon: {
content: `<div class="history-marker history-marker-${type}" style="--marker-color:${color}">${label}</div>`,
anchor: new naver.maps.Point(isEndpoint ? 24 : 8, isEndpoint ? 14 : 8)
if (mapState.historyStartMarker) {
mapState.historyStartMarker.setMap(null);
mapState.historyStartMarker = null;
}
});
marker.userTime = pos.time;
marker.infoWindow = new naver.maps.InfoWindow({
content: buildHistoryInfoContent(pos, type, label)
});
naver.maps.Event.addListener(marker, "click", () => {
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
mapState.openInfoWindow.close();
if (mapState.historyEndMarker) {
mapState.historyEndMarker.setMap(null);
mapState.historyEndMarker = null;
}
if (marker.infoWindow.getMap()) {
marker.infoWindow.close();
mapState.openInfoWindow = null;
} else {
marker.infoWindow.open(mapState.map, marker);
mapState.openInfoWindow = marker.infoWindow;
const idx = mapState.historyPoints.findIndex(item => item.time === pos.time && item.lat === pos.lat && item.lng === pos.lng);
if (idx >= 0) moveSelectedHistoryPoint(idx, false);
}
});
return marker;
};
const first = points[0];
const last = points.at(-1);
mapState.historyStartMarker = makeMarker(first, 'start');
mapState.historyEndMarker = makeMarker(last, 'end');
mapState.stayPoints.forEach(stay => {
const marker = new naver.maps.Marker({
@@ -2370,12 +2374,6 @@ function renderHistoryPoints(rawPoints) {
mapState.historyBoundsFitted = true;
}
if (!_overrideRange && !keepPlaybackIndex && !mapState.initialInfoOpened && mapState.historyEndMarker?.infoWindow) {
mapState.historyEndMarker.infoWindow.open(mapState.map, mapState.historyEndMarker);
mapState.openInfoWindow = mapState.historyEndMarker.infoWindow;
mapState.initialInfoOpened = true;
}
if (!mapState.mapClickHandlerAdded) {
naver.maps.Event.addListener(mapState.map, "click", () => {
if (mapState.openInfoWindow) {