Optimize Find My Device history

This commit is contained in:
seo
2026-06-12 16:42:50 +09:00
parent 5d73ac520b
commit f1f038ce23
3 changed files with 360 additions and 181 deletions
+164 -158
View File
@@ -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()}`;