614 lines
33 KiB
Markdown
614 lines
33 KiB
Markdown
# Находка
|
||
|
||
Платформа для поиска потерянных вещей и питомцев. SPA на React 18 с JWT-аутентификацией
|
||
и автоматическим обновлением токена, чатом на WebSocket (STOMP/SockJS), картами Яндекса,
|
||
загрузкой изображений, админ-панелью рекламы и модерации объявлений, i18n (ru/en) и
|
||
светлой/тёмной темой.
|
||
|
||
---
|
||
|
||
## Содержание
|
||
|
||
1. [Быстрый старт](#1-быстрый-старт)
|
||
2. [Переменные окружения](#2-переменные-окружения)
|
||
3. [Стек и зависимости](#3-стек-и-зависимости)
|
||
4. [Скрипты](#4-скрипты)
|
||
5. [Структура проекта](#5-структура-проекта)
|
||
6. [Маршруты](#6-маршруты)
|
||
7. [Ключевые компоненты](#7-ключевые-компоненты)
|
||
8. [Контексты](#8-контексты)
|
||
9. [Хуки](#9-хуки)
|
||
10. [API-эндпоинты](#10-api-эндпоинты)
|
||
11. [WebSocket](#11-websocket)
|
||
12. [Админские эндпоинты](#12-админские-эндпоинты)
|
||
13. [Стили и темы](#13-стили-и-темы)
|
||
14. [Основные сценарии](#14-основные-сценарии)
|
||
15. [Известные проблемы и предупреждения](#15-известные-проблемы-и-предупреждения)
|
||
|
||
---
|
||
|
||
## 1. Быстрый старт
|
||
|
||
Требуется Node.js 18+ (проверено на Node 24).
|
||
|
||
```bash
|
||
npm install # зависимости (см. раздел о .npmrc)
|
||
npm start # dev-сервер на http://localhost:3000
|
||
npm run build # production-сборка в build/
|
||
```
|
||
|
||
Фронтенд ожидает работающий бэкенд (по умолчанию `http://localhost:8080`).
|
||
Если нужно обращаться к другому адресу — скопируйте `.env.example` в `.env`
|
||
и задайте `REACT_APP_API_URL`.
|
||
|
||
---
|
||
|
||
## 2. Переменные окружения
|
||
|
||
Файл `.env` **не** коммитится (см. `.gitignore`), шаблон — `.env.example`.
|
||
|
||
| Переменная | Обязательна | По умолчанию | Описание |
|
||
|------------|-------------|--------------|----------|
|
||
| `REACT_APP_API_URL` | да | `http://localhost:8080` | Базовый URL бэкенда |
|
||
| `REACT_APP_YANDEX_GEOCODER_KEY` | для карты | — | Яндекс.Геокодер: обратное геокодирование координат → адрес |
|
||
| `REACT_APP_YANDEX_MAPS_KEY` | для карты | — | Публичный ключ Яндекс.Карт, подставляется в `public/index.html` |
|
||
| `REACT_APP_SUPPORT_EMAIL` | нет | есть значение по умолчанию | Почта поддержки в модалке «Поддержка» |
|
||
|
||
> **Безопасность.** Ключи Яндекса — публичные, но попадание реального `.env` в git
|
||
> означает утечку в историю коммитов. Если файл уже коммитился, перевыпустите ключи:
|
||
> добавление `.env` в `.gitignore` не удаляет его из истории.
|
||
|
||
---
|
||
|
||
## 3. Стек и зависимости
|
||
|
||
| Пакет | Назначение |
|
||
|-------|-----------|
|
||
| `react` / `react-dom` 18 | UI |
|
||
| `react-router-dom` 7 | Роутинг (`BrowserRouter`) |
|
||
| `@stomp/stompjs` + `sockjs-client` | WebSocket (STOMP) для чата и непрочитанных |
|
||
| `framer-motion` | Анимации модалок и переходов |
|
||
| `react-hook-form` + `@hookform/resolvers` + `zod` 4 | Формы и их валидация |
|
||
| `i18next` + `react-i18next` | Локализация ru/en |
|
||
| `@emoji-mart/react` + `@emoji-mart/data` + `emoji-mart` | Эмодзи-пикер в чате |
|
||
| `react-scripts` 5.0.1 | Сборка/дев-сервер (CRA) |
|
||
|
||
Карты Яндекса подключаются напрямую через глобальный `window.ymaps`
|
||
(см. `src/utils/ymapsApi.js`), без обёртки — она ждёт готовности API
|
||
с таймаутом и отдаёт ошибку вместо вечного спиннера.
|
||
|
||
Dev-зависимости: `typescript` 5.9, `@types/react` 18, `@types/react-dom`,
|
||
`@types/node`, `@testing-library/*`.
|
||
|
||
### `.npmrc` и конфликт peer-зависимостей
|
||
|
||
В репозитории лежит `.npmrc` с `legacy-peer-deps=true`. Причина: `react-scripts@5.0.1`
|
||
объявляет `typescript` **опциональным** peer-диапазоном `^3.2.1 || ^4`, а проекту нужен
|
||
TypeScript 5 (на TS 4 не собираются `@types/react` 18.3 и `zod` 4). Конфликт формальный:
|
||
CRA типы не проверяет — их вырезает babel, а проверка запускается отдельно через
|
||
`npm run typecheck`.
|
||
|
||
Побочный эффект `legacy-peer-deps`: npm не ставит peer-зависимости автоматически,
|
||
поэтому `emoji-mart` объявлен в `dependencies` явно (его требует `@emoji-mart/react`).
|
||
|
||
---
|
||
|
||
## 4. Скрипты
|
||
|
||
| Скрипт | Что делает |
|
||
|--------|-----------|
|
||
| `npm start` | Dev-сервер с hot reload |
|
||
| `npm run build` | Production-сборка в `build/` |
|
||
| `npm test` | Jest в watch-режиме |
|
||
| `npm run test:ci` | Тесты один раз (`--watchAll=false`) — для CI |
|
||
| `npm run lint` | ESLint по `src`, `--max-warnings=0` |
|
||
| `npm run typecheck` | `tsc --noEmit` по `tsconfig.json` |
|
||
|
||
Перед коммитом стоит прогнать все четыре проверки: `lint`, `typecheck`, `test:ci`, `build`.
|
||
|
||
---
|
||
|
||
## 5. Структура проекта
|
||
|
||
```
|
||
src/
|
||
├── index.js # Точка входа
|
||
├── App.js # Провайдеры + маршруты
|
||
├── i18n.js # Инициализация i18next, persist языка
|
||
├── legacy.d.ts # Типы ассетов (*.css, *.png, ...)
|
||
│
|
||
├── api/
|
||
│ ├── apiClient.jsx # apiFetch: Authorization + refresh токена по 401
|
||
│ ├── adminAdsApi.js # Кампании, реклама, фуллскрин-реклама
|
||
│ ├── adminContentApi.js # Админские объявления: список, статус, удаление
|
||
│ └── adsApi.js # Фуллскрин-реклама, пресigned-выгрузка видео
|
||
│
|
||
├── config/
|
||
│ ├── authConstants.js # API_URL
|
||
│ ├── feedConstants.js # Пункты меню, категории, пустая форма
|
||
│ └── landingConstants.js # Контент лендинга
|
||
│
|
||
├── context/
|
||
│ ├── AuthContext.jsx # user, token, isAuthenticated, isAdmin, login/logout
|
||
│ ├── ThemeContext.jsx # Светлая/тёмная тема
|
||
│ └── UnreadContext.jsx # Счётчики непрочитанных чатов
|
||
│
|
||
├── stores/
|
||
│ ├── activeChatStore.js # Активный чат
|
||
│ └── chatPostStore.js # Кэш «объявление ↔ чат»
|
||
│
|
||
├── hooks/ # см. раздел 9
|
||
│
|
||
├── utils/
|
||
│ ├── adSession.js # ID рекламной сессии (устойчив к запрету localStorage)
|
||
│ ├── campaignStatus.js # Статусы кампаний
|
||
│ ├── chatDisplay.jsx # Данные для списка чатов
|
||
│ ├── compressImage.js # Сжатие JPEG до 1200px
|
||
│ ├── formatPhone.js # Маска +7 (___) ___-__-__
|
||
│ ├── validatePostForm.js # Валидация формы объявления
|
||
│ ├── withAds.js # Рекламная обёртка
|
||
│ └── ymapsApi.js # Ожидание window.ymaps с таймаутом
|
||
│
|
||
├── pages/
|
||
│ ├── NakhodkaLanding.jsx # Лендинг «/»
|
||
│ ├── MainFeed.tsx # Лента объявлений — главная страница
|
||
│ ├── MapModal.jsx, MapView.jsx # Карта и полноэкранный режим
|
||
│ ├── ChatListPage.jsx, ChatPage.jsx
|
||
│ ├── ReviewsPage.jsx
|
||
│ ├── CreateUpdatePost.jsx # Создание/редактирование (в т.ч. adminMode)
|
||
│ ├── PostOnFeed.jsx # Карточка объявления
|
||
│ ├── RulesModal.jsx, SupportModal.jsx
|
||
│ └── AdminAdsPage.jsx # Админ-панель
|
||
│
|
||
├── components/
|
||
│ ├── feed/ FeedTopBar.tsx, FeedContent, FeedSearchBar, PullToRefresh, RewardPopup
|
||
│ ├── nav/ BottomNav.jsx, Sidebar.jsx
|
||
│ ├── layout/ AppTopBar.jsx
|
||
│ ├── chat/ ChatHeader, ChatInputBar, ChatListItem, MessageBubble
|
||
│ ├── reviews/ RatingSummary, ReviewCard, ReviewForm, StarRating
|
||
│ ├── profile/ Profile, ProfileAvatar, ProfileFormFields, ProfileModal, profileValidation
|
||
│ ├── admin/ PostsTab, AdvertisingCreate/EditModal, CampaignCreate/EditModal, ...
|
||
│ ├── landing/ Hero, Navbar, StepsSection, RadarBeacon, FinalCta, Footer, Icon
|
||
│ ├── icons/ NavIcons + Calendar/Close/Globe/Menu/Moon/Phone/Pin/Sun/Trash
|
||
│ └── … AuthModal, Toast, MapView, PostDetailModal, ImageViewer, AdCard, AdSlot, ...
|
||
│
|
||
├── styles/
|
||
│ ├── index.css # Точка входа CSS
|
||
│ ├── base.css # Переменные, сброс, keyframes
|
||
│ ├── layout.css, sidebar.css, card.css, modal.css, ui.css
|
||
│ └── chat.css, reviews.css, rules.css, admin.css, NakhodkaLanding.css
|
||
│
|
||
└── locales/
|
||
├── ru.json
|
||
└── en.json
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Маршруты
|
||
|
||
| Путь | Компонент | Доступ |
|
||
|------|-----------|--------|
|
||
| `/` | `NakhodkaLanding` | публичный лендинг |
|
||
| `/feed` | `MainFeed` | лента (главная) |
|
||
| `/reviews` | `ReviewsPage` | отзывы о площадке |
|
||
| `/chats` | `ChatListPage` | список чатов |
|
||
| `/chats/:chatId` | `ChatPage` | чат |
|
||
| `/admin/ads` | `AdminAdsPage` | только `isAdmin` (иначе редирект на `/feed`) |
|
||
| `*` | `NakhodkaLanding` | 404-fallback |
|
||
|
||
Провайдеры: `ThemeProvider` → `AuthProvider` → `BrowserRouter`.
|
||
|
||
---
|
||
|
||
## 7. Ключевые компоненты
|
||
|
||
### `MainFeed.tsx`
|
||
|
||
Главная страница. Собирает топбар, поиск, фильтры, ленту, сайдбары, карту,
|
||
модалки (объявление, профиль, вход, правила, поддержка, просмотр фото).
|
||
Делегирует загрузку и мутации хукам `useFeedPosts`, `useCityFeed`, `useSearchPosts`,
|
||
`useMyPosts`, `useFeedActions`, `useModals`. Ошибки удаления/отметки «найдено»
|
||
показываются через `Toast`.
|
||
|
||
### `PostOnFeed.jsx`
|
||
|
||
Карточка объявления: фото, бейджи категории/типа/статуса, заголовок, описание,
|
||
адрес, дата, телефон, кнопка карты и «Написать». Для владельца — редактирование,
|
||
удаление, отметка «найдено».
|
||
|
||
### `CreateUpdatePost.jsx`
|
||
|
||
Модалка создания/редактирования. Переключатель Потеря/Находка, заголовок,
|
||
описание, фото (сжатие на клиенте), категория, карта Яндекса с меткой и обратным
|
||
геокодированием, адрес, телефон с маской, чекбокс правил. Валидация —
|
||
`validatePostForm`. С пропом `adminMode` шлёт `PUT /api/v1/admin/posts/{id}`.
|
||
|
||
### `FeedContent.jsx` / `FeedTopBar.tsx` / `PullToRefresh.jsx`
|
||
|
||
Лента, топбар со спрятанной панелью на скролле, pull-to-refresh с корректным
|
||
cleanup класса активности.
|
||
|
||
### `MapView.jsx`
|
||
|
||
Полноэкранная карта. Ждёт готовности API Яндекса, показывает плашку ошибки,
|
||
если он не загрузился. Первая загрузка маркеров идёт по фактическим границам
|
||
карты, а не по событию `load` (которого у `ymaps.Map` в 2.1 нет).
|
||
|
||
### `AdminAdsPage.jsx` + `components/admin/PostsTab.jsx`
|
||
|
||
Админ-панель: вкладки «Рекламные кампании» и «Все объявления».
|
||
Кампании, реклама, загрузка видео (presigned → S3 → confirm), фуллскрин-реклама.
|
||
`PostsTab` — список всех объявлений с поиском, фильтрами, пагинацией, правкой,
|
||
закрытием/возвратом в ленту и удалением.
|
||
|
||
---
|
||
|
||
## 8. Контексты
|
||
|
||
### `AuthContext`
|
||
|
||
```js
|
||
{
|
||
user, // object | null
|
||
token, // string | null — JWT
|
||
isAuthenticated, // !!token
|
||
isAdmin, // user.role === "ADMIN" — только скрытие UI, не защита
|
||
login(user, token), // пишет в localStorage + state
|
||
logout(), // чистит localStorage + state
|
||
updateUser(userData), // обновляет пользователя
|
||
}
|
||
```
|
||
|
||
Хранится в `localStorage`: `user` (JSON), `token`, `refreshToken`.
|
||
Хук: `useAuthContext()`.
|
||
|
||
### `ThemeContext`
|
||
|
||
Светлая/тёмная тема; выбор сохраняется.
|
||
|
||
### `UnreadContext`
|
||
|
||
Счётчики непрочитанных по чатам; сбрасываются при смене пользователя,
|
||
данные приходят из WS-подписки `/topic/user/{userId}/unread`.
|
||
|
||
---
|
||
|
||
## 9. Хуки
|
||
|
||
### Авторизация и API
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `apiClient.jsx` → `apiFetch` | `fetch` + `Authorization` + автоматический refresh по 401 (один refresh на параллельные запросы) |
|
||
| `useProfile.js` | Загрузка профиля |
|
||
| `useUpdateProfile.js` | Обновление профиля с аватаром |
|
||
|
||
### Объявления
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `useFeedPosts.jsx` | Лента с пагинацией, request-id guard, дедупликация |
|
||
| `useCityFeed.jsx` | Лента по городу |
|
||
| `useSearchPosts.js` | Поиск с дебаунсом |
|
||
| `useMyPosts.jsx` | Мои объявления |
|
||
| `useFeedActions.jsx` | Удаление и отметка «найдено» + `error`/`clearError` |
|
||
| `usePostMutation.js` | Создание/обновление (обычное и `adminMode`) |
|
||
| `useDeletePost.js`, `usePostStatus.js` | Точечные мутации |
|
||
| `useImageUpload.js` | Выбор, сжатие, превью изображения |
|
||
| `useModals.jsx` | Управление модалками |
|
||
| `useInfiniteScroll.jsx` | Бесконечная прокрутка с cleanup `IntersectionObserver` |
|
||
| `useCities.js`, `useClickOutside.jsx`, `useIsDesktop.js` | Вспомогательные |
|
||
|
||
### Реклама
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `useFullscreenAdTrigger.js` | Показ фуллскрин-рекламы |
|
||
| `useAdClickTracking.js`, `useAdImpressionTracking.js` | Клики/показы |
|
||
| `useTranscodingStatusPolling.js` | Статус транскодинга видео: сброс между роликами, fallback `PROCESSING`, лимит попыток |
|
||
|
||
### Чат
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `useStompClient.js` | Общий STOMP-клиент: reconnect, обработка close/ошибок, стабильные колбэки |
|
||
| `useChatSocket.js` | Подписка на чат, отправка сообщений |
|
||
| `useUnreadSocket.js` | Непрочитанные |
|
||
| `useChats.js` | Список чатов, создание, сообщения (request-id guard) |
|
||
| `useContactSeller.js` | Создание/получение чата с продавцом |
|
||
| `useDeleteChat.js` | Удаление чата |
|
||
|
||
### Карта и локация
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `useGeolocation.js` | Геолокация пользователя со статусами |
|
||
| `useYandexMap.js` | Инициализация карты, метка (drag), обратное геокодирование, ошибка загрузки API |
|
||
|
||
### Прочее
|
||
|
||
| Хук | Назначение |
|
||
|-----|-----------|
|
||
| `useReviews.jsx` | Отзывы: загрузка, отправка, средний рейтинг и распределение |
|
||
| `useLanguageToggle.js` | Переключение ru/en |
|
||
|
||
---
|
||
|
||
## 10. API-эндпоинты
|
||
|
||
Базовый URL — `REACT_APP_API_URL`. Защищённые эндпоинты вызываются через
|
||
`apiFetch`, который сам добавляет `Authorization: Bearer <JWT>` и обновляет
|
||
токен при 401.
|
||
|
||
### Аутентификация
|
||
|
||
| Метод | Endpoint | Тело | Ответ |
|
||
|-------|----------|------|-------|
|
||
| `POST` | `/api/v1/auth/login` | `{ login, password }` | `{ token, refreshToken, user }` |
|
||
| `POST` | `/api/v1/auth/register` | `{ name, login, password }` | `{ token, refreshToken, user }` |
|
||
| `POST` | `/api/v1/auth/refresh` | `{ refreshToken }` | `{ token, refreshToken }` |
|
||
|
||
### Объявления
|
||
|
||
| Метод | Endpoint | Назначение |
|
||
|-------|----------|-----------|
|
||
| `GET` | `/api/v1/posts?page=&size=&…` | Лента с пагинацией (`content` / `items` / массив) |
|
||
| `GET` | `/api/v1/posts/search?…` | Поиск |
|
||
| `GET` | `/api/v1/posts/cities` | Список городов |
|
||
| `GET` | `/api/v1/posts/map?minLat&maxLat&minLng&maxLng&…` | Маркеры в границах карты |
|
||
| `GET` | `/api/v1/posts/detail/:postId` | Карточка объявления |
|
||
| `GET` | `/api/v1/posts/:userId` | Объявления пользователя |
|
||
| `POST` | `/api/v1/posts` | Создать (`FormData`) |
|
||
| `PUT` | `/api/v1/posts/:id` | Обновить (`FormData`) |
|
||
| `DELETE` | `/api/v1/posts/:id` | Удалить |
|
||
| `PATCH` | `/api/v1/posts/:postId/status` | Статус `ACTIVE` / `CLOSED` |
|
||
|
||
### Профиль
|
||
|
||
| Метод | Endpoint | Назначение |
|
||
|-------|----------|-----------|
|
||
| `PUT` | `/api/v1/users/:userId` | Обновить профиль (`FormData`) |
|
||
|
||
### Чаты
|
||
|
||
| Метод | Endpoint | Назначение |
|
||
|-------|----------|-----------|
|
||
| `GET` | `/api/v1/chats` | Список чатов |
|
||
| `POST` | `/api/v1/chats/with/:otherUserId` | Создать/получить чат (`{ postId }`) |
|
||
| `GET` | `/api/v1/chats/:chatId/messages` | Сообщения |
|
||
| `POST` | `/api/v1/chats/:chatId/messages` | Отправить |
|
||
| `POST` | `/api/v1/chats/:chatId/read` | Отметить прочитанным |
|
||
| `DELETE` | `/api/v1/chats/:chatId` | Удалить чат |
|
||
|
||
### Отзывы
|
||
|
||
| Метод | Endpoint | Тело |
|
||
|-------|----------|------|
|
||
| `GET` | `/api/v1/reviews` | — |
|
||
| `POST` | `/api/v1/reviews` | `{ rating, text }` |
|
||
|
||
### Файлы и реклама
|
||
|
||
| Метод | Endpoint | Назначение |
|
||
|-------|----------|-----------|
|
||
| `GET` | `/api/v1/files/:filename` | Изображение по имени |
|
||
| `GET` | `/api/fullscreen-ad` | Текущая фуллскрин-реклама |
|
||
| `GET` | `/api/ads/next` | Следующая реклама |
|
||
| `POST` | `/api/ads/click`, `/api/ads/impression` | Трекинг |
|
||
|
||
Значения категорий в запросах — **стабильные русские строки** (`Вещь`, `Питомец`,
|
||
`Другое`): они хранятся в БД, поэтому не переводятся вместе с интерфейсом.
|
||
Локализованы только подписи (`feedConstants.js` → `CATEGORIES`).
|
||
|
||
---
|
||
|
||
## 11. WebSocket
|
||
|
||
STOMP поверх SockJS: `${REACT_APP_API_URL}/ws` (токен передаётся при подключении).
|
||
|
||
| Назначение | Путь |
|
||
|-----------|------|
|
||
| Подписка на чат | `/topic/chat/{chatId}` |
|
||
| Подписка на непрочитанные | `/topic/user/{userId}/unread` |
|
||
| Отправка сообщения | `/app/chat.send` |
|
||
|
||
Общий клиент — `useStompClient`: единый STOMP-клиент с reconnect, обработкой
|
||
закрытия соединения и ошибок активации; колбэки хранятся в ref, поэтому
|
||
переподписка не пересоздаёт подключение.
|
||
|
||
---
|
||
|
||
## 12. Админские эндпоинты
|
||
|
||
Админ-панель живёт на `/admin/ads` (только для `isAdmin`, только десктоп) и
|
||
переключается вкладками:
|
||
|
||
- `/admin/ads` — рекламные кампании;
|
||
- `/admin/ads?tab=posts` — «Все объявления».
|
||
|
||
### Общие требования
|
||
|
||
- Префикс `/api/v1/admin`, все запросы с `Authorization: Bearer <JWT>`.
|
||
- **Роль `ADMIN` обязана проверяться на сервере по каждому эндпоинту.**
|
||
Клиентский `isAdmin` — только скрытие UI; подделка `localStorage` не должна
|
||
давать доступ к чужим объявлениям и переписке.
|
||
- Тело ошибок — JSON `{"message": "..."}`; фронт показывает этот текст.
|
||
- Пагинация как у `GET /api/v1/posts`: `{ "content": [...], "last": bool, "totalElements": N }`.
|
||
Фронт принимает и `content`, и `items`, и голый массив.
|
||
|
||
### Реклама (работает)
|
||
|
||
`/api/v1/admin/campaigns`, `/api/v1/admin/ads`, `/api/v1/admin/fullscreen-ads`.
|
||
|
||
### Объявления
|
||
|
||
#### `GET /api/v1/admin/posts`
|
||
|
||
Параметры (все опциональны): `page` (с 0), `size` (по умолчанию 20, максимум 100),
|
||
`search`, `type` (`LOSS`/`FOUND`), `status`.
|
||
|
||
Видны **все** статусы, включая `MODERATION` и `REJECTED`. Сортировка по умолчанию —
|
||
новые сверху (`createdAt DESC`). `search` ищет без учёта регистра по названию,
|
||
описанию, городу, району и данным владельца (login, имя, фамилия).
|
||
|
||
Элемент `content` — то же, что у публичного объявления, плюс данные автора
|
||
(`ownerId`, `ownerLogin`, `ownerName`, `ownerFirstName`, `ownerLastName`).
|
||
Фронт читает автора с запасом (`ownerLogin` / `authorLogin` / `userLogin` и т. п.).
|
||
|
||
#### `PUT /api/v1/admin/posts/{postId}`
|
||
|
||
`multipart/form-data`, как у пользовательского `PUT /api/v1/posts/{id}`:
|
||
`type`, `title`, `description`, `category`, `phone`, `reward`, `rewardText`,
|
||
`images`, `existingImages`, `latitude`, `longitude`, `address`.
|
||
Ответ `200` — обновлённое объявление.
|
||
|
||
Отличия от пользовательской правки:
|
||
|
||
- **Статус сохраняется** — админ чинит объявление, не убирая его из ленты
|
||
(`MODERATION` отправил бы на повторную AI-модерацию, `REJECTED` скрыл бы навсегда).
|
||
Смена статуса — отдельным `PATCH`.
|
||
- Падение геокодинга не блокирует правку: город берётся из `address`.
|
||
- Изображения, которых нет в `existingImages`, удаляются.
|
||
|
||
#### `DELETE /api/v1/admin/posts/{postId}`
|
||
|
||
Ответ `204 No Content`. Вместе с объявлением чистятся изображения в хранилище,
|
||
записи из `favorites` и обнуляется `post_id` в чатах.
|
||
|
||
#### `PATCH /api/v1/admin/posts/{postId}/status`
|
||
|
||
Тело `{ "status": "CLOSED" }` — закрыть («найдено»), `{ "status": "ACTIVE" }` —
|
||
вернуть в ленту. Принимаются все `PostStatus`: `ACTIVE`, `CLOSED`, `MODERATION`,
|
||
`REJECTED` (фронт отдаёт первые два). Переход в `CLOSED` увеличивает `posts_found`.
|
||
Неизвестный статус — `400`.
|
||
|
||
### Чаты (вкладка не сделана)
|
||
|
||
Требуемые эндпоинты:
|
||
|
||
| Метод | Endpoint | Назначение |
|
||
|-------|----------|-----------|
|
||
| `GET` | `/api/v1/admin/chats?page&size&search&postId` | Список чатов с превью |
|
||
| `GET` | `/api/v1/admin/chats/{chatId}/messages` | Сообщения чата |
|
||
| `POST` | `/api/v1/admin/chats/{chatId}/messages` | Написать в чат (`{ "content": "…" }`) |
|
||
| `DELETE` | `/api/v1/admin/chats/{chatId}` | Удалить чат с перепиской |
|
||
|
||
**Важно:** `senderId` должен браться **из JWT, а не из тела запроса**, иначе админ
|
||
мог бы слать сообщения от имени пользователей. Авторизация — только `ADMIN`;
|
||
это осознанное расширение прав по сравнению с пользовательским чатом
|
||
(например, чтобы предупредить о мошенничестве).
|
||
|
||
### Что сделано на клиенте
|
||
|
||
- `src/api/adminContentApi.js` — `getAdminPosts`, `deleteAdminPost`,
|
||
`updateAdminPostStatus`; пагинация принимает `content` / `items` / массив;
|
||
`type` нормализуется в `loss`/`found`.
|
||
- `CreateUpdatePost` с пропом `adminMode` → `usePostMutation` шлёт
|
||
`PUT /api/v1/admin/posts/{id}` (форма и валидация общие с пользовательской).
|
||
- `src/components/admin/PostsTab.jsx` — список, поиск с дебаунсом, фильтры,
|
||
«Показать ещё», карточки с действиями.
|
||
|
||
### Что осталось на клиенте
|
||
|
||
- Вкладка «Все чаты» (`ChatsTab.jsx` не создана) — ждёт эндпоинтов чатов.
|
||
- Проверка роли и скрытие UI: `AuthContext.isAdmin`, `AdminRoute` в `App.js`,
|
||
`isAdmin` в сайдбаре. Это удобство, не защита.
|
||
- Админка не постит сообщения через STOMP: используется REST, чтобы реплика
|
||
не зависела от того, разрешена ли админу подписка `/topic/chat/{id}`.
|
||
|
||
### Статус бэкенда
|
||
|
||
Репозиторий бэкенда: `D:\reFound\ReFound`.
|
||
|
||
| Файл | Назначение |
|
||
| --- | --- |
|
||
| `controller/admin/PostAdminController.java` | Эндпоинты объявлений, у каждого `@PreAuthorize("hasRole('ADMIN')")` |
|
||
| `service/admin/AdminPostService.java` | Логика: список, правка, удаление, статус |
|
||
| `dto/admin/AdminPostResponse.java` | Объявление + данные владельца |
|
||
| `dto/admin/AdminPostStatusUpdateRequest.java` | `{ "status": "..." }` |
|
||
| `util/PostSpecification.java` | `adminSearch` — поиск по полям объявления и владельца |
|
||
| `repository/ImageRepository.java` | `findByPostIdIn` — картинки страницы одним запросом вместо N+1 |
|
||
| `repository/ChatRepository.java` | `clearPostReference` — обнуление `post_id` при удалении |
|
||
| `test/.../AdminPostServiceTest.java` | 13 тестов: поиск, фильтры, статус, статистика, картинки, кэш |
|
||
|
||
Защита дублируется: `SecurityConfig` требует `ADMIN` на `/api/v1/admin/**`,
|
||
и методы контроллера дополнительно закрыты `@PreAuthorize`.
|
||
|
||
---
|
||
|
||
## 13. Стили и темы
|
||
|
||
Подключение через `src/styles/index.css`:
|
||
|
||
```
|
||
index.css → base.css → layout.css → sidebar.css → card.css → modal.css → ui.css
|
||
```
|
||
|
||
Дополнительно подключаются в компонентах: `chat.css` (`App.js`),
|
||
`reviews.css`, `rules.css`, `admin.css`, `NakhodkaLanding.css`.
|
||
|
||
CSS-переменные в `base.css`:
|
||
|
||
| Переменная | Значение | Описание |
|
||
|-----------|----------|----------|
|
||
| `--teal` | `#2de0c8` | Основной акцент |
|
||
| `--teal2` | `#1fc9b0` | Тёмный оттенок |
|
||
| `--border` | `rgba(255,255,255,0.09)` | Границы |
|
||
| `--muted` / `--muted2` | `rgba(255,255,255,.38)` / `.15` | Второстепенный текст |
|
||
| `--font` | `Nunito`, sans-serif | Шрифт |
|
||
| `--err` | `#ff6b6b` | Ошибка |
|
||
|
||
Тёмная тема — градиентные фоны, «стекло» (`backdrop-filter`), акцент `#2de0c8`;
|
||
светлая переключается через `ThemeContext`.
|
||
|
||
Адаптивность:
|
||
|
||
| Брейкпоинт | Что меняется |
|
||
|------------|-------------|
|
||
| `≤ 900px` | Правый сайдбар скрывается, навигация внизу |
|
||
| `≤ 768px` | Компактные отступы и кнопки |
|
||
| `≤ 600px` | Модалки на всю ширину |
|
||
|
||
---
|
||
|
||
## 14. Основные сценарии
|
||
|
||
**Аутентификация.** Гость нажимает «Разместить», «Мои объявления» или аватар →
|
||
открывается `AuthModal` → `POST /api/v1/auth/login` → `AuthContext.login()`
|
||
сохраняет `user`, `token`, `refreshToken` в `localStorage`. Просрочка access-токена
|
||
обрабатывается прозрачно: `apiFetch` при 401 обновляет токен и повторяет запрос;
|
||
при неудаче чистит сессию.
|
||
|
||
**Создание объявления.** Авторизованный пользователь → `CreateUpdatePost` →
|
||
заполняет форму → клиентское сжатие фото → `POST /api/v1/posts` (`FormData`) →
|
||
лента обновляется.
|
||
|
||
**Поиск и чат.** «Написать» → `POST /api/v1/chats/with/:otherUserId` →
|
||
переход на `/chats/:chatId` → подписка на `/topic/chat/{chatId}` →
|
||
сообщения в реальном времени.
|
||
|
||
**Карта.** `useYandexMap` ждёт `window.ymaps` (с таймаутом и ошибкой вместо
|
||
вечного спиннера), клик по карте ставит метку, drag метки и клик переводят
|
||
координаты в адрес через Яндекс.Геокодер; запрос на геокодинг защищён
|
||
request-id от гонок.
|
||
|
||
**Профиль.** Аватар в топбаре → `ProfileModal` → `PUT /api/v1/users/:id` →
|
||
данные обновляются в `AuthContext` и `localStorage`; «Выйти» — очистка сессии.
|
||
|
||
---
|
||
|
||
## 15. Известные проблемы и предупреждения
|
||
|
||
- **`fs.F_OK is deprecated`** при сборке — предупреждение Node 24 из внутренностей
|
||
`react-scripts@5`. Не влияет на результат; уходит при переходе на CRA 6 / Vite.
|
||
- **`npm audit`** показывает уязвимости в транзитивных зависимостях CRA
|
||
(webpack-dev-server и др.). Применять `npm audit fix --force` нельзя: это
|
||
ломающие обновления в пределах `react-scripts@5`.
|
||
- **Ключи Яндекса** попадали в историю git — их нужно перевыпустить.
|
||
- **Клиентская проверка роли** (`isAdmin`, `AdminRoute`) — только скрытие UI.
|
||
Реальная авторизация обязана быть на сервере по каждому эндпоинту.
|
||
- **Известная проблема бэкенда:** `GlobalExceptionHandler` ловит `Exception`
|
||
без исключений, поэтому битый JSON или неверный тип параметра дают `500`,
|
||
а не `400`. Из-за этого `type` и `status` в админских эндпоинтах принимаются
|
||
строками и разбираются вручную. |