Skip to content

6411 Емец Т.В. Лаб.2 Вар.2#121

Open
Cure232 wants to merge 2 commits intoitsecd:mainfrom
Cure232:main
Open

6411 Емец Т.В. Лаб.2 Вар.2#121
Cure232 wants to merge 2 commits intoitsecd:mainfrom
Cure232:main

Conversation

@Cure232
Copy link
Copy Markdown

@Cure232 Cure232 commented Apr 9, 2026

Это лабораторная работа №2 по дисциплине «Безопасность веб-приложений». По варианту 2 реализован веб-аналог «Прибывалки» для электричек: поиск станций, просмотр расписания по станции, выбор любимых станций, просмотр маршрутов проходящих между станций и через станции и адаптация интерфейса под мобильные устройства. Реализовано при помощи технологий React, Node.js и др. Более полное описание и инструкция по запуску в README.md.

@AvtoBBus AvtoBBus self-requested a review April 10, 2026 08:13
"dev": "nodemon server.js"
},
"dependencies": {
"axios": "^1.6.0",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не совсем рекомендую использовать axios

https://habr.com/ru/companies/first/articles/1017244/

Comment on lines +4 to +5
const BASE_URL = 'https://api.rasp.yandex.net/v3.0/';
const STATIONS_CACHE_TTL_MS = 10 * 60 * 1000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это тоже можно в .env

@@ -0,0 +1,41 @@
const BASE = 'http://localhost:5000/api';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тоже в .env

Comment on lines +14 to +41
export async function searchStation(query) {
const data = await requestJson(`${BASE}/search-station?query=${encodeURIComponent(query)}`);
return Array.isArray(data) ? data : [];
}

export async function getStationsInBounds(bounds) {
const params = new URLSearchParams({
minLat: String(bounds.minLat),
minLng: String(bounds.minLng),
maxLat: String(bounds.maxLat),
maxLng: String(bounds.maxLng),
zoom: String(bounds.zoom)
});
const data = await requestJson(`${BASE}/search-station?${params.toString()}`);
return Array.isArray(data) ? data : [];
}

export async function getSchedule(code) {
const data = await requestJson(`${BASE}/station-schedule?station=${encodeURIComponent(code)}`);
return Array.isArray(data) ? data : [];
}

export async function getRoutes(from, to) {
const data = await requestJson(
`${BASE}/train-route?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`
);
return Array.isArray(data) ? data : [];
} No newline at end of file
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сборка итогового url для запроса, должна выполнять функция requestJson, остальные, кто вызывают её должны просто отдавать объекты туда предварительно валидировав данные (условно проверять, что при запросе станции по esr_code у вас код точно состоит из N цифр и не содержит букв)

Comment on lines +18 to +20
if (inputRef.current) {
const rect = inputRef.current.getBoundingClientRect();
setDropdownStyle({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!inputRef.current) return;

/* остальной код */

Comment on lines +81 to +97
let dropdownContent = null;
if (loading) {
dropdownContent = <div className="autocomplete-loading-item">Поиск…</div>;
} else if (suggestions.length === 0 && value.trim() !== '') {
dropdownContent = <div className="autocomplete-empty-item">Ничего не найдено</div>;
} else if (suggestions.length > 0) {
dropdownContent = suggestions.map((station, idx) => (
<button
key={station.code || idx}
type="button"
className="autocomplete-item"
onClick={() => handleSelect(station)}
>
{station.displayTitle || station.title}
</button>
));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот это лучше организовать как функцию типо renderDropDown

Comment on lines +5 to +6
const DEFAULT_CENTER = [53.1959, 50.1008];
const DEFAULT_ZOOM = 10;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Либо в .env, либо в файл конфига

@@ -0,0 +1,122 @@
import { CircleMarker, MapContainer, Popup, TileLayer, useMapEvents } from 'react-leaflet';
import { useEffect } from 'react';
import 'leaflet/dist/leaflet.css';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не советую использовать Leaflet, так как это достаточно низкоуровневая библиотека для работы с картой (читать про OpenLayers)

Comment on lines +50 to +65
const timerId = setTimeout(async () => {
try {
const stations = await searchStation(value);
if (requestId !== fromRequestId.current) return;
setFromStations(stations);
if (stations.length === 0) setMessage('Станция отправления не найдена.');
} catch {
if (requestId !== fromRequestId.current) return;
setFromStations([]);
setMessage('Ошибка поиска станции отправления.');
} finally {
if (requestId === fromRequestId.current) setFromLoading(false);
}
}, 350);

return () => clearTimeout(timerId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Честно говоря затрудняюсь сказать, что вы хотели этим сделать, выглядит как некий debounce, но если что, такие функции уже готовые и реализованные уже есть

Comment on lines +80 to +93
const timerId = setTimeout(async () => {
try {
const stations = await searchStation(value);
if (requestId !== toRequestId.current) return;
setToStations(stations);
if (stations.length === 0) setMessage('Станция прибытия не найдена.');
} catch {
if (requestId !== toRequestId.current) return;
setToStations([]);
setMessage('Ошибка поиска станции прибытия.');
} finally {
if (requestId === toRequestId.current) setToLoading(false);
}
}, 350);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогично

Comment on lines +131 to +141
setTimeout(async () => {
if (requestId !== viewportRequestId.current) return;
try {
const stations = await getStationsInBounds(viewport);
if (requestId !== viewportRequestId.current) return;
setMapStations(stations);
} catch {
if (requestId !== viewportRequestId.current) return;
setMapStations([]);
}
}, 220);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Благодаря вот такой штуке у вас может произойти момент с тем, что вы превысите количество запросов по ключу к API яндекса, если же реализовывать такие вещи в реальном продакшене, где настроено ограничение запросов, то вы будете ловить 429, что негативно скажется на UX.

Самое логичное решение, исходя из вашего желания сделать перезапрос при изменении позиционирования на карте, состоит аж из 2 вариантов:

  1. Запросить один раз все станции и отрисовать их все сразу (тот же OpenLayers не рендерит те объекты, которые не видно)
  2. Выполнять запрос через debounce. Т.е. если меняется видимая зона, то ставиться таймер на условные 1.5 секунды и если в течении этого времени поле видимости не поменялось, то выполнять запрос

Comment on lines +9 to +52
const FAVORITES_STORAGE_KEY = 'favoriteStationSchedules';

function loadFavoritesFromStorage() {
if (typeof window === 'undefined') {
return [];
}

try {
const raw = window.localStorage.getItem(FAVORITES_STORAGE_KEY);
if (!raw) {
return [];
}

const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return [];
}

return parsed
.filter(
item =>
item &&
typeof item === 'object' &&
item.station &&
typeof item.station === 'object' &&
typeof item.station.code === 'string'
)
.map(item => ({ station: item.station }));
} catch {
return [];
}
}

function saveFavoritesToStorage(favorites) {
if (typeof window === 'undefined') {
return;
}

try {
window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(favorites));
} catch {
// Ignore storage errors (private mode, quota, etc.) so schedule page stays usable.
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Всю логику работы с localStorage вынести в отдельный файл.

Проверять доступность localStorage лучше вот так

Comment on lines +79 to +126
useEffect(() => {
const value = query.trim();
if (!value) {
setSuggestions([]);
setSearchLoading(false);
return undefined;
}

const requestId = ++searchRequestId.current;
setSearchLoading(true);
setMessage('');

const timerId = setTimeout(async () => {
try {
const foundStations = await searchStation(value);
if (requestId !== searchRequestId.current) return;
setSuggestions(foundStations);
if (foundStations.length === 0) {
setMessage('Станции не найдены.');
}
} catch {
if (requestId !== searchRequestId.current) return;
setSuggestions([]);
setMessage('Не удалось выполнить поиск станции.');
} finally {
if (requestId === searchRequestId.current) {
setSearchLoading(false);
}
}
}, 350);

return () => clearTimeout(timerId);
}, [query]);

const handleViewportChange = viewport => {
const requestId = ++viewportRequestId.current;
setTimeout(async () => {
if (requestId !== viewportRequestId.current) return;
try {
const stations = await getStationsInBounds(viewport);
if (requestId !== viewportRequestId.current) return;
setMapStations(stations);
} catch {
if (requestId !== viewportRequestId.current) return;
setMapStations([]);
}
}, 220);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогично про debounce и частоту запросов

<Route path="/" element={<StationPage />} />
<Route path="/station" element={<StationPage />} />
<Route path="/route" element={<RoutePage />} />
{/*<Route path="/favorites" element={<FavoritesPage />} />*/}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если не используете, можно удалить

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants