|
<!DOCTYPE html> |
|
<html lang="ko"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Home β Work Commute Tracker</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script> |
|
<style> |
|
.map-container { |
|
height: 400px; |
|
width: 100%; |
|
border-radius: 0.75rem; |
|
} |
|
.transport-card { |
|
transition: all 0.3s ease; |
|
} |
|
.transport-card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
|
} |
|
.loading-spinner { |
|
animation: spin 1s linear infinite; |
|
} |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
.slide-in { |
|
animation: slideIn 0.5s forwards; |
|
} |
|
@keyframes slideIn { |
|
from { transform: translateY(20px); opacity: 0; } |
|
to { transform: translateY(0); opacity: 1; } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
|
|
<header class="mb-8 text-center"> |
|
<h1 class="text-3xl md:text-4xl font-bold text-blue-600 mb-2">Home β Work Commute Tracker</h1> |
|
<p class="text-gray-600">μ€μκ°μΌλ‘ μ§κ³Ό νμ¬ μ¬μ΄μ μ΅μ μ μ΄λ κ²½λ‘λ₯Ό νμΈνμΈμ</p> |
|
</header> |
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<div class="lg:col-span-1 bg-white rounded-xl shadow-md p-6"> |
|
<h2 class="text-xl font-semibold mb-4 text-gray-800">μμΉ μ€μ </h2> |
|
|
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1" for="home-location">μ§ μμΉ</label> |
|
<div class="flex"> |
|
<input type="text" id="home-location" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="μ§ μ£Όμ μ
λ ₯"> |
|
<button id="home-current" class="bg-gray-100 px-3 py-2 border border-gray-300 rounded-r-md hover:bg-gray-200"> |
|
<i class="fas fa-location-arrow"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1" for="work-location">νμ¬ μμΉ</label> |
|
<div class="flex"> |
|
<input type="text" id="work-location" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="νμ¬ μ£Όμ μ
λ ₯"> |
|
<button id="work-current" class="bg-gray-100 px-3 py-2 border border-gray-300 rounded-r-md hover:bg-gray-200"> |
|
<i class="fas fa-location-arrow"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="pt-2"> |
|
<button id="save-locations" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition duration-300"> |
|
μμΉ μ μ₯νκΈ° |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-6 pt-4 border-t border-gray-200"> |
|
<h3 class="text-lg font-medium text-gray-800 mb-3">μ μ₯λ μμΉ</h3> |
|
<div id="saved-locations" class="space-y-2"> |
|
<div class="text-gray-500 italic">μμΉλ₯Ό μ μ₯ν΄μ£ΌμΈμ</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-2 space-y-6"> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-4"> |
|
<h2 class="text-xl font-semibold mb-4 text-gray-800">κ²½λ‘ μ§λ</h2> |
|
<div id="map" class="map-container bg-gray-100 flex items-center justify-center"> |
|
<div class="text-center text-gray-500"> |
|
<i class="fas fa-map-marked-alt text-4xl mb-2"></i> |
|
<p>μμΉλ₯Ό μ
λ ₯νλ©΄ μ§λκ° νμλ©λλ€</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-gray-800">μ΄λ μ΅μ
</h2> |
|
<div class="flex space-x-2"> |
|
<button id="refresh-btn" class="bg-gray-100 p-2 rounded-full hover:bg-gray-200"> |
|
<i class="fas fa-sync-alt"></i> |
|
</button> |
|
<div class="relative"> |
|
<button id="direction-btn" class="bg-gray-100 px-3 py-1 rounded-md hover:bg-gray-200 flex items-center"> |
|
<span id="direction-text">μ§ β νμ¬</span> |
|
<i class="fas fa-chevron-down ml-2 text-sm"></i> |
|
</button> |
|
<div id="direction-dropdown" class="hidden absolute right-0 mt-1 w-40 bg-white rounded-md shadow-lg z-10"> |
|
<a href="#" class="direction-option block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" data-direction="home-work">μ§ β νμ¬</a> |
|
<a href="#" class="direction-option block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" data-direction="work-home">νμ¬ β μ§</a> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="transport-results"> |
|
<div class="text-center py-10"> |
|
<div class="inline-block p-4 bg-blue-50 rounded-full mb-3"> |
|
<i class="fas fa-bus text-blue-500 text-2xl"></i> |
|
</div> |
|
<h3 class="text-lg font-medium text-gray-700 mb-1">μ΄λ μ΅μ
μ νμΈν΄λ³΄μΈμ</h3> |
|
<p class="text-gray-500 text-sm">μμΉλ₯Ό μ μ₯ν ν μ‘°νν μ μμ΅λλ€</p> |
|
</div> |
|
</div> |
|
|
|
<div id="loading" class="hidden text-center py-10"> |
|
<div class="inline-block p-4"> |
|
<i class="fas fa-circle-notch loading-spinner text-blue-500 text-2xl"></i> |
|
</div> |
|
<p class="text-gray-500 mt-2">μ΄λ μ΅μ
μ κ²μ μ€μ
λλ€...</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
initMap(); |
|
|
|
const homeLocationInput = document.getElementById('home-location'); |
|
const workLocationInput = document.getElementById('work-location'); |
|
const homeCurrentBtn = document.getElementById('home-current'); |
|
const workCurrentBtn = document.getElementById('work-current'); |
|
const saveLocationsBtn = document.getElementById('save-locations'); |
|
const savedLocationsDiv = document.getElementById('saved-locations'); |
|
const transportResults = document.getElementById('transport-results'); |
|
const loadingDiv = document.getElementById('loading'); |
|
const refreshBtn = document.getElementById('refresh-btn'); |
|
const directionBtn = document.getElementById('direction-btn'); |
|
const directionText = document.getElementById('direction-text'); |
|
const directionDropdown = document.getElementById('direction-dropdown'); |
|
|
|
let currentDirection = 'home-work'; |
|
|
|
|
|
let savedLocations = JSON.parse(localStorage.getItem('commuteLocations')) || {}; |
|
|
|
|
|
updateSavedLocationsDisplay(); |
|
|
|
|
|
homeCurrentBtn.addEventListener('click', () => { |
|
getCurrentLocation('home'); |
|
}); |
|
|
|
workCurrentBtn.addEventListener('click', () => { |
|
getCurrentLocation('work'); |
|
}); |
|
|
|
saveLocationsBtn.addEventListener('click', saveLocations); |
|
|
|
refreshBtn.addEventListener('click', fetchTransportOptions); |
|
|
|
directionBtn.addEventListener('click', toggleDirectionDropdown); |
|
|
|
document.querySelectorAll('.direction-option').forEach(option => { |
|
option.addEventListener('click', function(e) { |
|
e.preventDefault(); |
|
currentDirection = this.dataset.direction; |
|
directionText.textContent = this.textContent; |
|
directionDropdown.classList.add('hidden'); |
|
fetchTransportOptions(); |
|
}); |
|
}); |
|
|
|
|
|
document.addEventListener('click', function(e) { |
|
if (!directionBtn.contains(e.target) && !directionDropdown.contains(e.target)) { |
|
directionDropdown.classList.add('hidden'); |
|
} |
|
}); |
|
|
|
|
|
|
|
let map; |
|
let homeMarker; |
|
let workMarker; |
|
let directionsService; |
|
let directionsRenderer; |
|
|
|
|
|
function initMap() { |
|
map = new google.maps.Map(document.getElementById('map'), { |
|
center: {lat: 37.5665, lng: 126.9780}, |
|
zoom: 12 |
|
}); |
|
|
|
directionsService = new google.maps.DirectionsService(); |
|
directionsRenderer = new google.maps.DirectionsRenderer(); |
|
directionsRenderer.setMap(map); |
|
|
|
|
|
const homeAutocomplete = new google.maps.places.Autocomplete(homeLocationInput); |
|
const workAutocomplete = new google.maps.places.Autocomplete(workLocationInput); |
|
|
|
homeAutocomplete.addListener('place_changed', () => { |
|
const place = homeAutocomplete.getPlace(); |
|
if (place.geometry) { |
|
updateMapMarker('home', place.geometry.location); |
|
} |
|
}); |
|
|
|
workAutocomplete.addListener('place_changed', () => { |
|
const place = workAutocomplete.getPlace(); |
|
if (place.geometry) { |
|
updateMapMarker('work', place.geometry.location); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function updateMapMarker(type, location) { |
|
const markerOptions = { |
|
position: location, |
|
map: map, |
|
animation: google.maps.Animation.DROP |
|
}; |
|
|
|
if (type === 'home') { |
|
if (homeMarker) homeMarker.setMap(null); |
|
markerOptions.icon = { |
|
url: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png' |
|
}; |
|
homeMarker = new google.maps.Marker(markerOptions); |
|
} else { |
|
if (workMarker) workMarker.setMap(null); |
|
markerOptions.icon = { |
|
url: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png' |
|
}; |
|
workMarker = new google.maps.Marker(markerOptions); |
|
} |
|
|
|
map.panTo(location); |
|
map.setZoom(14); |
|
|
|
|
|
if (homeMarker && workMarker) { |
|
calculateAndDisplayRoute(); |
|
} |
|
} |
|
|
|
|
|
function calculateAndDisplayRoute() { |
|
const start = currentDirection === 'home-work' ? homeMarker.getPosition() : workMarker.getPosition(); |
|
const end = currentDirection === 'home-work' ? workMarker.getPosition() : homeMarker.getPosition(); |
|
|
|
directionsService.route({ |
|
origin: start, |
|
destination: end, |
|
travelMode: 'TRANSIT', |
|
provideRouteAlternatives: true |
|
}, (response, status) => { |
|
if (status === 'OK') { |
|
directionsRenderer.setDirections(response); |
|
} else { |
|
console.error('Directions request failed due to ' + status); |
|
} |
|
}); |
|
} |
|
|
|
function getCurrentLocation(type) { |
|
if (navigator.geolocation) { |
|
navigator.geolocation.getCurrentPosition( |
|
(position) => { |
|
const { latitude, longitude } = position.coords; |
|
const geocoder = new google.maps.Geocoder(); |
|
const latLng = new google.maps.LatLng(latitude, longitude); |
|
|
|
geocoder.geocode({ location: latLng }, (results, status) => { |
|
if (status === 'OK' && results[0]) { |
|
const inputField = type === 'home' ? homeLocationInput : workLocationInput; |
|
inputField.value = results[0].formatted_address; |
|
updateMapMarker(type, latLng); |
|
} else { |
|
alert('μ£Όμλ₯Ό μ°Ύμ μ μμ΅λλ€.'); |
|
} |
|
}); |
|
}, |
|
(error) => { |
|
alert('μμΉ μ 보λ₯Ό κ°μ Έμ€λλ° μ€ν¨νμ΅λλ€: ' + error.message); |
|
} |
|
); |
|
} else { |
|
alert('μ΄ λΈλΌμ°μ λ μμΉ μ 보 κΈ°λ₯μ μ§μνμ§ μμ΅λλ€.'); |
|
} |
|
} |
|
|
|
function saveLocations() { |
|
const homeLocation = homeLocationInput.value.trim(); |
|
const workLocation = workLocationInput.value.trim(); |
|
|
|
if (!homeLocation || !workLocation) { |
|
alert('μ§κ³Ό νμ¬ μμΉλ₯Ό λͺ¨λ μ
λ ₯ν΄μ£ΌμΈμ.'); |
|
return; |
|
} |
|
|
|
savedLocations = { |
|
home: homeLocation, |
|
work: workLocation |
|
}; |
|
|
|
localStorage.setItem('commuteLocations', JSON.stringify(savedLocations)); |
|
updateSavedLocationsDisplay(); |
|
|
|
|
|
const originalText = saveLocationsBtn.textContent; |
|
saveLocationsBtn.textContent = 'μ μ₯ μλ£!'; |
|
saveLocationsBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); |
|
saveLocationsBtn.classList.add('bg-green-500', 'hover:bg-green-600'); |
|
|
|
setTimeout(() => { |
|
saveLocationsBtn.textContent = originalText; |
|
saveLocationsBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); |
|
saveLocationsBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); |
|
}, 2000); |
|
|
|
|
|
fetchTransportOptions(); |
|
} |
|
|
|
function updateSavedLocationsDisplay() { |
|
if (savedLocations.home && savedLocations.work) { |
|
savedLocationsDiv.innerHTML = ` |
|
<div class="flex items-start"> |
|
<div class="bg-blue-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-home text-blue-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium text-gray-800">μ§</h4> |
|
<p class="text-sm text-gray-600">${savedLocations.home}</p> |
|
</div> |
|
</div> |
|
<div class="flex items-start"> |
|
<div class="bg-purple-100 p-2 rounded-full mr-3"> |
|
<i class="fas fa-briefcase text-purple-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium text-gray-800">νμ¬</h4> |
|
<p class="text-sm text-gray-600">${savedLocations.work}</p> |
|
</div> |
|
</div> |
|
`; |
|
|
|
|
|
homeLocationInput.value = savedLocations.home; |
|
workLocationInput.value = savedLocations.work; |
|
} |
|
} |
|
|
|
function toggleDirectionDropdown() { |
|
directionDropdown.classList.toggle('hidden'); |
|
} |
|
|
|
function fetchTransportOptions() { |
|
if (!savedLocations.home || !savedLocations.work) { |
|
alert('λ¨Όμ μ§κ³Ό νμ¬ μμΉλ₯Ό μ μ₯ν΄μ£ΌμΈμ.'); |
|
return; |
|
} |
|
|
|
|
|
transportResults.classList.add('hidden'); |
|
loadingDiv.classList.remove('hidden'); |
|
|
|
|
|
const geocoder = new google.maps.Geocoder(); |
|
|
|
geocoder.geocode({ address: savedLocations.home }, (homeResults, homeStatus) => { |
|
if (homeStatus !== 'OK') { |
|
alert('μ§ μ£Όμλ₯Ό μ°Ύμ μ μμ΅λλ€.'); |
|
loadingDiv.classList.add('hidden'); |
|
return; |
|
} |
|
|
|
geocoder.geocode({ address: savedLocations.work }, (workResults, workStatus) => { |
|
loadingDiv.classList.add('hidden'); |
|
|
|
if (workStatus !== 'OK') { |
|
alert('νμ¬ μ£Όμλ₯Ό μ°Ύμ μ μμ΅λλ€.'); |
|
return; |
|
} |
|
|
|
|
|
updateMapMarker('home', homeResults[0].geometry.location); |
|
updateMapMarker('work', workResults[0].geometry.location); |
|
|
|
|
|
const mockOptions = generateMockTransportOptions(); |
|
transportResults.innerHTML = mockOptions; |
|
transportResults.classList.remove('hidden'); |
|
|
|
|
|
const cards = document.querySelectorAll('.transport-card'); |
|
cards.forEach((card, index) => { |
|
setTimeout(() => { |
|
card.classList.add('slide-in'); |
|
}, index * 100); |
|
}); |
|
}); |
|
}); |
|
} |
|
|
|
function generateMockTransportOptions() { |
|
const now = new Date(); |
|
const options = []; |
|
|
|
|
|
const busOptions = [ |
|
{ |
|
type: 'bus', |
|
number: '146', |
|
from: currentDirection === 'home-work' ? 'μμΌλ' : 'μ¬μλ', |
|
to: currentDirection === 'home-work' ? 'μ¬μλ' : 'μμΌλ', |
|
departure: formatTime(addMinutes(now, 5)), |
|
arrival: formatTime(addMinutes(now, 35)), |
|
duration: '30λΆ', |
|
stops: 12, |
|
distance: '8.5km' |
|
}, |
|
{ |
|
type: 'bus', |
|
number: '740', |
|
from: currentDirection === 'home-work' ? 'μμΌλ' : 'μ¬μλ', |
|
to: currentDirection === 'home-work' ? 'μ¬μλ' : 'μμΌλ', |
|
departure: formatTime(addMinutes(now, 15)), |
|
arrival: formatTime(addMinutes(now, 50)), |
|
duration: '35λΆ', |
|
stops: 15, |
|
distance: '9.2km' |
|
} |
|
]; |
|
|
|
|
|
const subwayOptions = [ |
|
{ |
|
type: 'subway', |
|
line: '2νΈμ ', |
|
from: currentDirection === 'home-work' ? 'κ°λ¨μ' : 'μ¬μλμ', |
|
to: currentDirection === 'home-work' ? 'μ¬μλμ' : 'κ°λ¨μ', |
|
departure: formatTime(addMinutes(now, 8)), |
|
arrival: formatTime(addMinutes(now, 28)), |
|
duration: '20λΆ', |
|
stops: 5, |
|
distance: '7.8km' |
|
}, |
|
{ |
|
type: 'subway', |
|
line: '9νΈμ ', |
|
from: currentDirection === 'home-work' ? 'μ μ λ¦μ' : 'κ΅νμμ¬λΉμ', |
|
to: currentDirection === 'home-work' ? 'κ΅νμμ¬λΉμ' : 'μ μ λ¦μ', |
|
departure: formatTime(addMinutes(now, 12)), |
|
arrival: formatTime(addMinutes(now, 32)), |
|
duration: '20λΆ', |
|
stops: 4, |
|
distance: '7.2km' |
|
} |
|
]; |
|
|
|
|
|
const allOptions = [...busOptions, ...subwayOptions] |
|
.sort((a, b) => new Date(a.departure) - new Date(b.departure)); |
|
|
|
|
|
allOptions.forEach(option => { |
|
options.push(` |
|
<div class="transport-card mb-4 bg-white border border-gray-200 rounded-lg overflow-hidden slide-in" style="opacity: 0;"> |
|
<div class="flex items-start p-4"> |
|
<div class="mr-4 mt-1"> |
|
${option.type === 'bus' ? |
|
`<div class="bg-blue-100 p-3 rounded-full"> |
|
<i class="fas fa-bus text-blue-500 text-xl"></i> |
|
</div>` : |
|
`<div class="bg-green-100 p-3 rounded-full"> |
|
<i class="fas fa-subway text-green-500 text-xl"></i> |
|
</div>`} |
|
</div> |
|
<div class="flex-1"> |
|
<div class="flex justify-between items-start"> |
|
<div> |
|
<h3 class="font-bold text-lg"> |
|
${option.type === 'bus' ? `λ²μ€ ${option.number}` : `μ§νμ² ${option.line}`} |
|
</h3> |
|
<p class="text-gray-600 text-sm"> |
|
${option.from} β ${option.to} |
|
</p> |
|
</div> |
|
<div class="text-right"> |
|
<span class="font-bold text-gray-800">${option.duration}</span> |
|
<p class="text-gray-500 text-xs">${option.distance}</p> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-3 flex items-center justify-between"> |
|
<div> |
|
<span class="font-medium">${option.departure}</span> |
|
<span class="mx-2 text-gray-400">β</span> |
|
<span class="font-medium">${option.arrival}</span> |
|
</div> |
|
<div class="text-sm text-gray-500"> |
|
${option.stops}κ° μ λ₯μ₯ |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="bg-gray-50 px-4 py-3 text-sm text-gray-600 border-t border-gray-200"> |
|
<i class="far fa-clock mr-1"></i> |
|
${getNextDepartureText(option.departure)} |
|
</div> |
|
</div> |
|
`); |
|
}); |
|
|
|
return options.join(''); |
|
} |
|
|
|
function formatTime(date) { |
|
return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false }); |
|
} |
|
|
|
function addMinutes(date, minutes) { |
|
return new Date(date.getTime() + minutes * 60000); |
|
} |
|
|
|
function getNextDepartureText(departureTime) { |
|
const now = new Date(); |
|
const [hours, minutes] = departureTime.split(':').map(Number); |
|
const departureDate = new Date(); |
|
departureDate.setHours(hours, minutes, 0, 0); |
|
|
|
if (departureDate < now) { |
|
departureDate.setDate(departureDate.getDate() + 1); |
|
} |
|
|
|
const diffMs = departureDate - now; |
|
const diffMins = Math.round(diffMs / 60000); |
|
|
|
if (diffMins <= 0) { |
|
return "κ³§ μΆλ°ν©λλ€!"; |
|
} else if (diffMins < 60) { |
|
return `${diffMins}λΆ ν μΆλ°`; |
|
} else { |
|
const diffHours = Math.floor(diffMins / 60); |
|
const remainingMins = diffMins % 60; |
|
return `${diffHours}μκ° ${remainingMins}λΆ ν μΆλ°`; |
|
} |
|
} |
|
|
|
|
|
if (savedLocations.home && savedLocations.work) { |
|
fetchTransportOptions(); |
|
} |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 𧬠<a href="https://enzostvs-deepsite.hf.space?remix=r2d209/mybooks" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |