From 42b8fb097e8d4d35aa13cf48ce89b0a3dae513c2 Mon Sep 17 00:00:00 2001 From: seo Date: Sun, 12 Jul 2026 14:54:45 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=B2=B4?= =?UTF-8?q?=EB=A5=98=EC=A4=91=20=EB=88=84=EC=A0=81=20=EB=B0=B0=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- js/main.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) 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: `
${text}
`, + 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) {