mybooks / index.html
r2d209's picture
μ§‘ μœ„μΉ˜ μž…λ ₯μ‹œ 지도에 ν‘œμ‹œλ˜μ§€κ°€ μ•Šμ•„ - Follow Up Deployment
e04a7ac verified
<!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 -->
<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>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Location Input Section -->
<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>
<!-- Map and Results Section -->
<div class="lg:col-span-2 space-y-6">
<!-- Map Container -->
<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>
<!-- Transportation Options -->
<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();
// UI Elements
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'; // 'home-work' or 'work-home'
// Initialize saved locations from localStorage
let savedLocations = JSON.parse(localStorage.getItem('commuteLocations')) || {};
// Display saved locations if they exist
updateSavedLocationsDisplay();
// Event Listeners
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();
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!directionBtn.contains(e.target) && !directionDropdown.contains(e.target)) {
directionDropdown.classList.add('hidden');
}
});
// Functions
// Map variables
let map;
let homeMarker;
let workMarker;
let directionsService;
let directionsRenderer;
// Initialize map
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 37.5665, lng: 126.9780}, // Seoul coordinates
zoom: 12
});
directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);
// Add autocomplete to location inputs
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);
}
});
}
// Update map marker for home or work 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 both locations are set, show route
if (homeMarker && workMarker) {
calculateAndDisplayRoute();
}
}
// Calculate and display route between home and work
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();
// Show success message
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);
// Fetch transport options after saving
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>
`;
// Update input fields to show saved values
homeLocationInput.value = savedLocations.home;
workLocationInput.value = savedLocations.work;
}
}
function toggleDirectionDropdown() {
directionDropdown.classList.toggle('hidden');
}
function fetchTransportOptions() {
if (!savedLocations.home || !savedLocations.work) {
alert('λ¨Όμ € μ§‘κ³Ό νšŒμ‚¬ μœ„μΉ˜λ₯Ό μ €μž₯ν•΄μ£Όμ„Έμš”.');
return;
}
// Show loading state
transportResults.classList.add('hidden');
loadingDiv.classList.remove('hidden');
// Geocode both addresses
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;
}
// Update map markers
updateMapMarker('home', homeResults[0].geometry.location);
updateMapMarker('work', workResults[0].geometry.location);
// Generate transport options
const mockOptions = generateMockTransportOptions();
transportResults.innerHTML = mockOptions;
transportResults.classList.remove('hidden');
// Animate the cards
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 = [];
// Bus 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'
}
];
// Subway options
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'
}
];
// Combine and sort by departure time
const allOptions = [...busOptions, ...subwayOptions]
.sort((a, b) => new Date(a.departure) - new Date(b.departure));
// Generate HTML for each option
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}λΆ„ ν›„ 좜발`;
}
}
// Initialize with saved locations if they exist
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>