π User Story : μ¬μ©μμ κ΄μ μμ μ ν리μΌμ΄μ
μ κΈ°λ₯μ μ€λͺ
νλ κ²μ
λλ€
π Common format : [μ¬μ©μ μ ν]μΌλ‘μ, [μ΄λ€ λμ]μ νκ³ μΆμ΄μ, [μ΄λ€ μ΄μ ]μ μ»κ³ μ ν©λλ€.
- μ¬μ©μλ‘μ, μ λ λ¬λ μ΄λμ μμΉ, 거리, μκ°, μλ, κ·Έλ¦¬κ³ λΆλΉ 보ν(steps/minute)μ κΈ°λ‘νκ³ μΆμ΅λλ€. μ΄λ κ² νλ©΄ λͺ¨λ λ¬λ μ΄λ κΈ°λ‘μ λ¨κΈΈ μ μμ΅λλ€.
- μ¬μ©μλ‘μ, μ λ μ¬μ΄ν΄λ§ μ΄λμ μμΉ, 거리, μκ°, μλ, κ·Έλ¦¬κ³ μ¦κ° κ³ λ(elevation gain)λ₯Ό κΈ°λ‘νκ³ μΆμ΅λλ€. μ΄λ κ² νλ©΄ λͺ¨λ μ¬μ΄ν΄λ§ μ΄λ κΈ°λ‘μ λ¨κΈΈ μ μμ΅λλ€.
- μ¬μ©μλ‘μ, μ λ νλμ λͺ¨λ μ΄λ κΈ°λ‘μ λ³Ό μ μλλ‘ νκ³ μΆμ΅λλ€. μ΄λ κ² νλ©΄ μκ°μ΄ μ§λ¨μ λ°λΌ λ΄ μ§μ μ μ½κ² μΆμ ν μ μμ΅λλ€.
- μ¬μ©μλ‘μ, μ λ μ΄λ κΈ°λ‘μ μ§λμμλ νμΈνκ³ μΆμ΅λλ€. μ΄λ κ² νλ©΄ μ΄λμ κ°μ₯ λ§μ΄ μ΄λνλμ§ μ½κ² νμΈν μ μμ΅λλ€.
- μ¬μ©μλ‘μ, μ λ μ±μ λκ°κ³ λμ€μ λ€μ λ€μ΄μλ λͺ¨λ μ΄λ κΈ°λ‘μ λ³Ό μ μκΈ°λ₯Ό μν©λλ€. μ΄λ κ² νλ©΄ μ±μ μκ°μ΄ μ§λλ κ³μ μ¬μ©ν μ μμ΅λλ€.
- μμΉ, 거리, μκ°, νμ΄μ€, λΆλΉ 보ν(steps/minute)κ³Ό ν¨κ» λ¬λ μ΄λμ κΈ°λ‘ν©λλ€.
π μ¬μ©μκ° ν΄λ¦ν μμΉλ₯Ό 맡μ μΆκ°νλ κΈ°λ₯μ μ 곡ν©λλ€. (μμΉ μ’νλ₯Ό μ»λ κ°μ₯ μ’μ λ°©λ²μ λλ€.)
π νμ¬ μμΉλ₯Ό κΈ°λ°μΌλ‘ ν μ§λ¦¬μ μμΉ μ 보λ₯Ό μ¬μ©νμ¬ λ§΅μ νμνλ κΈ°λ₯μ μ 곡ν©λλ€. (μ¬μ©μ μΉνμ μΈ λ°©λ²μ λλ€.)
π 거리, μκ°, νμ΄μ€, λΆλΉ 보ν(steps/minute)μ μ λ ₯ν μ μλ νΌμ μ 곡ν©λλ€.
- μμΉ, 거리, μκ°, μλ, κ³ λ μμΉλκ³Ό ν¨κ» μ¬μ΄ν΄λ§ μ΄λμ κΈ°λ‘ν©λλ€.
π 거리(distance), μκ°(time), μλ(speed), κ³ λ μμΉλ(elevation gain)μ μ λ ₯ν μ μλ νΌμ μ 곡ν©λλ€.
- νλμ λͺ¨λ μ΄λ κΈ°λ‘μ νμΈν©λλ€.
π μ΄λ κΈ°λ‘μ λͺ©λ‘μΌλ‘ νμν©λλ€.
- μ§λ μμμ μ΄λ κΈ°λ‘μ νμΈν©λλ€.
π μ΄λ κΈ°λ‘μ μ§λμ νμν©λλ€.
- μ±μ λκ°κ³ λμ€μ λμμλ λͺ¨λ μ΄λ κΈ°λ‘μ λ³Ό μ μμ΅λλ€.
π λΈλΌμ°μ μ λ‘컬 μ€ν λ¦¬μ§ APIλ₯Ό μ¬μ©νμ¬ μ΄λ λ°μ΄ν°λ₯Ό μ μ₯ν©λλ€.
π νμ΄μ§ λ‘λ μ, λ‘컬 μ€ν 리μ§μμ μ μ₯λ λ°μ΄ν°λ₯Ό μ½μ΄μ νμν©λλ€.
- νμ¬ μμΉλ₯Ό κΈ°λ°μΌλ‘ μ§λλ₯Ό νμνλ κΈ°λ₯μ μν΄ μ§λ¦¬μ μμΉ μ 보(Geolocation)λ₯Ό μ¬μ©ν©λλ€.
- μ¬μ©μκ° ν΄λ¦ν μμΉλ₯Ό κΈ°λ°μΌλ‘ ν μ§λλ₯Ό νμνμ¬ μλ‘μ΄ μ΄λ κΈ°λ‘μ μΆκ°ν μ μμ΅λλ€.
- 거리, μκ°, νμ΄μ€, λΆλΉ 보ν(steps/minute)μ μ λ ₯ν μ μλ νΌμ μ 곡ν©λλ€.
- 거리, μκ°, μλ, κ³ λ μμΉλ(elevation gain)μ μ λ ₯ν μ μλ νΌμ μ 곡ν©λλ€.
- μ΄λ κΈ°λ‘μ λͺ©λ‘μΌλ‘ νμν©λλ€.
- μ΄λ κΈ°λ‘μ μ§λμ νμν©λλ€.
- μ΄λ λ°μ΄ν°λ₯Ό λΈλΌμ°μ μ μ μ₯νκΈ° μν΄ λ‘컬 μ€ν 리μ§λ₯Ό μ¬μ©ν©λλ€.
- νμ΄μ§ λ‘λ μ, μ μ₯λ λ°μ΄ν°λ₯Ό μ½μ΄μ νμν©λλ€.
- μ΄λ μμΉλ₯Ό ν΄λ¦ν λλ§λ€ μ§λλ₯Ό ν΄λΉ μμΉλ‘ μ΄λν©λλ€.
μ€μ κ°λ° κ³Όμ μμλ κ³ν λ¨κ³μμ μ΅μ’ νλ‘μ°μ°¨νΈλ₯Ό μμ±ν΄μΌ νλ κ²μ μλλλ€. ꡬν κ³Όμ λμ νλ‘μ°μ°¨νΈλ μ λ°μ μΈ κ΅¬μ‘°κ° λ³κ²½λ μ μλ κ²μ΄ μ μμ μ λλ€.
μννΈμ¨μ΄ κ°λ°νμ΄ λ λ§μ ν΅μ°°λ ₯μ μ»κ±°λ μλ‘μ΄ λμ μ μ§λ©΄νκ±°λ νΌλλ°±μ λ°μΌλ©΄μ μ΄κΈ° κ³νμ μ‘°μ νκ³ κ°μ ν΄μΌ ν μ μμ΅λλ€.
μλ₯Ό λ€μ΄, μ μμΌ κ°λ° λ°©λ²λ‘ μ λ°λ³΅μ μ΄κ³ μ μ§μ μΈ κ°λ°μ κ°μ‘°νμ¬ μ μ°μ±κ³Ό μ μλ ₯μ κ°μΆ μ μλλ‘ ν©λλ€. μ΄ λ°©λ²λ‘ μ μ κΈ°μ μΈ νΌλλ°±κ³Ό μ΄ν΄ κ΄κ³μμμ νμ μ μ₯λ €νμ¬ νλ‘μ νΈκ° λ³ννλ μꡬ μ¬νμ λ§κ² μ§νλ μ μλλ‘ ν©λλ€.
λ°λΌμ, κ³ν λ¨κ³λ₯Ό μμμ μΌλ‘ μ κ·Όνκ³ νλ‘μ νΈκ° μ§νλλ©΄μ μ‘°μ ν μ€λΉλ₯Ό ν΄μΌ ν©λλ€. μ μλ ₯κ³Ό 민첩μ±μ μ±κ³΅μ μΈ μννΈμ¨μ΄ μ루μ μ μ 곡νλ λ° μ€μν μμμ λλ€.
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
function (position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
console.log(
`https://www.google.pt/maps/@${latitude},${longitude},?entry=ttu`
); // google Mapμ ν΅ν΄ μμ μ νμ¬μμΉλ₯Ό ꡬν.
},
function () {
alert(`Could not get your position`); // μ²μ λ‘λν λ μμΉλ₯Ό νμ©νμ§ μμΌλ©΄ μλ¦Ό ꡬν
}
);
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
function (position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
console.log(
`https://www.google.pt/maps/@${latitude},${longitude},?entry=ttu`
); // google Mapμ ν΅ν΄ μμ μ νμ¬μμΉλ₯Ό ꡬν.
const coords = [latitude, longitude]; // νμ¬μμΉ μ’νλ₯Ό 맀κ°λ³μν¨.
const map = L.map("map").setView(coords, 13);
// Lμ Leafletμ λΌμ΄λΈλ¬λ¦¬λ₯Ό λ»ν¨.
//mapμ htmlνμΌμ idλ₯Ό κ°λ₯΄ν΄.
L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
L.marker(coords)
.addTo(map)
.bindPopup("A pretty CSS popup.<br> Easily customizable.")
.openPopup();
},
function () {
alert(`Could not get your position`);
}
);
map.on(`click`, function (mapEvent) {
console.log(mapEvent);
const { lat, lng } = mapEvent.latlng; // ν΄λ¦μ ν λλ§λ€ λ§μ»€κ° μ§μ λκ³ ν΄λΉ μ’νλ₯Ό νμνλ μ΄λ²€νΈ.
L.marker([lat, lng])
.addTo(map)
.bindPopup(
L.popup({
maxwidth: 250,
minwidth: 100,
autoClose: false,
closeOnClick: false,
className: `running-popup`,
})
)
.setPopupContent(`Workout`)
https://leafletjs.com/reference.html#popup
map.on(`click`, function (mapE) {
mapEvent = mapE;
form.classList.remove(`hidden`);
inputDistance.focus();
});
},
function () {
alert(`Could not get your position`);
}
);
form.addEventListener(`submit`, function (e) {
e.preventDefault();
// Clear input fields
inputDistance.value =
inputDuration.value =
inputDistance.value =
inputElevation.value =
``;
// Display marker
console.log(mapEvent);
const { lat, lng } = mapEvent.latlng;
L.marker([lat, lng])
.addTo(map)
.bindPopup(
L.popup({
maxwidth: 250,
minwidth: 100,
autoClose: false,
closeOnClick: false,
className: `running-popup`,
})
)
.setPopupContent(`Workout`)
.openPopup();
});
inputType.addEventListener(`change`, function () {
inputElevation.closest(`.form__row`).classList.toggle(`form__row--hidden`);
inputCadence.closest(`.form__row`).classList.toggle(`form__row--hidden`);
});
class WorkOut {
date = new Date();
id = (Date.now() + ``).slice(-10);
constructor(coords, distance, duration) {
this.coords = coords;
this.distance = distance;
this.duration = duration;
}
}
class Running extends WorkOut {
constructor(coords, distance, duration, cadance) {
super(coords, distance, duration);
this.cadance = cadance;
this.calcPace(0);
}
calcPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends WorkOut {
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
}
clacSpeed() {
// km/h
this.speed = this.distance / (this.duration / 60);
return this.speed;
}
}
// const run1 = new Running([39, -12], 5.2, 24, 178);
// const cycling1 = new Cycling([39, -12], 27, 95, 523);
// console.log(run1, cycling1);
/////////////////////////////////////////
// APPLICATION ARCHITECTURE
class App {
#map;
#mapEvent;
constructor() {
this._getPosition();
form.addEventListener(`submit`, this._newWorkout.bind(this));
inputType.addEventListener(`change`, this._toggleElevationField);
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Could not get your position`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
console.log(
`https://www.google.pt/maps/@${latitude},${longitude},?entry=ttu`
); // google Mapμ ν΅ν΄ μμ μ νμ¬μμΉλ₯Ό ꡬν.
const coords = [latitude, longitude];
this.#map = L.map("map").setView(coords, 13);
L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling clicks on map
this.#map.on(`click`, this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove(`hidden`);
inputDistance.focus();
}
_toggleElevationField() {
inputElevation.closest(`.form__row`).classList.toggle(`form__row--hidden`);
inputCadence.closest(`.form__row`).classList.toggle(`form__row--hidden`);
}
_newWorkout(e) {
e.preventDefault();
// Clear input fields
inputDistance.value =
inputDuration.value =
inputDistance.value =
inputElevation.value =
``;
// Display marker
const { lat, lng } = this.#mapEvent.latlng;
L.marker([lat, lng])
.addTo(this.#map)
.bindPopup(
L.popup({
maxwidth: 250,
minwidth: 100,
autoClose: false,
closeOnClick: false,
className: `running-popup`,
})
)
.setPopupContent(`Workout`)
.openPopup();
}
}
const app = new App();
"use strict";
class Workout {
date = new Date();
id = (Date.now() + "").slice(-10);
clicks = 0;
constructor(coords, distance, duration) {
// this.date = ...
// this.id = ...
this.coords = coords; // [lat, lng]
this.distance = distance; // in km
this.duration = duration; // in min
}
_setDescription() {
// prettier-ignore
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
months[this.date.getMonth()]
} ${this.date.getDate()}`;
}
click() {
this.clicks++;
}
}
class Running extends Workout {
type = "running";
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.calcPace();
this._setDescription();
}
calcPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
type = "cycling";
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
// this.type = 'cycling';
this.calcSpeed();
this._setDescription();
}
calcSpeed() {
// km/h
this.speed = this.distance / (this.duration / 60);
return this.speed;
}
}
// const run1 = new Running([39, -12], 5.2, 24, 178);
// const cycling1 = new Cycling([39, -12], 27, 95, 523);
// console.log(run1, cycling1);
///////////////////////////////////////
// APPLICATION ARCHITECTURE
const form = document.querySelector(".form");
const containerWorkouts = document.querySelector(".workouts");
const inputType = document.querySelector(".form__input--type");
const inputDistance = document.querySelector(".form__input--distance");
const inputDuration = document.querySelector(".form__input--duration");
const inputCadence = document.querySelector(".form__input--cadence");
const inputElevation = document.querySelector(".form__input--elevation");
class App {
#map;
#mapZoomLevel = 13;
#mapEvent;
#workouts = [];
constructor() {
// Get user's position
this._getPosition();
// Get data from local storage
this._getLocalStorage();
// Attach event handlers
form.addEventListener("submit", this._newWorkout.bind(this));
inputType.addEventListener("change", this._toggleElevationField);
containerWorkouts.addEventListener("click", this._moveToPopup.bind(this));
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert("Could not get your position");
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
// console.log(`https://www.google.pt/maps/@${latitude},${longitude}`);
const coords = [latitude, longitude];
this.#map = L.map("map").setView(coords, this.#mapZoomLevel);
L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling clicks on map
this.#map.on("click", this._showForm.bind(this));
this.#workouts.forEach((work) => {
this._renderWorkoutMarker(work);
});
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove("hidden");
inputDistance.focus();
}
_hideForm() {
// Empty inputs
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
"";
form.style.display = "none";
form.classList.add("hidden");
setTimeout(() => (form.style.display = "grid"), 1000);
}
_toggleElevationField() {
inputElevation.closest(".form__row").classList.toggle("form__row--hidden");
inputCadence.closest(".form__row").classList.toggle("form__row--hidden");
}
_newWorkout(e) {
const validInputs = (...inputs) =>
inputs.every((inp) => Number.isFinite(inp));
const allPositive = (...inputs) => inputs.every((inp) => inp > 0);
e.preventDefault();
// Get data from form
const type = inputType.value;
const distance = +inputDistance.value;
const duration = +inputDuration.value;
const { lat, lng } = this.#mapEvent.latlng;
let workout;
// If workout running, create running object
if (type === "running") {
const cadence = +inputCadence.value;
// Check if data is valid
if (
// !Number.isFinite(distance) ||
// !Number.isFinite(duration) ||
// !Number.isFinite(cadence)
!validInputs(distance, duration, cadence) ||
!allPositive(distance, duration, cadence)
)
return alert("Inputs have to be positive numbers!");
workout = new Running([lat, lng], distance, duration, cadence);
}
// If workout cycling, create cycling object
if (type === "cycling") {
const elevation = +inputElevation.value;
if (
!validInputs(distance, duration, elevation) ||
!allPositive(distance, duration)
)
return alert("Inputs have to be positive numbers!");
workout = new Cycling([lat, lng], distance, duration, elevation);
}
// Add new object to workout array
this.#workouts.push(workout);
// Render workout on map as marker
this._renderWorkoutMarker(workout);
// Render workout on list
this._renderWorkout(workout);
// Hide form + clear input fields
this._hideForm();
// Set local storage to all workouts
this._setLocalStorage();
}
_renderWorkoutMarker(workout) {
L.marker(workout.coords)
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: `${workout.type}-popup`,
})
)
.setPopupContent(
`${workout.type === "running" ? "πββοΈ" : "π΄ββοΈ"} ${workout.description}`
)
.openPopup();
}
_renderWorkout(workout) {
let html = `
<li class="workout workout--${workout.type}" data-id="${workout.id}">
<h2 class="workout__title">${workout.description}</h2>
<div class="workout__details">
<span class="workout__icon">${
workout.type === "running" ? "πββοΈ" : "π΄ββοΈ"
}</span>
<span class="workout__value">${workout.distance}</span>
<span class="workout__unit">km</span>
</div>
<div class="workout__details">
<span class="workout__icon">β±</span>
<span class="workout__value">${workout.duration}</span>
<span class="workout__unit">min</span>
</div>
`;
if (workout.type === "running")
html += `
<div class="workout__details">
<span class="workout__icon">β‘οΈ</span>
<span class="workout__value">${workout.pace.toFixed(1)}</span>
<span class="workout__unit">min/km</span>
</div>
<div class="workout__details">
<span class="workout__icon">π¦ΆπΌ</span>
<span class="workout__value">${workout.cadence}</span>
<span class="workout__unit">spm</span>
</div>
</li>
`;
if (workout.type === "cycling")
html += `
<div class="workout__details">
<span class="workout__icon">β‘οΈ</span>
<span class="workout__value">${workout.speed.toFixed(1)}</span>
<span class="workout__unit">km/h</span>
</div>
<div class="workout__details">
<span class="workout__icon">β°</span>
<span class="workout__value">${workout.elevationGain}</span>
<span class="workout__unit">m</span>
</div>
</li>
`;
form.insertAdjacentHTML("afterend", html);
}
_moveToPopup(e) {
// BUGFIX: When we click on a workout before the map has loaded, we get an error. But there is an easy fix:
if (!this.#map) return;
const workoutEl = e.target.closest(".workout");
if (!workoutEl) return;
const workout = this.#workouts.find(
(work) => work.id === workoutEl.dataset.id
);
this.#map.setView(workout.coords, this.#mapZoomLevel, {
animate: true,
pan: {
duration: 1,
},
});
// using the public interface
// workout.click();
}
_setLocalStorage() {
localStorage.setItem("workouts", JSON.stringify(this.#workouts));
}
_getLocalStorage() {
const data = JSON.parse(localStorage.getItem("workouts"));
if (!data) return;
this.#workouts = data;
this.#workouts.forEach((work) => {
this._renderWorkout(work);
});
}
reset() {
localStorage.removeItem("workouts");
location.reload();
}
}
const app = new App();