라이브 체류중 누적 배지 표시

This commit is contained in:
seo
2026-07-12 14:54:45 +09:00
parent 57f47c43ab
commit 42b8fb097e
2 changed files with 110 additions and 1 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그
집 판정은 설정된 서울 집과 목포 집 좌표를 중심점으로 한 150m 반경을 기준으로 합니다. GPS 튐 보정이 켜져 있으면 두 집 주변 1.2km 안의 지점은 가까운 집 위치로 흡수하고, 외출은 연속된 외부 지점이 5분 이상 지속될 때만 세션으로 확정합니다. 외부 지점 사이의 공백이 2시간을 넘으면 해당 구간은 알 수 없는 구간으로 보고 외출 지속시간에 합산하지 않습니다. 여러 번 나갔다 돌아온 경우 집 체류 시간은 합산하고, 외출은 출발/복귀 단위의 세션으로 따로 보여줍니다. 집 판정은 설정된 서울 집과 목포 집 좌표를 중심점으로 한 150m 반경을 기준으로 합니다. GPS 튐 보정이 켜져 있으면 두 집 주변 1.2km 안의 지점은 가까운 집 위치로 흡수하고, 외출은 연속된 외부 지점이 5분 이상 지속될 때만 세션으로 확정합니다. 외부 지점 사이의 공백이 2시간을 넘으면 해당 구간은 알 수 없는 구간으로 보고 외출 지속시간에 합산하지 않습니다. 여러 번 나갔다 돌아온 경우 집 체류 시간은 합산하고, 외출은 출발/복귀 단위의 세션으로 따로 보여줍니다.
기록 시간 슬라이더를 움직이면 지도 아래에는 `월. 일. 시간` 형식의 선택 시각만 간결하게 표시합니다. 경로 재생은 얼굴 아이콘 마커만 사용하며, 재생 중에는 현재 위치 얼굴과 정확도 원을 숨깁니다. 재생 버튼은 지점 수에 따라 의미 있는 배속만 남겨 `2x`, `5x`, `10x`, `20x`, `50x`, `100x`, `500x` 순서로 돌고, 각 배속에서 최소 80단계 이상 보여줄 수 없으면 해당 배속은 건너뜁니다. 100m 반경 안에서 10분 이상 머문 구간은 체류 지점 마커로 묶어 표시하되, 서울 집과 목포 집 안의 체류는 계산하거나 표시하지 않습니다. 같은 위치의 체류는 하나의 마커로 합산하고, 클릭 시 총 체류 시간과 방문별 시작/종료/체류 시간을 확인할 수 있습니다. 기록 시간 슬라이더를 움직이면 지도 아래에는 `월. 일. 시간` 형식의 선택 시각만 간결하게 표시합니다. 경로 재생은 얼굴 아이콘 마커만 사용하며, 재생 중에는 현재 위치 얼굴과 정확도 원을 숨깁니다. 재생 버튼은 지점 수에 따라 의미 있는 배속만 남겨 `2x`, `5x`, `10x`, `20x`, `50x`, `100x`, `500x` 순서로 돌고, 각 배속에서 최소 80단계 이상 보여줄 수 없으면 해당 배속은 건너뜁니다. 100m 반경 안에서 10분 이상 머문 구간은 체류 지점 마커로 묶어 표시하되, 서울 집과 목포 집 안의 체류는 계산하거나 표시하지 않습니다. 같은 위치의 체류는 하나의 마커로 합산하고, 클릭 시 총 체류 시간과 방문별 시작/종료/체류 시간을 확인할 수 있습니다. 라이브 위치가 기존 체류 지점 100m 안에 다시 들어오면 초상화 위를 가리지 않는 커스텀 배지로 누적 체류중 시간을 표시합니다.
재생 중 주기적 위치 갱신으로 히스토리 경로가 다시 그려져도 현재 재생 위치를 유지합니다. 재생 타이머는 시작 당시 지점 목록에 고정하지 않고 최신 히스토리 지점 목록을 기준으로 계속 진행합니다. 재생 중 주기적 위치 갱신으로 히스토리 경로가 다시 그려져도 현재 재생 위치를 유지합니다. 재생 타이머는 시작 당시 지점 목록에 고정하지 않고 최신 히스토리 지점 목록을 기준으로 계속 진행합니다.
+109
View File
@@ -433,6 +433,9 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
trackLines: [], trackLines: [],
smallMarkers: [], smallMarkers: [],
stayMarkers: [], stayMarkers: [],
currentStayMarker: null,
currentStayTimer: null,
currentStayState: null,
historyStartMarker: null, historyStartMarker: null,
historyEndMarker: null, historyEndMarker: null,
playbackMarker: null, playbackMarker: null,
@@ -2233,6 +2236,103 @@ function createFaceMarkerIcon(size = 60) {
}; };
} }
function createCurrentStayIcon(text) {
return {
content: `<div class="current-stay-badge-wrap"><div class="current-stay-badge">${text}</div></div>`,
anchor: new naver.maps.Point(0, 0)
};
}
function currentStayText(seconds) {
const duration = formatDuration(Math.round(seconds));
return langCode === 'ko' ? `누적 ${duration} 체류중` : `${duration} staying`;
}
function stopCurrentStayBadge() {
if (mapState.currentStayTimer) {
clearInterval(mapState.currentStayTimer);
mapState.currentStayTimer = null;
}
if (mapState.currentStayMarker) {
mapState.currentStayMarker.setMap(null);
}
mapState.currentStayState = null;
}
function currentStaySegmentStart(stay, currentPoint) {
const points = mapState.historyPoints || [];
let startTime = currentPoint.time || new Date().toISOString();
for (let i = points.length - 1; i >= 0; i--) {
const point = points[i];
if (distanceMeters(stay, point) > STAY_GROUP_RADIUS_M) {
break;
}
startTime = point.time || startTime;
}
return startTime;
}
function currentStayStateFor(point) {
if (!mapState.map || !hasValidCoords(point.lat, point.lng) || !mapState.stayPoints?.length) return null;
const stay = mapState.stayPoints.find(item => distanceMeters(item, point) <= STAY_GROUP_RADIUS_M);
if (!stay) return null;
const visits = Array.isArray(stay.visits) ? stay.visits : [];
const segmentStart = currentStaySegmentStart(stay, point);
const segmentStartTs = toTimestamp(segmentStart) || toTimestamp(point.time) || Date.now();
const pointTs = toTimestamp(point.time) || Date.now();
const lastVisit = visits.at(-1);
const latestVisitIsCurrent = lastVisit
&& Math.abs((toTimestamp(lastVisit.endTime) || 0) - pointTs) <= 60000
&& distanceMeters(stay, point) <= STAY_GROUP_RADIUS_M;
const baseDuration = Math.max(0, Number(stay.duration) || 0) - (latestVisitIsCurrent ? Math.max(0, Number(lastVisit.duration) || 0) : 0);
return {
latlng: point.latlng || new naver.maps.LatLng(point.lat, point.lng),
baseDuration,
startedAt: segmentStartTs,
};
}
function renderCurrentStayBadge() {
const state = mapState.currentStayState;
if (!state || !mapState.currentStayMarker) return;
const elapsed = Math.max(0, (Date.now() - state.startedAt) / 1000);
const text = currentStayText(state.baseDuration + elapsed);
mapState.currentStayMarker.setIcon(createCurrentStayIcon(text));
}
function updateCurrentStayBadge(point) {
if (mapState.playbackTimer || _overrideRange) {
stopCurrentStayBadge();
return;
}
const state = currentStayStateFor(point);
if (!state) {
stopCurrentStayBadge();
return;
}
mapState.currentStayState = state;
if (!mapState.currentStayMarker) {
mapState.currentStayMarker = new naver.maps.Marker({
position: state.latlng,
map: mapState.map,
zIndex: 130,
icon: createCurrentStayIcon(currentStayText(state.baseDuration))
});
} else {
mapState.currentStayMarker.setMap(mapState.map);
mapState.currentStayMarker.setPosition(state.latlng);
}
renderCurrentStayBadge();
if (!mapState.currentStayTimer) {
mapState.currentStayTimer = setInterval(renderCurrentStayBadge, 1000);
}
}
function createMainMarker(latlng) { function createMainMarker(latlng) {
mapState.marker = new naver.maps.Marker({ mapState.marker = new naver.maps.Marker({
position: latlng, position: latlng,
@@ -2266,6 +2366,7 @@ function setCurrentLocationVisible(visible) {
const map = visible ? mapState.map : null; const map = visible ? mapState.map : null;
mapState.marker?.setMap?.(map); mapState.marker?.setMap?.(map);
mapState.circle?.setMap?.(map); mapState.circle?.setMap?.(map);
if (!visible) stopCurrentStayBadge();
} }
function applyMarkerEffects(marker) { function applyMarkerEffects(marker) {
@@ -2321,6 +2422,7 @@ function bounceMarker(marker) {
function resetHistoryLayer() { function resetHistoryLayer() {
stopHistoryPlayback(); stopHistoryPlayback();
stopCurrentStayBadge();
clearTrackLines(); clearTrackLines();
if (mapState.smallMarkers?.length) { if (mapState.smallMarkers?.length) {
mapState.smallMarkers.forEach(m => m.setMap?.(null)); mapState.smallMarkers.forEach(m => m.setMap?.(null));
@@ -2479,6 +2581,7 @@ function updateDOMWithData(data) {
} }
updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude); updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude);
setCurrentLocationVisible(false); setCurrentLocationVisible(false);
stopCurrentStayBadge();
clearCurrentRangeCircles(); clearCurrentRangeCircles();
// 히스토리 궤적/포인트 업데이트 // 히스토리 궤적/포인트 업데이트
@@ -2530,6 +2633,12 @@ function updateDOMWithData(data) {
lastLat = data.latitude; lastLat = data.latitude;
lastLng = data.longitude; lastLng = data.longitude;
lastUpdateISOTime = data.updated; lastUpdateISOTime = data.updated;
updateCurrentStayBadge({
lat: data.latitude,
lng: data.longitude,
latlng,
time: data.updated || new Date().toISOString(),
});
} }
function animateCircleRadius(targetRadius) { function animateCircleRadius(targetRadius) {