Optimize Find My Device history
This commit is contained in:
+164
-158
@@ -51,10 +51,14 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
device: "기기",
|
||||
displayAudio: "화면 및 오디오",
|
||||
health: "건강",
|
||||
wellness: "건강/활동",
|
||||
car: "차량",
|
||||
storageData: "저장공간 및 데이터",
|
||||
notifications: "알림",
|
||||
alerts: "알림/통화",
|
||||
environment: "센서",
|
||||
networkType: "네트워크 방식",
|
||||
wifiState: "Wi-Fi 상태",
|
||||
wifiBssid: "Wi-Fi BSSID",
|
||||
mobileDataRoaming: "데이터 로밍",
|
||||
publicIpAddress: "공인 IP",
|
||||
@@ -213,10 +217,14 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
device: "Device",
|
||||
displayAudio: "Display & Audio",
|
||||
health: "Health",
|
||||
wellness: "Wellness",
|
||||
car: "Car",
|
||||
storageData: "Storage & Data",
|
||||
notifications: "Notifications",
|
||||
alerts: "Alerts & Calls",
|
||||
environment: "Sensors",
|
||||
networkType: "Network Type",
|
||||
wifiState: "Wi-Fi State",
|
||||
wifiBssid: "Wi-Fi BSSID",
|
||||
mobileDataRoaming: "Data Roaming",
|
||||
publicIpAddress: "Public IP",
|
||||
@@ -329,12 +337,15 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
map: null,
|
||||
marker: null,
|
||||
circle: null,
|
||||
trackLine: null,
|
||||
smallMarkers: [],
|
||||
openInfoWindow: null,
|
||||
mapClickHandlerAdded: false,
|
||||
initialInfoOpened: false,
|
||||
};
|
||||
trackLine: null,
|
||||
smallMarkers: [],
|
||||
historyStartMarker: null,
|
||||
historyEndMarker: null,
|
||||
openInfoWindow: null,
|
||||
mapClickHandlerAdded: false,
|
||||
initialInfoOpened: false,
|
||||
historyBoundsFitted: false,
|
||||
};
|
||||
let langCode = localStorage.getItem("currentLang") || deviceLang,
|
||||
lastAutoSwitchChecked,
|
||||
lastUpdateSwitchChecked,
|
||||
@@ -617,16 +628,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
// ✅ 사용자가 변경했거나 URL에 기간이 있으면 히스토리 모드
|
||||
if (userTriggered || hasQS) {
|
||||
_overrideRange = range;
|
||||
resetHistoryLayer();
|
||||
} else {
|
||||
_overrideRange = null; // 라이브 모드 유지
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchDeviceData(true, range);
|
||||
updateDOMWithData(data);
|
||||
const shouldUseHistory = userTriggered || hasQS;
|
||||
if (shouldUseHistory) {
|
||||
_overrideRange = range;
|
||||
resetHistoryLayer();
|
||||
} else {
|
||||
_overrideRange = null; // 라이브 모드 유지
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchDeviceData(true, shouldUseHistory ? range : null);
|
||||
updateDOMWithData(data);
|
||||
|
||||
const last = data.history && data.history[data.history.length - 1];
|
||||
if (last && mapState.map) {
|
||||
@@ -1085,128 +1097,122 @@ function resetHistoryLayer() {
|
||||
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: `
|
||||
<div style="position: relative; width:36px; height:36px;">
|
||||
<div style="position:absolute; top:0; left:0;
|
||||
width:36px; height:36px;
|
||||
border-radius:50%;
|
||||
background:transparent;
|
||||
cursor:pointer;"></div>
|
||||
<div style="position:absolute; top:12px; left:12px;
|
||||
width:12px; height:12px;
|
||||
background:#0051A8;
|
||||
border:2px solid white;
|
||||
border-radius:50%;
|
||||
box-shadow:0 0 2px rgba(0,0,0,0.5);"></div>
|
||||
</div>`,
|
||||
anchor: new naver.maps.Point(18, 18)
|
||||
}
|
||||
});
|
||||
smallMarker.userTime = pos.time; // ⚑ 표시된 마커의 시간 정보를 유지
|
||||
mapState.smallMarkers.push(smallMarker);
|
||||
|
||||
const infoWindow = new naver.maps.InfoWindow({
|
||||
content: `<div style="font-size:12px;padding:4px;">${getRelativeTime(pos.time)}</div>`
|
||||
});
|
||||
|
||||
smallMarker.infoWindow = infoWindow;
|
||||
|
||||
naver.maps.Event.addListener(smallMarker, "click", () => {
|
||||
if (mapState.openInfoWindow && mapState.openInfoWindow !== infoWindow) {
|
||||
mapState.openInfoWindow.close();
|
||||
}
|
||||
if (infoWindow.getMap()) {
|
||||
infoWindow.close();
|
||||
mapState.openInfoWindow = null;
|
||||
} else {
|
||||
infoWindow.open(mapState.map, smallMarker);
|
||||
mapState.openInfoWindow = infoWindow;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 2) ★★ 지금 "화면에 표시된 스몰마커들"만으로 폴리라인 갱신 ★★
|
||||
renderTrackFromDisplayedMarkers();
|
||||
|
||||
// ★ 최초 로드시 마지막(가장 최근) 스몰마커의 InfoWindow 자동 오픈
|
||||
if (!mapState.initialInfoOpened && mapState.smallMarkers.length) {
|
||||
// 시간순 정렬 후 마지막 선택
|
||||
const toTime = (v) => {
|
||||
if (v instanceof Date) return v.getTime();
|
||||
if (typeof v === 'number' && isFinite(v)) return v < 2e10 ? v * 1000 : v;
|
||||
const t = Date.parse(v);
|
||||
return isNaN(t) ? 0 : t;
|
||||
};
|
||||
|
||||
const lastMarker = mapState.smallMarkers
|
||||
.slice()
|
||||
.sort((a, b) => toTime(a.userTime) - toTime(b.userTime))
|
||||
.at(-1);
|
||||
|
||||
if (lastMarker && lastMarker.infoWindow) {
|
||||
// 이전 열린 창 닫기
|
||||
if (mapState.openInfoWindow && mapState.openInfoWindow !== lastMarker.infoWindow) {
|
||||
mapState.openInfoWindow.close();
|
||||
}
|
||||
|
||||
lastMarker.infoWindow.open(mapState.map, lastMarker);
|
||||
mapState.openInfoWindow = lastMarker.infoWindow;
|
||||
|
||||
// 보기 편하게 살짝 이동
|
||||
mapState.map.panTo(lastMarker.getPosition());
|
||||
}
|
||||
|
||||
mapState.initialInfoOpened = true;
|
||||
}
|
||||
|
||||
// map 클릭시 열린 InfoWindow 닫기 (1회만 등록)
|
||||
if (!mapState.mapClickHandlerAdded) {
|
||||
naver.maps.Event.addListener(mapState.map, "click", () => {
|
||||
if (mapState.openInfoWindow) {
|
||||
if (mapState.smallMarkers?.length) {
|
||||
mapState.smallMarkers.forEach(m => m.setMap?.(null));
|
||||
mapState.smallMarkers = [];
|
||||
}
|
||||
if (mapState.historyStartMarker) {
|
||||
mapState.historyStartMarker.setMap(null);
|
||||
mapState.historyStartMarker = null;
|
||||
}
|
||||
if (mapState.historyEndMarker) {
|
||||
mapState.historyEndMarker.setMap(null);
|
||||
mapState.historyEndMarker = null;
|
||||
}
|
||||
if (mapState.openInfoWindow) {
|
||||
mapState.openInfoWindow.close();
|
||||
mapState.openInfoWindow = null;
|
||||
}
|
||||
mapState.initialInfoOpened = false;
|
||||
mapState.historyBoundsFitted = false;
|
||||
}
|
||||
|
||||
function updateHistoryMarkers(history) {
|
||||
if (!Array.isArray(history) || history.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!mapState.map) return;
|
||||
|
||||
const toTime = (v) => {
|
||||
if (v instanceof Date) return v.getTime();
|
||||
if (typeof v === 'number' && isFinite(v)) return v < 2e10 ? v * 1000 : v;
|
||||
const t = Date.parse(v);
|
||||
return isNaN(t) ? 0 : t;
|
||||
};
|
||||
|
||||
const points = history
|
||||
.filter(pos => hasValidCoords(pos.lat, pos.lng))
|
||||
.sort((a, b) => toTime(a.time) - toTime(b.time))
|
||||
.map(pos => ({ ...pos, latlng: new naver.maps.LatLng(pos.lat, pos.lng) }));
|
||||
|
||||
if (!points.length) return;
|
||||
|
||||
if (mapState.trackLine) mapState.trackLine.setMap(null);
|
||||
mapState.trackLine = new naver.maps.Polyline({
|
||||
map: mapState.map,
|
||||
path: points.map(pos => pos.latlng),
|
||||
strokeColor: '#007AFF',
|
||||
strokeOpacity: 0.85,
|
||||
strokeWeight: 4
|
||||
});
|
||||
|
||||
if (mapState.smallMarkers?.length) {
|
||||
mapState.smallMarkers.forEach(marker => marker.setMap(null));
|
||||
mapState.smallMarkers = [];
|
||||
}
|
||||
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)
|
||||
}
|
||||
});
|
||||
marker.userTime = pos.time;
|
||||
marker.infoWindow = new naver.maps.InfoWindow({
|
||||
content: `<div style="font-size:12px;padding:6px 8px;line-height:1.4;">${label ? `${label}<br>` : ''}${getRelativeTime(pos.time)}</div>`
|
||||
});
|
||||
naver.maps.Event.addListener(marker, "click", () => {
|
||||
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
|
||||
mapState.openInfoWindow.close();
|
||||
}
|
||||
if (marker.infoWindow.getMap()) {
|
||||
marker.infoWindow.close();
|
||||
mapState.openInfoWindow = null;
|
||||
} else {
|
||||
marker.infoWindow.open(mapState.map, marker);
|
||||
mapState.openInfoWindow = marker.infoWindow;
|
||||
}
|
||||
});
|
||||
return marker;
|
||||
};
|
||||
|
||||
const first = points[0];
|
||||
const last = points.at(-1);
|
||||
mapState.historyStartMarker = makeMarker(first, 'start');
|
||||
mapState.historyEndMarker = makeMarker(last, 'end');
|
||||
|
||||
const maxDots = 60;
|
||||
const step = Math.max(1, Math.ceil(points.length / maxDots));
|
||||
points.forEach((pos, idx) => {
|
||||
if (idx === 0 || idx === points.length - 1 || idx % step !== 0) return;
|
||||
mapState.smallMarkers.push(makeMarker(pos));
|
||||
});
|
||||
|
||||
if (!mapState.historyBoundsFitted) {
|
||||
const bounds = new naver.maps.LatLngBounds(first.latlng, first.latlng);
|
||||
points.forEach(pos => bounds.extend(pos.latlng));
|
||||
mapState.map.fitBounds(bounds);
|
||||
mapState.historyBoundsFitted = true;
|
||||
}
|
||||
|
||||
if (!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) {
|
||||
mapState.openInfoWindow.close();
|
||||
mapState.openInfoWindow = null;
|
||||
}
|
||||
@@ -1251,20 +1257,16 @@ function updateDOMWithData(data) {
|
||||
updateActivitySection(data);
|
||||
|
||||
// 지도 초기화
|
||||
if (!mapState.map) {
|
||||
initMap(latlng, data.accuracy);
|
||||
if (data.history?.length > 0) updateHistoryMarkers(data.history);
|
||||
}
|
||||
if (!mapState.map) {
|
||||
initMap(latlng, data.accuracy);
|
||||
}
|
||||
|
||||
// 메인 마커 / 정확도 원 업데이트
|
||||
mapState.marker.setPosition(latlng);
|
||||
mapState.circle.setCenter(latlng);
|
||||
animateCircleRadius(Math.max(data.accuracy || 50, 10));
|
||||
|
||||
// history (있으면) 업데이트
|
||||
if (data.history?.length > 0) updateHistoryMarkers(data.history);
|
||||
|
||||
// autoPosition
|
||||
// autoPosition
|
||||
const isChecked = document.getElementById('autoPositionSwitch').checked;
|
||||
if (isChecked && (lastLat !== data.latitude || lastLng !== data.longitude)) {
|
||||
mapState.map.panTo(latlng);
|
||||
@@ -1341,18 +1343,22 @@ function syncUpdateBtn() {
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDeviceData(includeLang = true, range = null) {
|
||||
const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 });
|
||||
|
||||
// 우선순위: 직접 인자(range) > 전역 오버라이드(_overrideRange) > URL 쿼리
|
||||
const eff = range || _overrideRange || {};
|
||||
const qs = new URLSearchParams(window.location.search);
|
||||
|
||||
const s = eff.startTime || qs.get('startTime');
|
||||
const e = eff.endTime || qs.get('endTime');
|
||||
|
||||
if (s) params.set('startTime', s);
|
||||
if (e) params.set('endTime', e);
|
||||
function fetchDeviceData(includeLang = true, range = null) {
|
||||
const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 });
|
||||
|
||||
// 우선순위: 직접 인자(range) > 전역 오버라이드(_overrideRange)
|
||||
const eff = range || _overrideRange || {};
|
||||
const hasHistoryRange = includeLang && !!(eff.startTime || eff.endTime);
|
||||
|
||||
const s = eff.startTime;
|
||||
const e = eff.endTime;
|
||||
|
||||
if (s) params.set('startTime', s);
|
||||
if (e) params.set('endTime', e);
|
||||
if (hasHistoryRange) {
|
||||
params.set('history', '1');
|
||||
params.set('maxPoints', '300');
|
||||
}
|
||||
|
||||
const url = `https://chaegeon.com/custom/findmydevice/php/api.php?${params.toString()}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user