diff --git a/README.md b/README.md
index 9f0e6cd..808274a 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그
집 판정은 설정된 서울 집과 목포 집 좌표를 중심점으로 한 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 안에 다시 들어오면 초상화 위를 가리지 않는 커스텀 배지로 누적 체류중 시간을 표시합니다.
재생 중 주기적 위치 갱신으로 히스토리 경로가 다시 그려져도 현재 재생 위치를 유지합니다. 재생 타이머는 시작 당시 지점 목록에 고정하지 않고 최신 히스토리 지점 목록을 기준으로 계속 진행합니다.
diff --git a/js/main.js b/js/main.js
index 55502ff..e9d4981 100644
--- a/js/main.js
+++ b/js/main.js
@@ -433,6 +433,9 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
trackLines: [],
smallMarkers: [],
stayMarkers: [],
+ currentStayMarker: null,
+ currentStayTimer: null,
+ currentStayState: null,
historyStartMarker: null,
historyEndMarker: null,
playbackMarker: null,
@@ -2233,6 +2236,103 @@ function createFaceMarkerIcon(size = 60) {
};
}
+function createCurrentStayIcon(text) {
+ return {
+ content: `
`,
+ 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) {
mapState.marker = new naver.maps.Marker({
position: latlng,
@@ -2266,6 +2366,7 @@ function setCurrentLocationVisible(visible) {
const map = visible ? mapState.map : null;
mapState.marker?.setMap?.(map);
mapState.circle?.setMap?.(map);
+ if (!visible) stopCurrentStayBadge();
}
function applyMarkerEffects(marker) {
@@ -2321,6 +2422,7 @@ function bounceMarker(marker) {
function resetHistoryLayer() {
stopHistoryPlayback();
+ stopCurrentStayBadge();
clearTrackLines();
if (mapState.smallMarkers?.length) {
mapState.smallMarkers.forEach(m => m.setMap?.(null));
@@ -2479,6 +2581,7 @@ function updateDOMWithData(data) {
}
updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude);
setCurrentLocationVisible(false);
+ stopCurrentStayBadge();
clearCurrentRangeCircles();
// 히스토리 궤적/포인트 업데이트
@@ -2530,6 +2633,12 @@ function updateDOMWithData(data) {
lastLat = data.latitude;
lastLng = data.longitude;
lastUpdateISOTime = data.updated;
+ updateCurrentStayBadge({
+ lat: data.latitude,
+ lng: data.longitude,
+ latlng,
+ time: data.updated || new Date().toISOString(),
+ });
}
function animateCircleRadius(targetRadius) {