findmydevice 기록 경로 갱신과 요약 정리
This commit is contained in:
@@ -52,7 +52,7 @@ PWA standalone으로 진입한 경우에는 설치형 앱 사용성을 우선해
|
|||||||
|
|
||||||
## 히스토리와 지도
|
## 히스토리와 지도
|
||||||
|
|
||||||
실시간 화면은 현재 위치와 상태를 조회하고, 기본 기간 입력값에 해당하는 최근 하루 이동 기록도 지도에 함께 표시합니다. 기간 입력을 변경하면 해당 범위의 이동 기록을 `history=1` 옵션으로 다시 요청합니다. 서버는 기간이 길어도 Home Assistant 히스토리 API를 실행한 뒤, 브라우저 렌더링용으로 경로를 부드럽게 단순화해 응답합니다.
|
실시간 화면은 현재 위치와 상태를 조회하고, 기본 기간 입력값에 해당하는 최근 하루 이동 기록도 지도에 함께 표시합니다. 자동 위치 이동이 켜져 있어도 새 위치를 받을 때마다 경로선, 종료 지점, 기록 요약을 같은 히스토리 기준으로 다시 그립니다. 기간 입력을 변경하면 해당 범위의 이동 기록을 `history=1` 옵션으로 다시 요청합니다. 서버는 기간이 길어도 Home Assistant 히스토리 API를 실행한 뒤, 브라우저 렌더링용으로 경로를 부드럽게 단순화해 응답합니다.
|
||||||
|
|
||||||
상태 조회는 `seoul`, `main` Home Assistant의 `/states`를 모두 읽어 entity별로 병합합니다. 한쪽에만 의미 있는 값이 있으면 해당 값을 사용하고, 양쪽 모두 값이 있으면 더 최신 `last_updated` 또는 `last_changed` 값을 사용합니다. 선택된 상태의 attributes에 빈 값이 있으면 다른 쪽 attributes로 채워 빈칸을 줄입니다.
|
상태 조회는 `seoul`, `main` Home Assistant의 `/states`를 모두 읽어 entity별로 병합합니다. 한쪽에만 의미 있는 값이 있으면 해당 값을 사용하고, 양쪽 모두 값이 있으면 더 최신 `last_updated` 또는 `last_changed` 값을 사용합니다. 선택된 상태의 attributes에 빈 값이 있으면 다른 쪽 attributes로 채워 빈칸을 줄입니다.
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그
|
|||||||
|
|
||||||
지도 기능은 NAVER 지도 API v3의 기본 컨트롤, `Polyline`, `PointingIcon`, `Circle`, `fitBounds`를 활용합니다. 지도 아래 도구 패널에서 경로 전체 보기, 경로 재생, GPS 튐 보정, Naver 지도 열기를 제어합니다.
|
지도 기능은 NAVER 지도 API v3의 기본 컨트롤, `Polyline`, `PointingIcon`, `Circle`, `fitBounds`를 활용합니다. 지도 아래 도구 패널에서 경로 전체 보기, 경로 재생, GPS 튐 보정, Naver 지도 열기를 제어합니다.
|
||||||
|
|
||||||
히스토리 요약 패널은 이동 거리, 기록 시간, 평균 속도, 최대 기록 공백, 지점 수, 체류 지점 수, 집 반경, 집 체류 시간, 외출 시간, 외출 횟수, 마지막 복귀 시각과 현재 상태를 표시합니다. 각 히스토리 마커 정보창에는 기록 시각, 시작점 기준 누적 거리, 좌표를 함께 표시합니다.
|
히스토리 요약 패널은 이동 거리, 기록 시간, 지도에 실제로 그려진 지점 수를 기본으로 보여줍니다. 체류 지점, 현재 상태, 외출 시간, 외출 횟수, 마지막 복귀 시각은 값이 있을 때만 보여주므로 빈값이나 판단 가치가 낮은 항목은 화면에 남지 않습니다. 각 히스토리 마커 정보창에는 기록 시각, 시작점 기준 누적 거리, 좌표를 함께 표시합니다.
|
||||||
|
|
||||||
경로선은 Douglas-Peucker 단순화와 가벼운 좌표 smoothing을 거쳐 긴 기간에서도 완만한 단일 선으로 표시합니다. `GPS 튐 보정`이 켜져 있으면 집 중심 1.2km 안의 장기 체류성 흔들림은 집 위치로 흡수하고, 집 주변 보정 지점은 30분 단위로 줄여 지도 낙서를 줄입니다. 이전/다음 지점이 가까운데 중간 지점만 250m 이상 튀고 왕복 속도가 80km/h를 넘는 단발 스파이크, 500m 이상 220km/h 초과 이동, 2km 이상 120km/h 초과 이동, 정확도 300m 초과 지점은 제외합니다. 30분 이상 기록 공백은 선을 끊고 이동 거리 합산에서도 제외합니다. 지도 아래 `GPS 튐 보정` 버튼으로 원본 렌더링 경로와 보정 경로를 전환할 수 있습니다.
|
경로선은 Douglas-Peucker 단순화와 가벼운 좌표 smoothing을 거쳐 긴 기간에서도 완만한 단일 선으로 표시합니다. `GPS 튐 보정`이 켜져 있으면 집 중심 1.2km 안의 장기 체류성 흔들림은 집 위치로 흡수하고, 집 주변 보정 지점은 30분 단위로 줄여 지도 낙서를 줄입니다. 이전/다음 지점이 가까운데 중간 지점만 250m 이상 튀고 왕복 속도가 80km/h를 넘는 단발 스파이크, 500m 이상 220km/h 초과 이동, 2km 이상 120km/h 초과 이동, 정확도 300m 초과 지점은 제외합니다. 30분 이상 기록 공백은 선을 끊고 이동 거리 합산에서도 제외합니다. 지도 아래 `GPS 튐 보정` 버튼으로 원본 렌더링 경로와 보정 경로를 전환할 수 있습니다.
|
||||||
|
|
||||||
|
|||||||
+34
-16
@@ -1811,22 +1811,23 @@ function renderHistorySummary() {
|
|||||||
const home = stats.home;
|
const home = stats.home;
|
||||||
const sessions = home?.sessions || [];
|
const sessions = home?.sessions || [];
|
||||||
const recentSessions = sessions.slice(-3).reverse();
|
const recentSessions = sessions.slice(-3).reverse();
|
||||||
|
const rows = [
|
||||||
|
[t.historyDistance, formatDistance(stats.distance)],
|
||||||
|
[t.historyDuration, formatDuration(Math.round(stats.duration))],
|
||||||
|
[t.historyPoints, formatNumber(stats.points)],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (stats.stops > 0) rows.push([t.historyStops, formatNumber(stats.stops)]);
|
||||||
|
if (home?.status) rows.push([t.currentPlaceStatus, home.status]);
|
||||||
|
if (home && home.awaySeconds > 0) rows.push([t.awayTime, formatDuration(Math.round(home.awaySeconds))]);
|
||||||
|
if (sessions.length > 0) rows.push([t.awaySessions, formatNumber(sessions.length)]);
|
||||||
|
if (home?.lastReturn) rows.push([t.lastReturn, formatDateTime(home.lastReturn)]);
|
||||||
|
|
||||||
panel.hidden = false;
|
panel.hidden = false;
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="history-summary-title">${t.historySummary}</div>
|
<div class="history-summary-title">${t.historySummary}</div>
|
||||||
<div class="history-summary-grid">
|
<div class="history-summary-grid">
|
||||||
<span>${t.historyDistance}</span><strong>${formatDistance(stats.distance)}</strong>
|
${rows.map(([label, value]) => `<span>${label}</span><strong>${value || '-'}</strong>`).join('')}
|
||||||
<span>${t.historyDuration}</span><strong>${formatDuration(Math.round(stats.duration))}</strong>
|
|
||||||
<span>${t.historyAverageSpeed}</span><strong>${formatSpeed(stats.averageSpeed)}</strong>
|
|
||||||
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
|
|
||||||
<span>${t.historyPoints}</span><strong>${stats.rawPoints && stats.rawPoints !== stats.renderPoints ? `${formatNumber(stats.renderPoints)} / ${formatNumber(stats.rawPoints)}` : formatNumber(stats.points)}</strong>
|
|
||||||
<span>${t.historyStops}</span><strong>${formatNumber(stats.stops || 0)}</strong>
|
|
||||||
<span>${t.homeRadius}</span><strong>${mapState.filterOutliers ? `${formatNumber(homeRadiusMeters())}m + GPS 1.2km` : `${formatNumber(homeRadiusMeters())}m`}</strong>
|
|
||||||
<span>${t.homeStay}</span><strong>${home ? formatDuration(Math.round(home.homeSeconds)) : '-'}</strong>
|
|
||||||
<span>${t.awayTime}</span><strong>${home ? formatDuration(Math.round(home.awaySeconds)) : '-'}</strong>
|
|
||||||
<span>${t.awaySessions}</span><strong>${home ? formatNumber(sessions.length) : '-'}</strong>
|
|
||||||
<span>${t.lastReturn}</span><strong>${home?.lastReturn ? formatDateTime(home.lastReturn) : '-'}</strong>
|
|
||||||
<span>${t.currentPlaceStatus}</span><strong>${home?.status || '-'}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
${recentSessions.length ? `
|
${recentSessions.length ? `
|
||||||
<div class="away-session-list">
|
<div class="away-session-list">
|
||||||
@@ -1838,7 +1839,6 @@ function renderHistorySummary() {
|
|||||||
` : ''}
|
` : ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHistoryInfoContent(point, type, label) {
|
function buildHistoryInfoContent(point, type, label) {
|
||||||
const t = translations[langCode] || translations.ko;
|
const t = translations[langCode] || translations.ko;
|
||||||
const title = label || t.historyPoint;
|
const title = label || t.historyPoint;
|
||||||
@@ -2350,6 +2350,13 @@ function updateDOMWithData(data) {
|
|||||||
if (!mapState.map) {
|
if (!mapState.map) {
|
||||||
initMap(latlng, data.accuracy);
|
initMap(latlng, data.accuracy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 라이브 모드에서도 최신 24시간 기록을 계속 반영한다.
|
||||||
|
// 현재 위치 자동 이동은 지도 중심만 바꾸므로, 경로와 종료 마커는 여기서 별도로 갱신한다.
|
||||||
|
if (data.history?.length > 0) {
|
||||||
|
applyHistoryCounts(data);
|
||||||
|
updateHistoryMarkers(data.history);
|
||||||
|
}
|
||||||
|
|
||||||
// 메인 마커 / 정확도 원 업데이트
|
// 메인 마커 / 정확도 원 업데이트
|
||||||
mapState.marker.setPosition(latlng);
|
mapState.marker.setPosition(latlng);
|
||||||
@@ -2430,6 +2437,18 @@ function syncUpdateBtn() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readHistoryRangeInputs() {
|
||||||
|
const toServerTime = value => value
|
||||||
|
? (value.includes('T') ? value.replace('T', ' ') : value) + (value.length === 16 ? ':00' : '')
|
||||||
|
: null;
|
||||||
|
const start = document.getElementById('rangeStart')?.value || '';
|
||||||
|
const end = document.getElementById('rangeEnd')?.value || '';
|
||||||
|
return {
|
||||||
|
startTime: toServerTime(start),
|
||||||
|
endTime: toServerTime(end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function fetchDeviceData(includeLang = true, range = null) {
|
function fetchDeviceData(includeLang = true, range = null) {
|
||||||
if (!findmydeviceAuthenticated) {
|
if (!findmydeviceAuthenticated) {
|
||||||
return Promise.reject(new Error('not_authenticated'));
|
return Promise.reject(new Error('not_authenticated'));
|
||||||
@@ -2437,8 +2456,7 @@ function fetchDeviceData(includeLang = true, range = null) {
|
|||||||
|
|
||||||
const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 });
|
const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 });
|
||||||
|
|
||||||
// 우선순위: 직접 인자(range) > 전역 오버라이드(_overrideRange)
|
const eff = range || _overrideRange || (includeLang ? readHistoryRangeInputs() : {});
|
||||||
const eff = range || _overrideRange || {};
|
|
||||||
const hasHistoryRange = includeLang && !!(eff.startTime || eff.endTime);
|
const hasHistoryRange = includeLang && !!(eff.startTime || eff.endTime);
|
||||||
|
|
||||||
const s = eff.startTime;
|
const s = eff.startTime;
|
||||||
@@ -2449,7 +2467,7 @@ function fetchDeviceData(includeLang = true, range = null) {
|
|||||||
if (hasHistoryRange) {
|
if (hasHistoryRange) {
|
||||||
params.set('history', '1');
|
params.set('history', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://chaegeon.com/custom/findmydevice/php/api.php?${params.toString()}`;
|
const url = `https://chaegeon.com/custom/findmydevice/php/api.php?${params.toString()}`;
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
|
|||||||
Reference in New Issue
Block a user