1806 lines
68 KiB
JavaScript
1806 lines
68 KiB
JavaScript
const deviceLang = (navigator.language || navigator.userLanguage || "en").startsWith("ko") ? "ko" : "en",
|
||
translations = {
|
||
ko: {
|
||
blog: "블로그",
|
||
title: "나의 찾기",
|
||
keepScreenOn: "화면 켜짐 유지",
|
||
settings: "설정",
|
||
location: "위치",
|
||
currentPosition: "현재 위치",
|
||
gpsAccuracy: "GPS 정확도",
|
||
moveToCurrent: "현재 위치로 이동하기",
|
||
update: "업데이트",
|
||
lastUpdate: "마지막 업데이트",
|
||
updateTrigger: "업데이트 트리거",
|
||
autoCurrentUpdate: "현재 위치 자동 이동",
|
||
autoUpdate: "자동 업데이트",
|
||
forceUpdate: "업데이트 하기",
|
||
battery: "배터리",
|
||
batteryState: "배터리 상태",
|
||
batteryLevel: "배터리 잔량",
|
||
watchbatteryState: "워치 배터리 상태",
|
||
watchbatteryLevel: "워치 배터리 잔량",
|
||
isCharging: "충전 여부",
|
||
chargerType: "충전 방식",
|
||
batteryHealth: "배터리 건강",
|
||
batteryTemperature: "배터리 온도",
|
||
batteryPower: "배터리 전력",
|
||
remainingChargeTime: "충전 남은 시간",
|
||
batteryCycleCount: "배터리 사이클",
|
||
activity: "활동",
|
||
currentActivity: "현재 활동",
|
||
activityAccuracy: "활동 정확도",
|
||
distance: "도보로 움직인 거리",
|
||
steps: "걸음 수",
|
||
ascended: "오른 계단",
|
||
descended: "내려간 계단",
|
||
etc: "기타",
|
||
connection: "연결 방법",
|
||
wifiConnection: "Wi-Fi",
|
||
wifiIp: "Wi-Fi IP",
|
||
wifiLinkSpeed: "Wi-Fi 속도",
|
||
wifiFrequency: "Wi-Fi 주파수",
|
||
wifiSignalStrength: "Wi-Fi 신호",
|
||
mobileData: "모바일 데이터",
|
||
phoneState: "전화 상태",
|
||
interactive: "화면 활성",
|
||
dozeMode: "Doze 모드",
|
||
powerSave: "절전 모드",
|
||
deviceLocked: "잠금 상태",
|
||
network: "네트워크",
|
||
device: "기기",
|
||
displayAudio: "화면 및 오디오",
|
||
health: "건강",
|
||
wellness: "건강/활동",
|
||
car: "차량",
|
||
storageData: "저장공간 및 데이터",
|
||
notifications: "알림",
|
||
alerts: "알림/통화",
|
||
environment: "센서",
|
||
networkType: "네트워크 방식",
|
||
wifiState: "Wi-Fi 상태",
|
||
wifiBssid: "Wi-Fi BSSID",
|
||
mobileDataRoaming: "데이터 로밍",
|
||
publicIpAddress: "공인 IP",
|
||
hotspotState: "핫스팟",
|
||
ipv6Addresses: "IPv6 주소",
|
||
nfcState: "NFC",
|
||
sim1: "SIM 1",
|
||
sim2: "SIM 2",
|
||
signalStrengthSim1: "SIM 1 신호",
|
||
signalStrengthSim2: "SIM 2 신호",
|
||
dataNetworkTypeSim1: "SIM 1 데이터망",
|
||
dataNetworkTypeSim2: "SIM 2 데이터망",
|
||
osVersion: "OS 버전",
|
||
securityPatch: "보안 패치",
|
||
currentVersion: "앱 버전",
|
||
lastReboot: "마지막 재부팅",
|
||
currentTimeZone: "시간대",
|
||
deviceSecure: "보안 설정",
|
||
keyguardLocked: "키가드 잠김",
|
||
keyguardSecure: "키가드 보안",
|
||
workProfile: "업무 프로필",
|
||
lastUsedApp: "마지막 사용 앱",
|
||
appInactive: "앱 비활성",
|
||
appStandbyBucket: "앱 대기 버킷",
|
||
appImportance: "앱 중요도",
|
||
appMemory: "앱 메모리",
|
||
screenBrightness: "화면 밝기",
|
||
screenOffTimeout: "화면 꺼짐 시간",
|
||
screenOrientation: "화면 방향",
|
||
screenRotation: "화면 회전",
|
||
doNotDisturb: "방해 금지",
|
||
ringerMode: "벨소리 모드",
|
||
audioMode: "오디오 모드",
|
||
headphones: "헤드폰",
|
||
micMuted: "마이크 음소거",
|
||
speakerphone: "스피커폰",
|
||
musicActive: "미디어 재생",
|
||
volumeAlarm: "알람 볼륨",
|
||
volumeCall: "통화 볼륨",
|
||
volumeMusic: "미디어 볼륨",
|
||
volumeRinger: "벨소리 볼륨",
|
||
volumeNotification: "알림 볼륨",
|
||
volumeSystem: "시스템 볼륨",
|
||
volumeDtmf: "DTMF 볼륨",
|
||
volumeAccessibility: "접근성 볼륨",
|
||
sleepConfidence: "수면 신뢰도",
|
||
sleepSegment: "수면 구간",
|
||
sleepDuration: "수면 시간",
|
||
activeCaloriesBurned: "활동 칼로리",
|
||
totalCaloriesBurned: "총 칼로리",
|
||
basalBodyTemperature: "기초 체온",
|
||
basalMetabolicRate: "기초대사량",
|
||
bloodGlucose: "혈당",
|
||
bodyFat: "체지방",
|
||
bodyTemperature: "체온",
|
||
bodyWaterMass: "체수분",
|
||
boneMass: "골량",
|
||
diastolicBloodPressure: "이완기 혈압",
|
||
systolicBloodPressure: "수축기 혈압",
|
||
heartRate: "심박수",
|
||
heartRateVariability: "심박 변이도",
|
||
restingHeartRate: "안정 심박수",
|
||
oxygenSaturation: "산소포화도",
|
||
respiratoryRate: "호흡수",
|
||
vo2Max: "VO2 Max",
|
||
weight: "체중",
|
||
height: "키",
|
||
dailyHydration: "일일 수분",
|
||
dailyElevationGained: "일일 고도 상승",
|
||
stepsSensor: "걸음 센서",
|
||
androidAuto: "Android Auto",
|
||
carName: "차량 이름",
|
||
carBattery: "차량 배터리",
|
||
carChargingStatus: "차량 충전 상태",
|
||
carEvConnectorType: "차량 충전 커넥터",
|
||
carFuel: "차량 연료",
|
||
carFuelType: "차량 연료 종류",
|
||
carOdometer: "차량 주행거리",
|
||
carSpeed: "차량 속도",
|
||
carRangeRemaining: "차량 잔여 주행거리",
|
||
internalStorage: "내부 저장공간",
|
||
externalStorage: "외부 저장공간",
|
||
appRxGb: "앱 수신 데이터",
|
||
appTxGb: "앱 송신 데이터",
|
||
mobileRxGb: "모바일 수신 데이터",
|
||
mobileTxGb: "모바일 송신 데이터",
|
||
totalRxGb: "전체 수신 데이터",
|
||
totalTxGb: "전체 송신 데이터",
|
||
nextAlarm: "다음 알람",
|
||
lastNotification: "마지막 알림",
|
||
lastRemovedNotification: "마지막 제거 알림",
|
||
activeNotificationCount: "활성 알림 수",
|
||
mediaSession: "미디어 세션",
|
||
lightSensor: "조도 센서",
|
||
pressureSensor: "기압 센서",
|
||
proximitySensor: "근접 센서",
|
||
bluetoothConnection: "블루투스 연결",
|
||
bluetoothState: "블루투스",
|
||
bleTransmitter: "BLE 송신기",
|
||
beaconMonitor: "비콘 모니터",
|
||
accentColor: "강조 색상",
|
||
editPage: "페이지 수정",
|
||
pwa: "눌러 홈 화면에 추가해 바로 보기",
|
||
history: "기록",
|
||
startTime: "시작",
|
||
endTime: "종료",
|
||
mapNormal: "일반",
|
||
mapSatellite: "위성",
|
||
mapTraffic: "교통",
|
||
mapStreet: "거리뷰",
|
||
mapFit: "전체",
|
||
mapPlay: "재생",
|
||
mapPause: "정지",
|
||
historySummary: "기록 요약",
|
||
historyDistance: "이동 거리",
|
||
historyDuration: "기록 시간",
|
||
historyPoints: "지점",
|
||
historyAverageSpeed: "평균 속도",
|
||
historyMaxGap: "최대 공백",
|
||
historyStart: "시작",
|
||
historyEnd: "종료",
|
||
historyPoint: "기록 지점",
|
||
},
|
||
en: {
|
||
blog: "Blog",
|
||
title: "Find My Device",
|
||
keepScreenOn: "Keep Screen On",
|
||
settings: "Settings",
|
||
location: "Location",
|
||
currentPosition: "Current Location",
|
||
gpsAccuracy: "GPS Accuracy",
|
||
moveToCurrent: "Move to Current Location",
|
||
update: "Update",
|
||
lastUpdate: "Last Update",
|
||
updateTrigger: "Update Trigger",
|
||
autoCurrentUpdate: "Current Location Auto Update",
|
||
autoUpdate: "Auto Update",
|
||
forceUpdate: "Force Update",
|
||
battery: "Battery",
|
||
batteryState: "Battery State",
|
||
batteryLevel: "Battery Level",
|
||
watchbatteryState: "Watch Battery State",
|
||
watchbatteryLevel: "Watch Battery Level",
|
||
isCharging: "Charging",
|
||
chargerType: "Charger Type",
|
||
batteryHealth: "Battery Health",
|
||
batteryTemperature: "Battery Temp",
|
||
batteryPower: "Battery Power",
|
||
remainingChargeTime: "Remaining Charge",
|
||
batteryCycleCount: "Battery Cycles",
|
||
activity: "Activity",
|
||
currentActivity: "Current Activity",
|
||
activityAccuracy: "Activity Confidence",
|
||
distance: "Distance walked",
|
||
steps: "Steps",
|
||
ascended: "Floors Ascended",
|
||
descended: "Floors Descended",
|
||
etc: "Others",
|
||
connection: "Network Type",
|
||
wifiConnection: "Wi-Fi",
|
||
wifiIp: "Wi-Fi IP",
|
||
wifiLinkSpeed: "Wi-Fi Speed",
|
||
wifiFrequency: "Wi-Fi Frequency",
|
||
wifiSignalStrength: "Wi-Fi Signal",
|
||
mobileData: "Mobile Data",
|
||
phoneState: "Phone State",
|
||
interactive: "Interactive",
|
||
dozeMode: "Doze Mode",
|
||
powerSave: "Power Save",
|
||
deviceLocked: "Device Locked",
|
||
network: "Network",
|
||
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",
|
||
hotspotState: "Hotspot",
|
||
ipv6Addresses: "IPv6 Addresses",
|
||
nfcState: "NFC",
|
||
sim1: "SIM 1",
|
||
sim2: "SIM 2",
|
||
signalStrengthSim1: "SIM 1 Signal",
|
||
signalStrengthSim2: "SIM 2 Signal",
|
||
dataNetworkTypeSim1: "SIM 1 Data Network",
|
||
dataNetworkTypeSim2: "SIM 2 Data Network",
|
||
osVersion: "OS Version",
|
||
securityPatch: "Security Patch",
|
||
currentVersion: "App Version",
|
||
lastReboot: "Last Reboot",
|
||
currentTimeZone: "Time Zone",
|
||
deviceSecure: "Device Secure",
|
||
keyguardLocked: "Keyguard Locked",
|
||
keyguardSecure: "Keyguard Secure",
|
||
workProfile: "Work Profile",
|
||
lastUsedApp: "Last Used App",
|
||
appInactive: "App Inactive",
|
||
appStandbyBucket: "App Standby Bucket",
|
||
appImportance: "App Importance",
|
||
appMemory: "App Memory",
|
||
screenBrightness: "Screen Brightness",
|
||
screenOffTimeout: "Screen Timeout",
|
||
screenOrientation: "Screen Orientation",
|
||
screenRotation: "Screen Rotation",
|
||
doNotDisturb: "Do Not Disturb",
|
||
ringerMode: "Ringer Mode",
|
||
audioMode: "Audio Mode",
|
||
headphones: "Headphones",
|
||
micMuted: "Mic Muted",
|
||
speakerphone: "Speakerphone",
|
||
musicActive: "Music Active",
|
||
volumeAlarm: "Alarm Volume",
|
||
volumeCall: "Call Volume",
|
||
volumeMusic: "Music Volume",
|
||
volumeRinger: "Ringer Volume",
|
||
volumeNotification: "Notification Volume",
|
||
volumeSystem: "System Volume",
|
||
volumeDtmf: "DTMF Volume",
|
||
volumeAccessibility: "Accessibility Volume",
|
||
sleepConfidence: "Sleep Confidence",
|
||
sleepSegment: "Sleep Segment",
|
||
sleepDuration: "Sleep Duration",
|
||
activeCaloriesBurned: "Active Calories Burned",
|
||
totalCaloriesBurned: "Total Calories Burned",
|
||
basalBodyTemperature: "Basal Body Temperature",
|
||
basalMetabolicRate: "Basal Metabolic Rate",
|
||
bloodGlucose: "Blood Glucose",
|
||
bodyFat: "Body Fat",
|
||
bodyTemperature: "Body Temperature",
|
||
bodyWaterMass: "Body Water Mass",
|
||
boneMass: "Bone Mass",
|
||
diastolicBloodPressure: "Diastolic Blood Pressure",
|
||
systolicBloodPressure: "Systolic Blood Pressure",
|
||
heartRate: "Heart Rate",
|
||
heartRateVariability: "Heart Rate Variability",
|
||
restingHeartRate: "Resting Heart Rate",
|
||
oxygenSaturation: "Oxygen Saturation",
|
||
respiratoryRate: "Respiratory Rate",
|
||
vo2Max: "VO2 Max",
|
||
weight: "Weight",
|
||
height: "Height",
|
||
dailyHydration: "Daily Hydration",
|
||
dailyElevationGained: "Daily Elevation Gained",
|
||
stepsSensor: "Step Sensor",
|
||
androidAuto: "Android Auto",
|
||
carName: "Car Name",
|
||
carBattery: "Car Battery",
|
||
carChargingStatus: "Car Charging Status",
|
||
carEvConnectorType: "Car EV Connector",
|
||
carFuel: "Car Fuel",
|
||
carFuelType: "Car Fuel Type",
|
||
carOdometer: "Car Odometer",
|
||
carSpeed: "Car Speed",
|
||
carRangeRemaining: "Car Range Remaining",
|
||
internalStorage: "Internal Storage",
|
||
externalStorage: "External Storage",
|
||
appRxGb: "App RX Data",
|
||
appTxGb: "App TX Data",
|
||
mobileRxGb: "Mobile RX Data",
|
||
mobileTxGb: "Mobile TX Data",
|
||
totalRxGb: "Total RX Data",
|
||
totalTxGb: "Total TX Data",
|
||
nextAlarm: "Next Alarm",
|
||
lastNotification: "Last Notification",
|
||
lastRemovedNotification: "Last Removed Notification",
|
||
activeNotificationCount: "Active Notifications",
|
||
mediaSession: "Media Session",
|
||
lightSensor: "Light Sensor",
|
||
pressureSensor: "Pressure Sensor",
|
||
proximitySensor: "Proximity Sensor",
|
||
bluetoothConnection: "Bluetooth Connection",
|
||
bluetoothState: "Bluetooth",
|
||
bleTransmitter: "BLE Transmitter",
|
||
beaconMonitor: "Beacon Monitor",
|
||
accentColor: "Accent Color",
|
||
editPage: "Edit Page",
|
||
pwa: "Add to Home Screen for quick access!",
|
||
history: "History",
|
||
startTime: "Start",
|
||
endTime: "End",
|
||
mapNormal: "Normal",
|
||
mapSatellite: "Satellite",
|
||
mapTraffic: "Traffic",
|
||
mapStreet: "Street",
|
||
mapFit: "Fit",
|
||
mapPlay: "Play",
|
||
mapPause: "Pause",
|
||
historySummary: "History Summary",
|
||
historyDistance: "Distance",
|
||
historyDuration: "Duration",
|
||
historyPoints: "Points",
|
||
historyAverageSpeed: "Avg Speed",
|
||
historyMaxGap: "Max Gap",
|
||
historyStart: "Start",
|
||
historyEnd: "End",
|
||
historyPoint: "History Point",
|
||
},
|
||
},
|
||
mapState = {
|
||
map: null,
|
||
marker: null,
|
||
circle: null,
|
||
trackLine: null,
|
||
smallMarkers: [],
|
||
historyStartMarker: null,
|
||
historyEndMarker: null,
|
||
playbackMarker: null,
|
||
playbackTimer: null,
|
||
historyPoints: [],
|
||
historyStats: null,
|
||
layers: {},
|
||
layerActive: {},
|
||
activeMapType: 'normal',
|
||
openInfoWindow: null,
|
||
mapClickHandlerAdded: false,
|
||
initialInfoOpened: false,
|
||
historyBoundsFitted: false,
|
||
};
|
||
let langCode = localStorage.getItem("currentLang") || deviceLang,
|
||
lastAutoSwitchChecked,
|
||
lastUpdateSwitchChecked,
|
||
lastHash,
|
||
lastLat,
|
||
lastLng,
|
||
lastUpdateISOTime,
|
||
map,
|
||
marker,
|
||
circle,
|
||
radiusInterval,
|
||
_overrideRange,
|
||
wakeLock = null;
|
||
|
||
applyLanguage(langCode);
|
||
|
||
document.querySelectorAll('.locateRow').forEach(row => {
|
||
row.addEventListener('pointerdown', () => {
|
||
row.style.background = '#d3d3d3';
|
||
});
|
||
row.addEventListener('pointerup', () => {
|
||
row.style.background = '#fff';
|
||
});
|
||
row.addEventListener('pointerleave', () => {
|
||
row.style.background = '#fff';
|
||
});
|
||
});
|
||
|
||
document.getElementById('langToggle').addEventListener('click', () => {
|
||
langCode = langCode === 'ko' ? 'en' : 'ko';
|
||
localStorage.setItem('currentLang', langCode);
|
||
applyLanguage(langCode);
|
||
|
||
fetchDeviceData(true).then(data => updateDOMWithData(data));
|
||
|
||
// locateBtn 즉시 재렌더링
|
||
if (document.getElementById('locateBtn')) {
|
||
document.getElementById('labelMoveToCurrent').textContent =
|
||
langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location';
|
||
}
|
||
|
||
if (document.getElementById('autoUpdateBtn')) {
|
||
document.getElementById('labelAutoUpdate').textContent =
|
||
langCode === 'ko' ? '자동 업데이트' : 'Auto Update';
|
||
}
|
||
|
||
if (document.getElementById('HistoryResetBtn')) {
|
||
document.getElementById('labelHistoryReset').textContent =
|
||
langCode === 'ko' ? '초기화' : 'Reset';
|
||
}
|
||
|
||
if (document.getElementById('updateBtn')) {
|
||
document.getElementById('labelForceUpdate').textContent =
|
||
langCode === 'ko' ? '업데이트 하기' : 'Force Update';
|
||
}
|
||
});
|
||
|
||
document.getElementById('locateBtn').addEventListener('click', () => {
|
||
if (_overrideRange) return;
|
||
if (hasValidCoords(lastLat, lastLng)) {
|
||
const latlng = new naver.maps.LatLng(lastLat, lastLng);
|
||
mapState.map.panTo(latlng);
|
||
}
|
||
});
|
||
|
||
// [REPLACE] 기존 초기 updateBtn 리스너
|
||
document.getElementById('updateBtn').addEventListener('click', triggerForceUpdate);
|
||
|
||
document.getElementById('autoPositionSwitch').addEventListener('change', () => {
|
||
syncLocateBtn();
|
||
lastAutoSwitchChecked = document.getElementById('autoPositionSwitch').checked;
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||
|
||
if (!isMobile) {
|
||
const el = document.getElementById('mobileonly');
|
||
if (el) el.remove();
|
||
}
|
||
|
||
const container = document.getElementById('mySettingContainer');
|
||
container.style.userSelect = 'none';
|
||
container.style.webkitUserSelect = 'none';
|
||
container.style.msUserSelect = 'none';
|
||
container.style.MozUserSelect = 'none';
|
||
container.style.webkitTouchCallout = 'none';
|
||
container.addEventListener('dragstart', e => e.preventDefault());
|
||
|
||
document.getElementById('blogBackLink').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
location.href = '/blog';
|
||
});
|
||
|
||
// PWA 상태인지 확인 (iOS 및 기타 브라우저 지원)
|
||
function isInStandaloneMode() {
|
||
return (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone === true);
|
||
}
|
||
|
||
if (location.pathname.startsWith('/findmydevice') && !isInStandaloneMode()) {
|
||
fetch('/custom/launcher/api.php?session=1', { credentials: 'same-origin' })
|
||
.then(res => res.ok ? res.json() : Promise.reject())
|
||
.then(session => {
|
||
if (!session.authenticated) {
|
||
location.replace('/blog');
|
||
}
|
||
})
|
||
.catch(() => location.replace('/blog'));
|
||
}
|
||
|
||
const btnArea = document.querySelector('body > div.btnArea');
|
||
const pageModifyLink = btnArea?.querySelectorAll('a')[1]?.getAttribute('href') || '';
|
||
if (btnArea) btnArea.remove();
|
||
|
||
// 페이지 수정 버튼 요소 생성
|
||
const editRow = document.createElement('div');
|
||
editRow.className = 'settingRow locateRow';
|
||
editRow.style.cursor = 'pointer';
|
||
editRow.innerHTML = `<span id="labelEditPage" style="color:#007AFF">${langCode === 'ko' ? '페이지 수정' : 'Edit Page'}</span>`;
|
||
editRow.addEventListener('click', () => {
|
||
window.location.href = pageModifyLink;
|
||
});
|
||
|
||
const settingTitle = [...document.querySelectorAll('.settingTitle')].find(el => el.textContent.trim() === (langCode === 'ko' ? '설정' : 'Settings'));
|
||
let settingSection = settingTitle?.nextElementSibling;
|
||
|
||
// 삽입 기준: #mapContainer 다음
|
||
const mapContainer = document.getElementById('mapContainer');
|
||
const toggleContent = document.getElementById('toggleContent');
|
||
|
||
if (pageModifyLink) {
|
||
// 설정 섹션이 없으면 새로 생성 (mapContainer 아래)
|
||
if (!settingSection || !settingSection.classList.contains('settingSection')) {
|
||
const newTitle = document.createElement('div');
|
||
newTitle.className = 'settingTitle';
|
||
newTitle.innerHTML = `<span id="titleSettings">${langCode === 'ko' ? '설정' : 'Settings'}</span>`;
|
||
|
||
const newSection = document.createElement('div');
|
||
newSection.className = 'settingSection';
|
||
newSection.appendChild(editRow);
|
||
|
||
if (mapContainer) {
|
||
toggleContent.insertBefore(newTitle, mapContainer.nextSibling);
|
||
toggleContent.insertBefore(newSection, newTitle.nextSibling);
|
||
}
|
||
} else {
|
||
// 설정 섹션이 이미 존재하는 경우
|
||
const keepRow = settingSection.querySelector('#labelKeepScreenOn')?.closest('.settingRow');
|
||
|
||
if (keepRow) {
|
||
if (!isMobile) keepRow.remove(), settingSection.prepend(editRow);
|
||
else keepRow.insertAdjacentElement('afterend', editRow);
|
||
} else {
|
||
settingSection.appendChild(editRow);
|
||
}
|
||
}
|
||
} else {
|
||
// pageModifyLink 없음 → 셀렉터 존재 안 함
|
||
if (!isMobile && settingSection && settingTitle) {
|
||
settingTitle.remove();
|
||
settingSection.remove();
|
||
}
|
||
}
|
||
|
||
const tooltip = document.getElementById("ios-pwa-tooltip");
|
||
const closeBtn = document.getElementById("close-tooltip");
|
||
const isStandalone = window.navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
|
||
const tooltipClosed = getCookie('iosPwaTooltipClosed'); // 없으면 쿠키 만료 or 미설정
|
||
|
||
if (tooltip && closeBtn && isIosSafari() && !isStandalone && !tooltipClosed) {
|
||
function handleScroll() {
|
||
const scrollPercent = ((window.scrollY || document.documentElement.scrollTop) /
|
||
(document.documentElement.scrollHeight - document.documentElement.clientHeight)) * 100;
|
||
if (scrollPercent > 10) {
|
||
tooltip.classList.add("visible");
|
||
} else {
|
||
tooltip.classList.remove("visible");
|
||
}
|
||
}
|
||
|
||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||
|
||
closeBtn.addEventListener("click", function () {
|
||
tooltip.classList.remove("visible");
|
||
setCookieHours("iosPwaTooltipClosed", "1", 12); // 12시간 동안 숨김
|
||
window.removeEventListener("scroll", handleScroll);
|
||
});
|
||
}
|
||
|
||
const btn = document.getElementById('HistoryResetBtn');
|
||
if (btn) {
|
||
// 라벨
|
||
const label = document.getElementById('labelHistoryReset');
|
||
(label || btn).textContent = (langCode === 'ko') ? '초기화' : 'Reset';
|
||
|
||
btn.addEventListener('click', () => {
|
||
// 1) 히스토리 모드 해제 → 라이브 복귀
|
||
_overrideRange = null;
|
||
|
||
// 2) 입력 요소 가져오기 (같은 스코프에서!)
|
||
const startEl = document.getElementById('rangeStart');
|
||
const endEl = document.getElementById('rangeEnd');
|
||
if (startEl && endEl) {
|
||
const pad = n => String(n).padStart(2, '0');
|
||
const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`;
|
||
|
||
// 기준 시각(현재)
|
||
const current = new Date();
|
||
|
||
// 시작: 24시간 전 → 정각 버림
|
||
const start24 = new Date(current.getTime() - 24 * 3600 * 1000);
|
||
start24.setMinutes(0, 0, 0);
|
||
|
||
// 종료: 현재 시각의 다음 정각 올림
|
||
const endRoundUp = new Date(current);
|
||
endRoundUp.setMinutes(0, 0, 0);
|
||
if (current.getMinutes() > 0 || current.getSeconds() > 0 || current.getMilliseconds() > 0) {
|
||
endRoundUp.setHours(endRoundUp.getHours() + 1);
|
||
}
|
||
|
||
// 입력값 세팅
|
||
startEl.value = toLocalHour(start24);
|
||
endEl.value = toLocalHour(endRoundUp);
|
||
}
|
||
|
||
// 3) 지도 히스토리 초기화 & 라이브 데이터 재요청 + 현재 위치로 이동
|
||
resetHistoryLayer();
|
||
fetchDeviceData(true).then(data => {
|
||
updateDOMWithData(data);
|
||
if (data.latitude && data.longitude && mapState.map) {
|
||
mapState.map.panTo(new naver.maps.LatLng(data.latitude, data.longitude));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
(function initInstantHistorySearch() {
|
||
const startEl = document.getElementById('rangeStart');
|
||
const endEl = document.getElementById('rangeEnd');
|
||
if (!startEl || !endEl) return;
|
||
|
||
const pad = n => String(n).padStart(2, '0');
|
||
const toLocalHour = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`;
|
||
const toServerTime = v =>
|
||
v ? (v.includes('T') ? v.replace('T', ' ') : v) + (v.length === 16 ? ':00' : '') : null;
|
||
|
||
const now = new Date();
|
||
|
||
// 시작: 현재 시각에서 24시간 전, 정각 버림
|
||
const start24 = new Date(now.getTime() - 24 * 3600 * 1000);
|
||
start24.setMinutes(0, 0, 0);
|
||
|
||
// 종료: 현재 시각, 다음 정각 올림
|
||
const endRoundUp = new Date(now);
|
||
endRoundUp.setMinutes(0, 0, 0);
|
||
if (now.getMinutes() > 0 || now.getSeconds() > 0 || now.getMilliseconds() > 0) {
|
||
endRoundUp.setHours(endRoundUp.getHours() + 1);
|
||
}
|
||
|
||
const qs = new URLSearchParams(window.location.search);
|
||
startEl.value = qs.get('startTime')
|
||
? qs.get('startTime').replace(' ', 'T').slice(0, 16)
|
||
: toLocalHour(start24);
|
||
endEl.value = qs.get('endTime')
|
||
? qs.get('endTime').replace(' ', 'T').slice(0, 16)
|
||
: toLocalHour(endRoundUp);
|
||
|
||
const hasQS = qs.has('startTime') || qs.has('endTime');
|
||
|
||
const fire = debounce(async (userTriggered = false) => {
|
||
const start = startEl.value;
|
||
const end = endEl.value;
|
||
if (!start || !end) return;
|
||
|
||
if (start > end) endEl.value = start;
|
||
|
||
const range = {
|
||
startTime: toServerTime(startEl.value),
|
||
endTime: toServerTime(endEl.value)
|
||
};
|
||
|
||
const shouldUseHistory = userTriggered || hasQS;
|
||
const shouldRequestHistory = shouldUseHistory || !userTriggered;
|
||
if (shouldUseHistory) {
|
||
_overrideRange = range;
|
||
resetHistoryLayer();
|
||
} else {
|
||
_overrideRange = null; // 라이브 모드 유지
|
||
}
|
||
|
||
try {
|
||
const data = await fetchDeviceData(true, shouldRequestHistory ? range : null);
|
||
updateDOMWithData(data);
|
||
if (!shouldUseHistory && data.history?.length > 0) {
|
||
updateHistoryMarkers(data.history);
|
||
}
|
||
|
||
const last = data.history && data.history[data.history.length - 1];
|
||
if (shouldUseHistory && last && mapState.map) {
|
||
mapState.map.panTo(new naver.maps.LatLng(last.lat, last.lng));
|
||
}
|
||
} catch (err) {
|
||
alert(langCode === 'ko' ? '기록을 불러오지 못했습니다.' : 'Failed to load history.');
|
||
}
|
||
}, 400);
|
||
|
||
['change', 'input'].forEach(ev => {
|
||
startEl.addEventListener(ev, () => fire(true));
|
||
endEl.addEventListener(ev, () => fire(true));
|
||
});
|
||
|
||
fire(false);
|
||
})();
|
||
});
|
||
|
||
document.getElementById('wakeLockSwitch').addEventListener('change', async function () {
|
||
if (this.checked) {
|
||
try {
|
||
wakeLock = await navigator.wakeLock.request('screen');
|
||
|
||
wakeLock.addEventListener('release', () => {
|
||
wakeLock = null;
|
||
document.getElementById('wakeLockSwitch').checked = false;
|
||
});
|
||
|
||
} catch (err) {
|
||
this.checked = false;
|
||
}
|
||
} else {
|
||
if (wakeLock) {
|
||
wakeLock.release();
|
||
wakeLock = null;
|
||
}
|
||
}
|
||
});
|
||
|
||
document.getElementById('autoUpdateSwitch').addEventListener('change', () => {
|
||
syncUpdateBtn();
|
||
const isChecked = document.getElementById('autoUpdateSwitch')?.checked;
|
||
|
||
if (isChecked) {
|
||
callDeviceWebhook('auto_on');
|
||
} else {
|
||
callDeviceWebhook('auto_off');
|
||
}
|
||
});
|
||
|
||
['pagehide', 'visibilitychange', 'blur', 'beforeunload'].forEach(eventType => {
|
||
window.addEventListener(eventType, function (e) {
|
||
|
||
const switchEl = document.getElementById('autoUpdateSwitch');
|
||
const isAutoUpdateOn = switchEl?.checked;
|
||
|
||
if (isAutoUpdateOn) {
|
||
callDeviceWebhook('auto_off', true);
|
||
|
||
// 스위치 UI 강제 끔
|
||
switchEl.checked = false;
|
||
syncUpdateBtn();
|
||
}
|
||
});
|
||
});
|
||
|
||
setInterval(() => {
|
||
if (document.getElementById('autoUpdateSwitch')?.checked) {
|
||
callDeviceWebhook('auto_tick');
|
||
}
|
||
}, 10000);
|
||
|
||
setInterval(() => {
|
||
if (lastUpdateISOTime) {
|
||
const el = document.getElementById("lastUpdateTime");
|
||
if (el) el.textContent = getRelativeTime(lastUpdateISOTime);
|
||
}
|
||
|
||
// 히스토리 범위가 활성화된 경우: hash 체크 생략하고 같은 범위로 재요청(낭비 방지로 5초 권장)
|
||
if (_overrideRange) return; // 간단히 폴링 정지 (원하면 5초 주기로 같은 범위 재요청하도록 변경 가능)
|
||
|
||
fetchDeviceData(false).then(({ hash }) => {
|
||
if (!hash || hash === lastHash) return;
|
||
lastHash = hash;
|
||
fetchDeviceData(true).then(data => updateDOMWithData(data));
|
||
});
|
||
}, 1000);
|
||
|
||
function formatNumber(num) {
|
||
if (num == null || isNaN(num)) return '-';
|
||
return new Intl.NumberFormat().format(num);
|
||
}
|
||
|
||
function getRelativeTime(isoTime) {
|
||
const d = new Date(isoTime), n = new Date();
|
||
const diffDays = Math.floor(
|
||
(new Date(n.getFullYear(), n.getMonth(), n.getDate()) -
|
||
new Date(d.getFullYear(), d.getMonth(), d.getDate())) / 86400000
|
||
);
|
||
|
||
const isKo = (typeof langCode !== 'undefined' && langCode === 'ko');
|
||
const pad = (x) => x.toString().padStart(2, '0');
|
||
const h24 = d.getHours();
|
||
const p = isKo ? (h24 < 12 ? '오전' : '오후') : (h24 < 12 ? 'AM' : 'PM');
|
||
const h12 = pad(h24 % 12 === 0 ? 12 : h24 % 12);
|
||
const m = pad(d.getMinutes()), s = pad(d.getSeconds());
|
||
|
||
if (isKo) {
|
||
return diffDays === 0
|
||
? `${p} ${h12}:${m}:${s}`
|
||
: `${diffDays}일 전 ${p} ${h12}:${m}:${s}`;
|
||
}
|
||
return diffDays === 0
|
||
? `${p} ${h12}:${m}:${s}`
|
||
: `${diffDays} day${diffDays > 1 ? 's' : ''} ago ${p} ${h12}:${m}:${s}`;
|
||
}
|
||
|
||
function applyLanguage(langCode) {
|
||
document.title = langCode === 'ko' ? '나의 찾기' : 'Find My Device';
|
||
|
||
const t = translations[langCode] || translations.ko,
|
||
titleMap = { titleSettings: t.settings, titleLocation: t.location, titleUpdate: t.update, titleBattery: t.battery, titleActivity: t.activity, titleEtc: t.etc, titleHistory: t.history },
|
||
labelMap = {
|
||
labelKeepScreenOn: t.keepScreenOn,
|
||
labelCurrentPosition: t.currentPosition,
|
||
labelGpsAccuracy: t.gpsAccuracy,
|
||
labelMoveToCurrent: t.moveToCurrent,
|
||
labelLastUpdate: t.lastUpdate,
|
||
labelUpdateTrigger: t.updateTrigger,
|
||
labelCurrentAutoPosition: t.autoCurrentUpdate,
|
||
labelForceUpdate: t.forceUpdate,
|
||
labelBatteryState: t.batteryState,
|
||
labelBatteryLevel: t.batteryLevel,
|
||
labelWatchBatteryState: t.watchbatteryState,
|
||
labelWatchBatteryLevel: t.watchbatteryLevel,
|
||
labelCurrentActivity: t.currentActivity,
|
||
labelActivityAccuracy: t.activityAccuracy,
|
||
labelDistance: t.distance,
|
||
labelSteps: t.steps,
|
||
labelAscended: t.ascended,
|
||
labelDescended: t.descended,
|
||
labelConnection: t.connection,
|
||
labelEditPage: t.editPage,
|
||
labelAutoUpdate: t.autoUpdate,
|
||
labelStartTime: t.startTime,
|
||
labelEndTime: t.endTime,
|
||
labelPwa: t.pwa,
|
||
};
|
||
|
||
document.getElementById('blogBackLink').innerHTML = `<span class="backArrow">‹</span> ${t.blog}`;
|
||
document.querySelector('.headerTitle').textContent = t.title;
|
||
|
||
for (const id in titleMap) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = titleMap[id];
|
||
}
|
||
|
||
for (const id in labelMap) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = labelMap[id];
|
||
}
|
||
|
||
document.querySelectorAll('[data-sensor-title="1"]').forEach(title => {
|
||
const sectionKey = title.dataset.sensorSection;
|
||
const label = title.querySelector('span');
|
||
if (label) label.textContent = t[sectionKey] || sectionKey;
|
||
});
|
||
|
||
document.querySelectorAll('[data-sensor-row="1"]').forEach(row => {
|
||
const key = row.dataset.sensorKey;
|
||
const label = row.querySelector('span');
|
||
if (label && key) label.textContent = t[key] || key;
|
||
});
|
||
|
||
updateMapToolLabels();
|
||
renderHistorySummary();
|
||
}
|
||
|
||
function setCookieHours(name, value, hours) {
|
||
const d = new Date();
|
||
d.setTime(d.getTime() + hours * 60 * 60 * 1000);
|
||
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; expires=${d.toUTCString()}; path=/; SameSite=Lax`;
|
||
}
|
||
|
||
function updateDOM(data) {
|
||
const textMap = {
|
||
currentPosition: data.address,
|
||
lastUpdateTime: data.updated ? getRelativeTime(data.updated) : "-",
|
||
nowActivity: data.activity,
|
||
connectionType: data.connection,
|
||
updateTrigger: data.trigger,
|
||
batteryStatus: data.state,
|
||
Battery: data.level > 0 ? `${data.level}%` : "-",
|
||
WbatteryStatus: data.wstate,
|
||
WBattery: data.wlevel > 0 ? `${data.wlevel}%` : "-",
|
||
todaySteps: data.steps > 0 ? `${formatNumber(data.steps)}${langCode === "ko" ? "걸음" : "steps"}` : "-",
|
||
todayAscended: data.ascended > 0 ? `${formatNumber(data.ascended)}${langCode === "ko" ? "층" : "floors"}` : "-",
|
||
todayDescended: data.descended > 0 ? `${formatNumber(data.descended)}${langCode === "ko" ? "층" : "floors"}` : "-",
|
||
todayDistance: data.distance > 0 ? `${formatNumber(data.distance)}m` : "-",
|
||
accuracyValue: data.accuracy > 0 ? `${formatNumber(data.accuracy)}m` : "-"
|
||
};
|
||
|
||
for (const [id, value] of Object.entries(textMap)) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = value || "-";
|
||
}
|
||
|
||
toggleOptionalRow('WbatteryStatus', data.wstate);
|
||
toggleOptionalRow('WBattery', data.wlevel);
|
||
updateSensorSections(data.sensors || {});
|
||
}
|
||
|
||
function updateSensorSections(sections) {
|
||
const t = translations[langCode] || translations.ko;
|
||
const activeSectionIds = new Set();
|
||
const activeRowIds = new Set();
|
||
|
||
Object.entries(sections || {}).forEach(([sectionKey, rows]) => {
|
||
const entries = Object.entries(rows || {}).filter(([, value]) => normalizeDisplayValue(value));
|
||
if (!entries.length) return;
|
||
|
||
activeSectionIds.add(sectionKey);
|
||
const section = ensureSensorSection(sectionKey);
|
||
if (!section) return;
|
||
|
||
const title = document.getElementById(`sensorTitleText_${sectionKey}`);
|
||
if (title) title.textContent = t[sectionKey] || sectionKey;
|
||
|
||
entries.forEach(([key, value]) => {
|
||
activeRowIds.add(`sensor_${sectionKey}_${key}`);
|
||
upsertOptionalRow(section, `sensor_${sectionKey}_${key}`, t[key] || key, value);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-sensor-row="1"]').forEach(row => {
|
||
if (!activeRowIds.has(row.id)) row.remove();
|
||
});
|
||
|
||
document.querySelectorAll('[data-sensor-title="1"]').forEach(title => {
|
||
const sectionKey = title.dataset.sensorSection;
|
||
const section = document.getElementById(`sensorSection_${sectionKey}`);
|
||
if (!activeSectionIds.has(sectionKey) || !section?.querySelector('[data-sensor-row="1"]')) {
|
||
section?.remove();
|
||
title.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
function ensureSensorSection(sectionKey) {
|
||
let section = document.getElementById(`sensorSection_${sectionKey}`);
|
||
if (section) return section;
|
||
|
||
const t = translations[langCode] || translations.ko;
|
||
const toggleContent = document.getElementById('toggleContent') || document.body;
|
||
|
||
const title = document.createElement('div');
|
||
title.className = 'settingTitle';
|
||
title.id = `sensorTitle_${sectionKey}`;
|
||
title.dataset.sensorTitle = '1';
|
||
title.dataset.sensorSection = sectionKey;
|
||
title.innerHTML = `<span id="sensorTitleText_${sectionKey}">${t[sectionKey] || sectionKey}</span>`;
|
||
|
||
section = document.createElement('div');
|
||
section.className = 'settingSection';
|
||
section.id = `sensorSection_${sectionKey}`;
|
||
section.dataset.sensorSection = sectionKey;
|
||
|
||
toggleContent.appendChild(title);
|
||
toggleContent.appendChild(section);
|
||
return section;
|
||
}
|
||
|
||
function upsertOptionalRow(section, id, label, value) {
|
||
if (!section) return;
|
||
const displayValue = normalizeDisplayValue(value);
|
||
let row = document.getElementById(id);
|
||
|
||
if (!displayValue) {
|
||
if (row) row.remove();
|
||
return;
|
||
}
|
||
|
||
if (!row) {
|
||
row = document.createElement('div');
|
||
row.className = 'settingRow';
|
||
row.id = id;
|
||
row.dataset.sensorRow = id.startsWith('sensor_') ? '1' : '0';
|
||
row.innerHTML = `<span></span><span></span>`;
|
||
section.appendChild(row);
|
||
}
|
||
|
||
if (id.startsWith('sensor_')) {
|
||
row.dataset.sensorKey = id.split('_').slice(2).join('_');
|
||
}
|
||
row.children[0].textContent = label;
|
||
row.children[1].textContent = displayValue;
|
||
}
|
||
|
||
function toggleOptionalRow(valueId, value) {
|
||
const el = document.getElementById(valueId);
|
||
const row = el?.closest('.settingRow');
|
||
if (!row) return;
|
||
row.style.display = normalizeDisplayValue(value) ? '' : 'none';
|
||
}
|
||
|
||
function normalizeDisplayValue(value) {
|
||
if (value === null || typeof value === 'undefined' || value === '' || value === '-' || value === 'unknown' || value === 'unavailable') {
|
||
return '';
|
||
}
|
||
return String(value);
|
||
}
|
||
|
||
function updateActivitySection(data) {
|
||
const validActivities = ["Stationary", "Walking", "Running", "In Vehicle", "Cycling", "가만히 있음", "걷는 중", "달리는 중", "차량 이동 중", "자전거 이동 중"];
|
||
const validAccuracies = ["높음", "중간", "낮음", "High", "Middle", "Low"];
|
||
|
||
let existActivityEl = document.getElementById('existnowActivity');
|
||
if (existActivityEl) {
|
||
if (validActivities.includes(data.activity || '') && validAccuracies.includes(data.aaccuracy || '')) {
|
||
existActivityEl.style.display = 'flex';
|
||
document.getElementById('nowactivityAccuracy').textContent = data.aaccuracy || '-';
|
||
} else {
|
||
existActivityEl.remove();
|
||
}
|
||
}
|
||
|
||
if (!document.getElementById('existnowActivity') &&
|
||
validActivities.includes(data.activity || '') &&
|
||
validAccuracies.includes(data.aaccuracy || '')) {
|
||
const titles = document.querySelectorAll('.settingTitle');
|
||
let activitySection = null;
|
||
|
||
for (let title of titles) {
|
||
if (title.textContent.trim() === (langCode === 'ko' ? '활동' : 'Activity')) {
|
||
const next = title.nextElementSibling;
|
||
if (next && next.classList.contains('settingSection')) {
|
||
activitySection = next;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (activitySection) {
|
||
const rows = activitySection.querySelectorAll('.settingRow');
|
||
let targetRow = null;
|
||
|
||
for (let row of rows) {
|
||
const label = row.querySelector('span');
|
||
if (label && label.textContent.trim() === (langCode === 'ko' ? '현재 활동' : 'Current Activity')) {
|
||
targetRow = row;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (targetRow) {
|
||
const newRow = document.createElement('div');
|
||
newRow.className = 'settingRow';
|
||
newRow.id = 'existnowActivity';
|
||
newRow.innerHTML = `
|
||
<span>${langCode === 'ko' ? '활동 정확도' : 'Activity Accuracy'}</span>
|
||
<span id="nowactivityAccuracy">${data.aaccuracy || '-'}</span>
|
||
`;
|
||
activitySection.insertBefore(newRow, targetRow.nextSibling);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function initMap(latlng, accuracy = 50) {
|
||
mapState.map = new naver.maps.Map('map', {
|
||
center: latlng,
|
||
zoom: 12,
|
||
zoomControl: true,
|
||
zoomControlOptions: {
|
||
position: naver.maps.Position.RIGHT_CENTER
|
||
},
|
||
scaleControl: true,
|
||
mapDataControl: false
|
||
});
|
||
|
||
createMainMarker(latlng);
|
||
createAccuracyCircle(latlng, accuracy);
|
||
initMapLayers();
|
||
ensureMapTools();
|
||
}
|
||
|
||
function initMapLayers() {
|
||
if (!mapState.map || mapState.layers.traffic) return;
|
||
mapState.layers.traffic = new naver.maps.TrafficLayer();
|
||
mapState.layers.street = new naver.maps.StreetLayer();
|
||
}
|
||
|
||
function ensureMapTools() {
|
||
const container = document.getElementById('mapContainer') || document.getElementById('map')?.parentElement;
|
||
if (!container || document.getElementById('mapTools')) return;
|
||
container.classList.add('has-map-tools');
|
||
|
||
const tools = document.createElement('div');
|
||
tools.id = 'mapTools';
|
||
tools.className = 'map-tools';
|
||
tools.innerHTML = `
|
||
<button type="button" class="map-tool active" data-map-action="normal"></button>
|
||
<button type="button" class="map-tool" data-map-action="satellite"></button>
|
||
<button type="button" class="map-tool" data-map-action="traffic"></button>
|
||
<button type="button" class="map-tool" data-map-action="street"></button>
|
||
<button type="button" class="map-tool" data-map-action="fit"></button>
|
||
<button type="button" class="map-tool" data-map-action="play"></button>
|
||
`;
|
||
container.appendChild(tools);
|
||
|
||
tools.addEventListener('click', event => {
|
||
const button = event.target.closest('[data-map-action]');
|
||
if (!button) return;
|
||
handleMapTool(button.dataset.mapAction);
|
||
});
|
||
|
||
const summary = document.createElement('div');
|
||
summary.id = 'historySummaryPanel';
|
||
summary.className = 'history-summary-panel';
|
||
summary.hidden = true;
|
||
container.appendChild(summary);
|
||
updateMapToolLabels();
|
||
}
|
||
|
||
function updateMapToolLabels() {
|
||
const t = translations[langCode] || translations.ko;
|
||
const labelMap = {
|
||
normal: t.mapNormal,
|
||
satellite: t.mapSatellite,
|
||
traffic: t.mapTraffic,
|
||
street: t.mapStreet,
|
||
fit: t.mapFit,
|
||
play: mapState.playbackTimer ? t.mapPause : t.mapPlay,
|
||
};
|
||
document.querySelectorAll('[data-map-action]').forEach(button => {
|
||
const action = button.dataset.mapAction;
|
||
button.textContent = labelMap[action] || action;
|
||
button.title = labelMap[action] || action;
|
||
});
|
||
}
|
||
|
||
function handleMapTool(action) {
|
||
if (!mapState.map) return;
|
||
if (action === 'normal' || action === 'satellite') {
|
||
mapState.activeMapType = action;
|
||
const mapTypes = naver.maps.MapTypeId || {};
|
||
const typeId = action === 'satellite'
|
||
? (mapTypes.HYBRID || mapTypes.SATELLITE || 'satellite')
|
||
: (mapTypes.NORMAL || 'normal');
|
||
mapState.map.setMapTypeId(typeId);
|
||
document.querySelectorAll('[data-map-action="normal"], [data-map-action="satellite"]').forEach(button => {
|
||
button.classList.toggle('active', button.dataset.mapAction === action);
|
||
});
|
||
return;
|
||
}
|
||
if (action === 'traffic' || action === 'street') {
|
||
const layer = mapState.layers[action];
|
||
if (!layer) return;
|
||
const nextActive = !mapState.layerActive[action];
|
||
layer.setMap(nextActive ? mapState.map : null);
|
||
mapState.layerActive[action] = nextActive;
|
||
document.querySelector(`[data-map-action="${action}"]`)?.classList.toggle('active', nextActive);
|
||
return;
|
||
}
|
||
if (action === 'fit') {
|
||
fitHistoryOrCurrentBounds();
|
||
return;
|
||
}
|
||
if (action === 'play') {
|
||
toggleHistoryPlayback();
|
||
}
|
||
}
|
||
|
||
function toTimestamp(value) {
|
||
if (value instanceof Date) return value.getTime();
|
||
if (typeof value === 'number' && isFinite(value)) return value < 2e10 ? value * 1000 : value;
|
||
const parsed = Date.parse(value);
|
||
return Number.isNaN(parsed) ? 0 : parsed;
|
||
}
|
||
|
||
function distanceMeters(a, b) {
|
||
const earth = 6371000;
|
||
const lat1 = Number(a.lat);
|
||
const lat2 = Number(b.lat);
|
||
const lng1 = Number(a.lng);
|
||
const lng2 = Number(b.lng);
|
||
if (![lat1, lat2, lng1, lng2].every(Number.isFinite)) return 0;
|
||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||
const dLng = (lng2 - lng1) * Math.PI / 180;
|
||
const rLat1 = lat1 * Math.PI / 180;
|
||
const rLat2 = lat2 * Math.PI / 180;
|
||
const h = Math.sin(dLat / 2) ** 2 + Math.cos(rLat1) * Math.cos(rLat2) * Math.sin(dLng / 2) ** 2;
|
||
return 2 * earth * Math.asin(Math.min(1, Math.sqrt(h)));
|
||
}
|
||
|
||
function formatDistance(meters) {
|
||
if (!Number.isFinite(meters) || meters <= 0) return '-';
|
||
if (meters >= 1000) return `${(meters / 1000).toFixed(meters >= 10000 ? 1 : 2)}km`;
|
||
return `${Math.round(meters)}m`;
|
||
}
|
||
|
||
function formatDuration(seconds) {
|
||
if (!Number.isFinite(seconds) || seconds <= 0) return '-';
|
||
const days = Math.floor(seconds / 86400);
|
||
seconds %= 86400;
|
||
const hours = Math.floor(seconds / 3600);
|
||
seconds %= 3600;
|
||
const minutes = Math.floor(seconds / 60);
|
||
const parts = [];
|
||
if (langCode === 'ko') {
|
||
if (days) parts.push(`${days}일`);
|
||
if (hours) parts.push(`${hours}시간`);
|
||
if (minutes) parts.push(`${minutes}분`);
|
||
} else {
|
||
if (days) parts.push(`${days}d`);
|
||
if (hours) parts.push(`${hours}h`);
|
||
if (minutes) parts.push(`${minutes}m`);
|
||
}
|
||
return parts.join(' ') || (langCode === 'ko' ? '1분 미만' : '<1m');
|
||
}
|
||
|
||
function formatSpeed(kmh) {
|
||
if (!Number.isFinite(kmh) || kmh <= 0) return '-';
|
||
return `${kmh.toFixed(kmh >= 10 ? 1 : 2)}km/h`;
|
||
}
|
||
|
||
function buildHistoryStats(points) {
|
||
if (!Array.isArray(points) || points.length < 2) return null;
|
||
let distance = 0;
|
||
let maxGap = 0;
|
||
for (let i = 1; i < points.length; i++) {
|
||
distance += distanceMeters(points[i - 1], points[i]);
|
||
const gap = Math.max(0, (toTimestamp(points[i].time) - toTimestamp(points[i - 1].time)) / 1000);
|
||
maxGap = Math.max(maxGap, gap);
|
||
}
|
||
const startedAt = toTimestamp(points[0].time);
|
||
const endedAt = toTimestamp(points.at(-1).time);
|
||
const duration = Math.max(0, (endedAt - startedAt) / 1000);
|
||
return {
|
||
distance,
|
||
duration,
|
||
maxGap,
|
||
points: points.length,
|
||
averageSpeed: duration > 0 ? distance / duration * 3.6 : 0,
|
||
startedAt: points[0].time,
|
||
endedAt: points.at(-1).time,
|
||
};
|
||
}
|
||
|
||
function renderHistorySummary() {
|
||
const panel = document.getElementById('historySummaryPanel');
|
||
if (!panel) return;
|
||
const stats = mapState.historyStats;
|
||
if (!stats) {
|
||
panel.hidden = true;
|
||
panel.innerHTML = '';
|
||
return;
|
||
}
|
||
const t = translations[langCode] || translations.ko;
|
||
panel.hidden = false;
|
||
panel.innerHTML = `
|
||
<div class="history-summary-title">${t.historySummary}</div>
|
||
<div class="history-summary-grid">
|
||
<span>${t.historyDistance}</span><strong>${formatDistance(stats.distance)}</strong>
|
||
<span>${t.historyDuration}</span><strong>${formatDuration(Math.round(stats.duration))}</strong>
|
||
<span>${t.historyAverageSpeed}</span><strong>${formatSpeed(stats.averageSpeed)}</strong>
|
||
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
|
||
<span>${t.historyPoints}</span><strong>${formatNumber(stats.points)}</strong>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function buildHistoryInfoContent(point, type, label) {
|
||
const t = translations[langCode] || translations.ko;
|
||
const title = label || t.historyPoint;
|
||
const index = mapState.historyPoints.findIndex(item => item.time === point.time && item.lat === point.lat && item.lng === point.lng);
|
||
let fromStart = 0;
|
||
for (let i = 1; i <= index; i++) {
|
||
fromStart += distanceMeters(mapState.historyPoints[i - 1], mapState.historyPoints[i]);
|
||
}
|
||
const typeClass = type === 'start' ? 'start' : (type === 'end' ? 'end' : 'point');
|
||
return `
|
||
<div class="history-info-window history-info-${typeClass}">
|
||
<strong>${title}</strong>
|
||
<span>${getRelativeTime(point.time)}</span>
|
||
<span>${t.historyDistance}: ${index > 0 ? formatDistance(fromStart) : '-'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function fitHistoryOrCurrentBounds() {
|
||
if (!mapState.map) return;
|
||
if (mapState.historyPoints?.length) {
|
||
const coords = mapState.historyPoints.map(point => point.latlng);
|
||
if (coords.length === 1) {
|
||
mapState.map.panTo(coords[0]);
|
||
return;
|
||
}
|
||
mapState.map.fitBounds(coords, { top: 40, right: 28, bottom: 120, left: 28, maxZoom: 16 });
|
||
return;
|
||
}
|
||
if (hasValidCoords(lastLat, lastLng)) {
|
||
mapState.map.panTo(new naver.maps.LatLng(lastLat, lastLng));
|
||
}
|
||
}
|
||
|
||
function stopHistoryPlayback() {
|
||
if (mapState.playbackTimer) {
|
||
clearInterval(mapState.playbackTimer);
|
||
mapState.playbackTimer = null;
|
||
}
|
||
updateMapToolLabels();
|
||
}
|
||
|
||
function toggleHistoryPlayback() {
|
||
if (!mapState.map || !mapState.historyPoints?.length) return;
|
||
if (mapState.playbackTimer) {
|
||
stopHistoryPlayback();
|
||
return;
|
||
}
|
||
|
||
const points = mapState.historyPoints;
|
||
let index = 0;
|
||
if (!mapState.playbackMarker) {
|
||
mapState.playbackMarker = new naver.maps.Marker({
|
||
position: points[0].latlng,
|
||
map: mapState.map,
|
||
zIndex: 120,
|
||
icon: {
|
||
content: '<div class="history-playback-marker"></div>',
|
||
anchor: new naver.maps.Point(11, 11)
|
||
}
|
||
});
|
||
} else {
|
||
mapState.playbackMarker.setMap(mapState.map);
|
||
mapState.playbackMarker.setPosition(points[0].latlng);
|
||
}
|
||
|
||
mapState.playbackTimer = setInterval(() => {
|
||
const point = points[index];
|
||
mapState.playbackMarker.setPosition(point.latlng);
|
||
if (index % 8 === 0) mapState.map.panTo(point.latlng);
|
||
index += 1;
|
||
if (index >= points.length) stopHistoryPlayback();
|
||
}, Math.max(120, Math.min(450, 10000 / points.length)));
|
||
updateMapToolLabels();
|
||
}
|
||
|
||
function createMainMarker(latlng) {
|
||
mapState.marker = new naver.maps.Marker({
|
||
position: latlng,
|
||
map: mapState.map,
|
||
zIndex: 100,
|
||
icon: {
|
||
content: `<div style="width:60px; height:60px; background:url('https://chaegeon.com/custom/findmydevice/img/30c0fc9d9.png') no-repeat center/contain;"></div>`,
|
||
anchor: new naver.maps.Point(30, 30)
|
||
}
|
||
});
|
||
applyMarkerEffects(mapState.marker);
|
||
}
|
||
|
||
function createAccuracyCircle(latlng, accuracy = 50) {
|
||
mapState.circle = new naver.maps.Circle({
|
||
map: mapState.map,
|
||
center: latlng,
|
||
radius: Math.max(accuracy, 10),
|
||
strokeColor: '#007AFF',
|
||
strokeOpacity: 0.8,
|
||
strokeWeight: 2,
|
||
fillColor: '#007AFF',
|
||
fillOpacity: 0.2
|
||
});
|
||
}
|
||
|
||
function applyMarkerEffects(marker) {
|
||
naver.maps.Event.addListener(marker, 'mouseover', () => {
|
||
const el = marker.getElement();
|
||
if (el) el.style.transform = `scale(1.2)`;
|
||
});
|
||
naver.maps.Event.addListener(marker, 'mouseout', () => {
|
||
const el = marker.getElement();
|
||
if (el) el.style.transform = `scale(1)`;
|
||
});
|
||
naver.maps.Event.addListener(marker, 'click', () => bounceMarker(marker));
|
||
breathingMarker(marker);
|
||
}
|
||
|
||
function breathingMarker(marker) {
|
||
const el = marker.getElement();
|
||
let scale = 1, direction = 1;
|
||
setInterval(() => {
|
||
scale += 0.01 * direction;
|
||
if (scale > 1.1) direction = -1;
|
||
if (scale < 1.0) direction = 1;
|
||
if (el) {
|
||
el.style.transition = 'transform 0.1s';
|
||
el.style.transform = `scale(${scale})`;
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
function getCookie(name) {
|
||
const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)'));
|
||
return m ? decodeURIComponent(m[1]) : null;
|
||
}
|
||
|
||
function bounceMarker(marker) {
|
||
let scale = 1, up = true, count = 0;
|
||
const interval = setInterval(() => {
|
||
scale += up ? 0.1 : -0.1;
|
||
if (scale >= 1.3) up = false;
|
||
if (scale <= 1) {
|
||
clearInterval(interval);
|
||
scale = 1;
|
||
}
|
||
marker.setIcon({
|
||
url: 'https://chaegeon.com/custom/findmydevice/img/30c0fc9d9.png',
|
||
size: new naver.maps.Size(60 * scale, 60 * scale),
|
||
scaledSize: new naver.maps.Size(60 * scale, 60 * scale),
|
||
anchor: new naver.maps.Point(30 * scale, 30 * scale)
|
||
});
|
||
if (count++ > 10) clearInterval(interval);
|
||
}, 30);
|
||
}
|
||
|
||
function resetHistoryLayer() {
|
||
stopHistoryPlayback();
|
||
if (mapState.trackLine) {
|
||
mapState.trackLine.setMap(null);
|
||
mapState.trackLine = null;
|
||
}
|
||
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;
|
||
}
|
||
if (mapState.playbackMarker) {
|
||
mapState.playbackMarker.setMap(null);
|
||
}
|
||
mapState.historyPoints = [];
|
||
mapState.historyStats = null;
|
||
renderHistorySummary();
|
||
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;
|
||
mapState.historyPoints = points;
|
||
|
||
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,
|
||
strokeLineCap: 'round',
|
||
strokeLineJoin: 'round',
|
||
endIcon: naver.maps.PointingIcon?.BLOCK_ARROW,
|
||
endIconSize: 16,
|
||
clickable: true
|
||
});
|
||
mapState.historyStats = buildHistoryStats(points);
|
||
if (mapState.trackLine.getDistance && mapState.historyStats) {
|
||
const lineDistance = mapState.trackLine.getDistance();
|
||
if (Number.isFinite(lineDistance) && lineDistance > 0) {
|
||
mapState.historyStats.distance = lineDistance;
|
||
mapState.historyStats.averageSpeed = mapState.historyStats.duration > 0 ? lineDistance / mapState.historyStats.duration * 3.6 : 0;
|
||
}
|
||
}
|
||
renderHistorySummary();
|
||
naver.maps.Event.addListener(mapState.trackLine, 'click', () => {
|
||
const panel = document.getElementById('historySummaryPanel');
|
||
if (panel) panel.hidden = !panel.hidden;
|
||
});
|
||
|
||
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: buildHistoryInfoContent(pos, type, label)
|
||
});
|
||
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 = 180;
|
||
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) {
|
||
fitHistoryOrCurrentBounds();
|
||
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;
|
||
}
|
||
});
|
||
mapState.mapClickHandlerAdded = true;
|
||
}
|
||
}
|
||
|
||
function updateDOMWithData(data) {
|
||
// ⚑ 히스토리 모드: 값은 "-"로, 지도엔 히스토리만 반영
|
||
if (typeof _overrideRange !== 'undefined' && _overrideRange) {
|
||
resetDisplayValues();
|
||
|
||
// 히스토리 기준 중심점 선택
|
||
const last = data.history && data.history[data.history.length - 1];
|
||
const baseLatLng = last
|
||
? new naver.maps.LatLng(last.lat, last.lng)
|
||
: (data.latitude && data.longitude ? new naver.maps.LatLng(data.latitude, data.longitude) : null);
|
||
|
||
// 지도 초기화 (최초 1회)
|
||
if (!mapState.map && baseLatLng) {
|
||
initMap(baseLatLng, data.accuracy || 50);
|
||
}
|
||
|
||
// 히스토리 궤적/포인트 업데이트
|
||
if (data.history?.length > 0) {
|
||
updateHistoryMarkers(data.history);
|
||
}
|
||
|
||
// 상대시간 갱신 중단
|
||
lastUpdateISOTime = null;
|
||
|
||
// 히스토리 모드에서는 메인 마커/원, auto pan 업데이트 스킵
|
||
return;
|
||
}
|
||
|
||
// ── 라이브 모드 기존 로직 ──
|
||
const latlng = new naver.maps.LatLng(data.latitude, data.longitude);
|
||
|
||
// DOM 업데이트
|
||
updateDOM(data);
|
||
updateActivitySection(data);
|
||
|
||
// 지도 초기화
|
||
if (!mapState.map) {
|
||
initMap(latlng, data.accuracy);
|
||
}
|
||
|
||
// 메인 마커 / 정확도 원 업데이트
|
||
mapState.marker.setPosition(latlng);
|
||
mapState.circle.setCenter(latlng);
|
||
animateCircleRadius(Math.max(data.accuracy || 50, 10));
|
||
|
||
// autoPosition
|
||
const isChecked = document.getElementById('autoPositionSwitch').checked;
|
||
if (isChecked && (lastLat !== data.latitude || lastLng !== data.longitude)) {
|
||
mapState.map.panTo(latlng);
|
||
}
|
||
lastLat = data.latitude;
|
||
lastLng = data.longitude;
|
||
lastUpdateISOTime = data.updated;
|
||
}
|
||
|
||
function animateCircleRadius(targetRadius) {
|
||
if (!mapState.circle) return;
|
||
if (window.radiusInterval) clearInterval(window.radiusInterval);
|
||
|
||
let currentRadius = mapState.circle.getRadius();
|
||
const step = (targetRadius - currentRadius) / 100;
|
||
let count = 0;
|
||
|
||
window.radiusInterval = setInterval(() => {
|
||
currentRadius += step;
|
||
mapState.circle.setRadius(currentRadius);
|
||
if (count++ >= 100) {
|
||
mapState.circle.setRadius(targetRadius);
|
||
clearInterval(window.radiusInterval);
|
||
}
|
||
}, 30);
|
||
}
|
||
|
||
function syncLocateBtn() {
|
||
const isChecked = document.getElementById('autoPositionSwitch').checked;
|
||
const parent = document.getElementById('autoLocateBtn');
|
||
const exist = document.getElementById('locateBtn');
|
||
|
||
if (isChecked && exist) {
|
||
exist.remove();
|
||
} else if (!isChecked && !exist && parent) {
|
||
const newDiv = document.createElement('div');
|
||
newDiv.className = 'settingRow locateRow';
|
||
newDiv.id = 'locateBtn';
|
||
newDiv.innerHTML = `<span style="color:#007AFF; cursor:default;" id="labelMoveToCurrent">${langCode === 'ko' ? '현재 위치로 이동하기' : 'Move to Current Location'}</span>`;
|
||
|
||
newDiv.addEventListener('click', () => {
|
||
if (hasValidCoords(lastLat, lastLng)) {
|
||
mapState.map.panTo(new naver.maps.LatLng(lastLat, lastLng));
|
||
}
|
||
});
|
||
newDiv.addEventListener('pointerdown', () => newDiv.style.background = '#d3d3d3');
|
||
newDiv.addEventListener('pointerup', () => newDiv.style.background = '#fff');
|
||
newDiv.addEventListener('pointerleave', () => newDiv.style.background = '#fff');
|
||
|
||
parent.parentNode.insertBefore(newDiv, parent.nextSibling);
|
||
}
|
||
}
|
||
|
||
function syncUpdateBtn() {
|
||
const isChecked = document.getElementById('autoUpdateSwitch').checked;
|
||
const parent = document.getElementById('autoUpdateBtn');
|
||
const exist = document.getElementById('updateBtn');
|
||
|
||
if (isChecked && exist) {
|
||
exist.remove();
|
||
} else if (!isChecked && !exist && parent) {
|
||
const newDiv = document.createElement('div');
|
||
newDiv.className = 'settingRow locateRow';
|
||
newDiv.id = 'updateBtn';
|
||
newDiv.innerHTML = `<span style="color:#007AFF; cursor:default;" id="labelForceUpdate">${langCode === 'ko' ? '업데이트 하기' : 'Force Update'}</span>`;
|
||
|
||
newDiv.addEventListener('pointerdown', () => newDiv.style.background = '#d3d3d3');
|
||
newDiv.addEventListener('pointerup', () => newDiv.style.background = '#fff');
|
||
newDiv.addEventListener('pointerleave', () => newDiv.style.background = '#fff');
|
||
|
||
newDiv.addEventListener('click', triggerForceUpdate);
|
||
|
||
parent.parentNode.insertBefore(newDiv, parent.nextSibling);
|
||
}
|
||
}
|
||
|
||
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', '800');
|
||
}
|
||
|
||
const url = `https://chaegeon.com/custom/findmydevice/php/api.php?${params.toString()}`;
|
||
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify({})
|
||
}).then(res => res.json());
|
||
}
|
||
|
||
function isIosSafari() {
|
||
const ua = window.navigator.userAgent;
|
||
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
||
const isSafari = /^((?!chrome|android).)*safari/i.test(ua);
|
||
return isIOS && isSafari;
|
||
}
|
||
|
||
function debounce(fn, delay = 400) {
|
||
let t;
|
||
return (...args) => {
|
||
clearTimeout(t);
|
||
t = setTimeout(() => fn(...args), delay);
|
||
};
|
||
}
|
||
|
||
function resetDisplayValues() {
|
||
const ids = [
|
||
'currentPosition', 'accuracyValue', 'lastUpdateTime', 'updateTrigger',
|
||
'batteryStatus', 'Battery', 'WbatteryStatus', 'WBattery',
|
||
'nowActivity', 'nowactivityAccuracy', 'todayDistance',
|
||
'todaySteps', 'todayAscended', 'todayDescended', 'connectionType'
|
||
];
|
||
ids.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = '-';
|
||
});
|
||
document.querySelectorAll('[data-sensor-row="1"], [data-sensor-title="1"], [id^="sensorSection_"]').forEach(el => el.remove());
|
||
}
|
||
|
||
function triggerForceUpdate() {
|
||
callDeviceWebhook('force_update');
|
||
|
||
const el = document.getElementById('updateBtn');
|
||
if (!el) return;
|
||
el.style.pointerEvents = 'none';
|
||
el.style.color = '#8e8e93';
|
||
el.textContent = (langCode === 'ko' ? '갱신중...' : 'Updating...');
|
||
|
||
setTimeout(() => {
|
||
el.style.pointerEvents = 'auto';
|
||
el.style.color = '#007AFF';
|
||
el.textContent = (langCode === 'ko' ? '업데이트 하기' : 'Force Update');
|
||
}, 5000);
|
||
}
|
||
|
||
function hasValidCoords(lat, lng) {
|
||
return typeof lat === 'number' && isFinite(lat) &&
|
||
typeof lng === 'number' && isFinite(lng);
|
||
}
|
||
|
||
function renderTrackFromDisplayedMarkers() {
|
||
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; // 초 → ms 보정
|
||
}
|
||
const t = Date.parse(v);
|
||
return isNaN(t) ? 0 : t;
|
||
};
|
||
|
||
// 현재 표시된 스몰마커만 정렬 → 경로 생성
|
||
const points = mapState.smallMarkers
|
||
.slice()
|
||
.sort((a, b) => toTime(a.userTime) - toTime(b.userTime))
|
||
.map(m => m.getPosition());
|
||
|
||
// 라인 갱신
|
||
if (!mapState.trackLine) {
|
||
mapState.trackLine = new naver.maps.Polyline({
|
||
map: mapState.map,
|
||
path: points,
|
||
strokeColor: '#007AFF',
|
||
strokeOpacity: 0.8,
|
||
strokeWeight: 3
|
||
});
|
||
} else {
|
||
mapState.trackLine.setPath(points);
|
||
}
|
||
}
|
||
|
||
function callDeviceWebhook(name, keepalive = false) {
|
||
return fetch('https://chaegeon.com/custom/findmydevice/php/api.php?action=webhook', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
keepalive,
|
||
body: JSON.stringify({ name })
|
||
}).catch(() => null);
|
||
}
|