Browse Source

Полная документация проекта + фикс бага callerInfo

- Go: package-level doc comments для всех 8 пакетов (main, config, log, validator, models, handlers, middleware, repository, services)
- Frontend: JSDoc для всех 18 файлов (компоненты, хуки, типы, утилиты, страницы)
- Исправлен баг callerInfo в log.go: string(rune(line)) -> strconv.Itoa(line)
- Создан CONTRIBUTING.md (правила работы, структура, code review)
- Создан FINDINGS.md (отчёт о найденных проблемах: 5 P0, 5 P1, 2 P2)
- Документация на русском языке
neyrogovnarik 1 month ago
parent
commit
195ecabd23
53 changed files with 589 additions and 1 deletions
  1. 120 0
      CONTRIBUTING.md
  2. 121 0
      FINDINGS.md
  3. 4 0
      backend/cmd/api/main.go
  4. 4 0
      backend/internal/config/config.go
  5. 1 0
      backend/internal/handlers/auth.go
  6. 1 0
      backend/internal/handlers/bookings.go
  7. 4 0
      backend/internal/handlers/helpers.go
  8. 1 0
      backend/internal/handlers/places.go
  9. 1 0
      backend/internal/handlers/reviews.go
  10. 1 0
      backend/internal/handlers/services.go
  11. 1 0
      backend/internal/handlers/tags.go
  12. 1 0
      backend/internal/handlers/upload.go
  13. 1 0
      backend/internal/handlers/users.go
  14. 1 0
      backend/internal/handlers/websocket.go
  15. 6 1
      backend/internal/log/log.go
  16. 4 0
      backend/internal/middleware/auth.go
  17. 1 0
      backend/internal/middleware/ratelimit.go
  18. 1 0
      backend/internal/middleware/ratelimit_redis.go
  19. 1 0
      backend/internal/models/booking.go
  20. 1 0
      backend/internal/models/place.go
  21. 1 0
      backend/internal/models/review.go
  22. 1 0
      backend/internal/models/service.go
  23. 1 0
      backend/internal/models/tag.go
  24. 4 0
      backend/internal/models/user.go
  25. 1 0
      backend/internal/repository/bookings.go
  26. 4 0
      backend/internal/repository/db.go
  27. 1 0
      backend/internal/repository/places.go
  28. 1 0
      backend/internal/repository/refresh_tokens.go
  29. 1 0
      backend/internal/repository/reviews.go
  30. 1 0
      backend/internal/repository/services.go
  31. 1 0
      backend/internal/repository/tags.go
  32. 1 0
      backend/internal/repository/users.go
  33. 4 0
      backend/internal/services/auth.go
  34. 1 0
      backend/internal/services/places.go
  35. 4 0
      backend/internal/validator/validator.go
  36. 6 0
      frontend/src/app/admin/layout.tsx
  37. 5 0
      frontend/src/app/admin/page.tsx
  38. 6 0
      frontend/src/app/admin/tags/page.tsx
  39. 5 0
      frontend/src/app/admin/users/page.tsx
  40. 5 0
      frontend/src/app/auth/login/page.tsx
  41. 5 0
      frontend/src/app/auth/register/page.tsx
  42. 6 0
      frontend/src/app/layout.tsx
  43. 5 0
      frontend/src/app/page.tsx
  44. 5 0
      frontend/src/app/places/add/page.tsx
  45. 6 0
      frontend/src/app/services/add/page.tsx
  46. 5 0
      frontend/src/app/studios/add/page.tsx
  47. 5 0
      frontend/src/components/Header.tsx
  48. 11 0
      frontend/src/components/MapView.tsx
  49. 9 0
      frontend/src/components/PlaceForm.tsx
  50. 20 0
      frontend/src/hooks/useAuth.tsx
  51. 30 0
      frontend/src/lib/api.ts
  52. 34 0
      frontend/src/lib/map.ts
  53. 118 0
      frontend/src/types/index.ts

+ 120 - 0
CONTRIBUTING.md

@@ -0,0 +1,120 @@
+# Contributing
+
+## Стек
+
+| Компонент | Технология |
+|-----------|-----------|
+| Фронтенд | Next.js 14, React 18, TypeScript, Tailwind CSS |
+| Бэкенд | Go 1.22, Chi router, pgx v5 |
+| База данных | PostgreSQL 16 + PostGIS |
+| Кеш | Redis 7 |
+| Файлы | MinIO (S3-совместимое) |
+| Аутентификация | JWT (access + refresh токены) |
+| Инфраструктура | Docker, docker-compose |
+
+## Начало разработки
+
+```bash
+git clone http://192.168.88.96:10880/foxtime/photoplaces.git
+cd photoplaces
+
+# 1. Поднять инфраструктуру (БД, Redis, MinIO)
+docker compose up -d postgres redis minio
+
+# 2. Переменные окружения
+cp backend/.env.example backend/.env
+cp frontend/.env.local.example frontend/.env.local
+
+# 3. Бэкенд (в терминале 1)
+cd backend && go run ./cmd/api
+
+# 4. Фронтенд (в терминале 2)
+cd frontend && npm install && npm run dev
+```
+
+## Правила работы с git
+
+- Ветки: `feature/краткое-описание`, `fix/краткое-описание`
+- Коммиты на русском, в настоящем времени
+- Перед коммитом: `go vet ./...`, `npm run lint` (бэкенд/фронтенд)
+- Не коммитить `deploy/env.prod` (в .gitignore)
+- Не коммитить `backend/.env`, `frontend/.env.local`
+
+## Сборка Docker
+
+```bash
+# Локальная сборка (проверка Dockerfile)
+docker compose build backend
+docker compose build frontend
+
+# Полный продакшен
+bash deploy/deploy.sh
+```
+
+## Тестирование
+
+```bash
+make test-backend    # go test ./... -v -race
+make test-frontend   # npm run test (когда появится)
+```
+
+Тестов пока нет — см. `obsidian_data/Photoplaces_data/decision-test-strategy.md`.
+
+## Миграции БД
+
+```bash
+make migrate        # накатить
+make migrate-down   # откатить последнюю
+```
+
+Новая миграция: `migrate create -ext sql -dir backend/migrations -seq название`.
+
+## Структура бэкенда
+
+```
+backend/
+  cmd/api/main.go          # точка входа
+  internal/
+    config/                # загрузка из env / _FILE (Docker secrets)
+    log/                   # структурированное логирование (slog)
+    models/                # структуры данных (User, Place, Booking...)
+    repository/            # слой доступа к БД (pgx)
+    services/              # бизнес-логика
+    handlers/              # HTTP-обработчики (Chi router)
+    middleware/             # JWT, rate limiter, CORS
+    validator/             # кастомные валидаторы (go-playground)
+  migrations/              # SQL-миграции
+  docs/                    # API контракт, схема БД, ТЗ
+```
+
+## Структура фронтенда
+
+```
+frontend/
+  src/
+    app/                   # Next.js App Router
+      admin/               # панель модератора
+      auth/                # логин/регистрация
+      places/add/          # добавление места
+      services/add/        # добавление услуги
+      studios/add/         # добавление студии
+    components/            # Header, MapView, PlaceForm
+    hooks/                 # useAuth (AuthProvider)
+    lib/                   # api.ts (клиент), map.ts (Яндекс.Карты)
+    types/                 # TypeScript-интерфейсы
+```
+
+## Документирование
+
+- Go: package-level doc comment (`// Package xxx`) — обязательно для каждого пакета
+- TypeScript: JSDoc (`/** ... */`) перед каждым экспортом
+- Комментарии на русском языке
+- Решения архитектуры — в Obsidian (`obsidian_data/Photoplaces_data/`)
+
+## Code Review
+
+- Проверять обработку ошибок (все `err` должны быть обработаны)
+- Проверять `_FILE` суффикс для Docker secrets в конфигах
+- Проверять валидацию входных данных (go-playground/validator)
+- Проверять rate limiting на публичных эндпоинтах
+- Проверять авторизацию (владелец ресурса или admin/moderator)

+ 121 - 0
FINDINGS.md

@@ -0,0 +1,121 @@
+# Findings: Аудит проекта PhotoPlaces
+
+Дата аудита: 2026-06-13
+
+---
+
+## 1. Безопасность
+
+### 🔴 P0: `websocket.go` — CheckOrigin всегда true
+`backend/internal/handlers/websocket.go:13` — WebSocket принимает соединения с любых источников. В production должен проверять `AllowedOrigins`.
+
+```go
+CheckOrigin: func(r *http.Request) bool { return true }
+```
+
+### 🔴 P0: `places.go` — ошибка авторизации не типизирована
+`backend/internal/services/places.go:118` — использует `fmt.Errorf("not your place")` вместо sentinel error. Вызывающий код не может отличить "не ваше место" от других ошибок.
+
+### 🔴 P0: Dev-секреты в `docker-compose.yml`
+Локальный `docker-compose.yml` содержит:
+- `POSTGRES_PASSWORD: photoplaces_dev`
+- `REDIS_PASSWORD: photoplaces_dev`
+- `MINIO_ROOT_PASSWORD: photoplaces_dev`
+- `JWT_SECRET: dev-secret-change-in-production`
+
+**Риск:** если запустить `docker compose up` без переопределения секретов, они уходят в production.
+
+### 🔴 P0: env.prod с placeholder-секретами
+`deploy/env.prod`:
+- `DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD`
+- `REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD`
+- `JWT_SECRET=CHANGE_ME_JWT_SECRET_32_HEX`
+- `S3_SECRET_KEY=CHANGE_ME_MINIO_PASSWORD`
+- `YANDEX_MAPS_API_KEY=ВАШ_КЛЮЧ_ЯНДЕКС_КАРТ`
+
+**Риск:** деплой с этими значениями провалится, но если кто-то скопирует env.prod на другой сервер — будет дыра.
+
+### 🟡 P1: `backend/cmd/api/main.go` — rate limiter in-memory активен
+Несмотря на наличие `ratelimit_redis.go`, в `main.go` используется in-memory rate limiter. Redis-версия не подключена.
+
+---
+
+## 2. Баги
+
+### 🔴 P0: `log.go:114` — `string(rune(line))` вместо `strconv.Itoa(line)`
+**Исправлено.** Функция `callerInfo` конвертировала номер строки в символ Unicode, а не в строку.
+
+### 🟡 P1: `strPtr` дублируется
+Определена дважды:
+- `backend/internal/handlers/helpers.go:7`
+- `backend/internal/services/auth.go:252`
+
+**Риск:** при изменении одной копии вторая рассинхронизируется.
+
+### 🟡 P1: `reviews.go` — самописные утилиты вместо стандартных
+`backend/internal/handlers/reviews.go:88-103`:
+- `isUniqueViolation` — ручной парсинг ошибки pgx, когда есть `pgconn.PgError`
+- `contains`, `searchString` — дубликат `strings.Contains`
+
+---
+
+## 3. Тесты
+
+### 🔴 P0: 0 тестов во всём проекте
+- `make test-backend` → `go test ./...` ничего не найдёт
+- `make test-frontend` → `npm run test` упадёт (нет скрипта в `package.json`)
+- `frontend/package.json` не содержит `"test"` скрипта
+
+Стратегия тестирования описана в `obsidian_data/Photoplaces_data/decision-test-strategy.md`, но не реализована.
+
+---
+
+## 4. Архитектура / Незавершённое
+
+### 🟡 P1: Только Яндекс.Карты
+`frontend/src/lib/map.ts` — интерфейс `MapProvider` есть, но реализован только Yandex Maps. MapLibre/OSM (из docs/MAP_PROVIDERS.md) не реализованы.
+
+### 🟡 P1: Redis rate limiter не подключён
+`backend/cmd/api/main.go` использует in-memory `ratelimit.go`. `ratelimit_redis.go` написан, но не включён. Решение в Obsidian (`decision-rate-limiter-redis.md`) помечено как "Accepted (needs implementation)".
+
+### 🟡 P2: `env.prod` с хардкодом IP
+`S3_PUBLIC_ENDPOINT=http://192.168.88.128:9000` — IP зашит, а не домен. При смене сервера требует ручного редактирования.
+
+---
+
+## 5. Документация (исправлено)
+
+### ✅ Go package-level docs
+Добавлены во все 8 пакетов:
+- `main`, `config`, `log`, `validator`, `models`, `handlers`, `middleware`, `repository`, `services`
+
+### ✅ Frontend JSDoc
+Добавлены ко всем экспортам: 18 файлов, все компоненты, хуки, типы, утилиты.
+
+### ✅ CONTRIBUTING.md
+Создан.
+
+### ✅ README.md
+Уже был, полный и актуальный.
+
+---
+
+## 6. Рекомендации
+
+### Приоритет 0 (немедленно)
+1. Заменить `CheckOrigin: return true` на проверку `ALLOWED_ORIGINS`
+2. Заменить `fmt.Errorf("not your place")` на sentinel error `ErrNotYourPlace`
+3. Написать хотя бы 1 smoke-тест для health endpoint
+4. Заменить placeholder-секреты в `deploy/env.prod` на сгенерированные
+5. Убрать dev-секреты из `docker-compose.yml` (вынести в `.env`)
+
+### Приоритет 1 (до production)
+6. Удалить дубликат `strPtr` из `services/auth.go`, использовать из `helpers.go`
+7. Заменить самописные `contains`/`searchString` на `strings.Contains`
+8. Подключить Redis rate limiter вместо in-memory
+9. Заменить IP в `env.prod` на доменное имя
+
+### Приоритет 2 (улучшения)
+10. Реализовать MapLibre/OSM провайдер для карт
+11. Настроить `"test"` скрипт в `frontend/package.json`
+12. Добавить `npm run test` в CI

+ 4 - 0
backend/cmd/api/main.go

@@ -1,3 +1,7 @@
+// Package main — точка входа HTTP API сервера Photoplaces.
+// Инициализирует конфигурацию, логгер, подключение к БД и Redis,
+// репозитории, сервисы, хендлеры, middleware (CORS, rate limiting, auth)
+// и запускает HTTP сервер на chi-роутере с graceful shutdown.
 package main
 
 import (

+ 4 - 0
backend/internal/config/config.go

@@ -1,3 +1,7 @@
+// Package config предоставляет конфигурацию приложения.
+// Загружает переменные окружения через godotenv, поддерживает Docker-секреты
+// через суффикс _FILE (getSecret), сборку DatabaseURL/RedisURL из компонентов.
+// Экспортирует структуру Config со всеми настройками (БД, S3, JWT, Redis, CORS).
 package config
 
 import (

+ 1 - 0
backend/internal/handlers/auth.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/bookings.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 4 - 0
backend/internal/handlers/helpers.go

@@ -1,3 +1,7 @@
+// Package handlers содержит HTTP-обработчики API-эндпоинтов Photoplaces.
+// Реализованы хендлеры: Auth, User, Place, Booking, Review, Service, Tag,
+// Upload (S3 presigned URLs) и WebSocket (посетители на карте).
+// Используют валидацию через validator и middleware для аутентификации/ролей.
 package handlers
 
 func strPtr(s string) *string {

+ 1 - 0
backend/internal/handlers/places.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/reviews.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/services.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/tags.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/upload.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/users.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 1 - 0
backend/internal/handlers/websocket.go

@@ -1,3 +1,4 @@
+// Package handlers
 package handlers
 
 import (

+ 6 - 1
backend/internal/log/log.go

@@ -1,3 +1,7 @@
+// Package log предоставляет структурированное логирование на основе slog.
+// Поддерживает JSON-формат для production и текстовый для development.
+// Позволяет получать/сохранять логгер в контексте (FromContext/WithContext),
+// добавлять request_id, user_id, error и произвольные поля к записям.
 package log
 
 import (
@@ -5,6 +9,7 @@ import (
 	"log/slog"
 	"os"
 	"runtime"
+	"strconv"
 )
 
 type ctxKey string
@@ -106,5 +111,5 @@ func callerInfo(skip int) slog.Attr {
 	if !ok {
 		return slog.String("caller", "unknown")
 	}
-	return slog.String("caller", file+":"+string(rune(line)))
+	return slog.String("caller", file+":"+strconv.Itoa(line))
 }

+ 4 - 0
backend/internal/middleware/auth.go

@@ -1,3 +1,7 @@
+// Package middleware предоставляет middleware для HTTP-роутера.
+// Включает аутентификацию через JWT (AuthMiddleware), проверку ролей
+// (RoleMiddleware), in-memory rate limiter и Redis-based rate limiter
+// с настройками для auth, read, write и admin эндпоинтов.
 package middleware
 
 import (

+ 1 - 0
backend/internal/middleware/ratelimit.go

@@ -1,3 +1,4 @@
+// Package middleware
 package middleware
 
 import (

+ 1 - 0
backend/internal/middleware/ratelimit_redis.go

@@ -1,3 +1,4 @@
+// Package middleware
 package middleware
 
 import (

+ 1 - 0
backend/internal/models/booking.go

@@ -1,3 +1,4 @@
+// Package models
 package models
 
 import "time"

+ 1 - 0
backend/internal/models/place.go

@@ -1,3 +1,4 @@
+// Package models
 package models
 
 import "time"

+ 1 - 0
backend/internal/models/review.go

@@ -1,3 +1,4 @@
+// Package models
 package models
 
 import "time"

+ 1 - 0
backend/internal/models/service.go

@@ -1,3 +1,4 @@
+// Package models
 package models
 
 import "time"

+ 1 - 0
backend/internal/models/tag.go

@@ -1,3 +1,4 @@
+// Package models
 package models
 
 type Tag struct {

+ 4 - 0
backend/internal/models/user.go

@@ -1,3 +1,7 @@
+// Package models определяет основные доменные модели Photoplaces.
+// Содержит структуры: User, Place, PlaceImage, Booking, Review, Service,
+// ServiceImage, Tag, Feature, а также фильтры (UserFilter, PlaceFilter, ServiceFilter)
+// и вспомогательные типы (Bounds). Все модели имеют JSON-теги для сериализации.
 package models
 
 import "time"

+ 1 - 0
backend/internal/repository/bookings.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 4 - 0
backend/internal/repository/db.go

@@ -1,3 +1,7 @@
+// Package repository реализует слой доступа к данным (PostgreSQL через pgxpool).
+// Предоставляет репозитории: UserRepo, RefreshTokenRepo, PlaceRepo, BookingRepo,
+// ReviewRepo, ServiceRepo, TagRepo, FeatureRepo.
+// Содержит функции для CRUD, фильтрации, работы с тегами/фичами мест и услуг.
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/places.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/refresh_tokens.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/reviews.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/services.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/tags.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 1 - 0
backend/internal/repository/users.go

@@ -1,3 +1,4 @@
+// Package repository
 package repository
 
 import (

+ 4 - 0
backend/internal/services/auth.go

@@ -1,3 +1,7 @@
+// Package services реализует бизнес-логику приложения (слой сервисов).
+// AuthService: регистрация, логин, JWT-токены (access/refresh), валидация,
+// ротация токенов, отзыв сессий.
+// PlaceService: CRUD мест, модерация, проверка владельца.
 package services
 
 import (

+ 1 - 0
backend/internal/services/places.go

@@ -1,3 +1,4 @@
+// Package services
 package services
 
 import (

+ 4 - 0
backend/internal/validator/validator.go

@@ -1,3 +1,7 @@
+// Package validator предоставляет кастомные валидаторы на базе go-playground/validator.
+// Реализованы валидации: uuid, slug, latitude, longitude, place_type, user_role,
+// currency, user_status, place_status, service_status, booking_status, datetime.
+// Экспортирует функции Validate (структуры) и ValidateVar (одно поле).
 package validator
 
 import (

+ 6 - 0
frontend/src/app/admin/layout.tsx

@@ -3,6 +3,12 @@
 import Link from 'next/link'
 import { usePathname } from 'next/navigation'
 
+/**
+ * Layout панели администратора. Отображает боковую панель
+ * с навигацией по разделам: модерация, пользователи, теги.
+ * Подсвечивает активный пункт меню.
+ * @param children - Контент текущей страницы админки
+ */
 export default function AdminLayout({ children }: { children: React.ReactNode }) {
   const pathname = usePathname()
 

+ 5 - 0
frontend/src/app/admin/page.tsx

@@ -4,6 +4,11 @@ import { useState, useEffect, useCallback } from 'react'
 import { api, ApiRequestError } from '@/lib/api'
 import type { Place } from '@/types'
 
+/**
+ * Страница модерации мест. Загружает места со статусом pending_moderation
+ * и позволяет модератору одобрить или отклонить каждое место.
+ * Использует API /places/:id/moderate для выполнения действий.
+ */
 export default function AdminModerationPage() {
   const [places, setPlaces] = useState<Place[]>([])
   const [loading, setLoading] = useState(true)

+ 6 - 0
frontend/src/app/admin/tags/page.tsx

@@ -4,6 +4,12 @@ import { useState, useEffect } from 'react'
 import { api } from '@/lib/api'
 import type { Tag, Feature } from '@/types'
 
+/**
+ * Страница управления тегами и характеристиками.
+ * Отображает две колонки: список тегов (стили съёмки) и список
+ * характеристик. Администратор может добавлять новые записи через
+ * форму с полями ID и названия.
+ */
 export default function AdminTagsPage() {
   const [tags, setTags] = useState<Tag[]>([])
   const [features, setFeatures] = useState<Feature[]>([])

+ 5 - 0
frontend/src/app/admin/users/page.tsx

@@ -18,6 +18,11 @@ const statusLabels: Record<string, string> = {
   pending_verification: 'Ожидает проверки',
 }
 
+/**
+ * Страница управления пользователями. Позволяет фильтровать
+ * пользователей по роли, изменять роль, блокировать/разблокировать.
+ * Загружает до 100 пользователей через API /admin/users.
+ */
 export default function AdminUsersPage() {
   const [users, setUsers] = useState<User[]>([])
   const [loading, setLoading] = useState(true)

+ 5 - 0
frontend/src/app/auth/login/page.tsx

@@ -5,6 +5,11 @@ import { useRouter } from 'next/navigation'
 import { useAuth } from '@/hooks/useAuth'
 import Link from 'next/link'
 
+/**
+ * Страница входа в систему. Содержит форму с полями email и пароль.
+ * После успешного входа перенаправляет пользователя на главную страницу.
+ * При ошибке отображает сообщение об ошибке.
+ */
 export default function LoginPage() {
   const router = useRouter()
   const { login } = useAuth()

+ 5 - 0
frontend/src/app/auth/register/page.tsx

@@ -5,6 +5,11 @@ import { useRouter } from 'next/navigation'
 import { useAuth } from '@/hooks/useAuth'
 import Link from 'next/link'
 
+/**
+ * Страница регистрации нового пользователя. Содержит форму с email,
+ * паролем (мин. 6 символов) и выбором роли (заказчик, арендодатель,
+ * исполнитель). После успешной регистрации перенаправляет на главную.
+ */
 export default function RegisterPage() {
   const router = useRouter()
   const { register } = useAuth()

+ 6 - 0
frontend/src/app/layout.tsx

@@ -3,11 +3,17 @@ import '@/styles/globals.css'
 import { AuthProvider } from '@/hooks/useAuth'
 import Header from '@/components/Header'
 
+/** Метаданные корневого layout — заголовок и описание всего приложения */
 export const metadata: Metadata = {
   title: 'PhotoPlaces — карта мест для фотосъёмок',
   description: 'Находите красивые места для фотосессий, бронируйте студии и нанимайте фотографов',
 }
 
+/**
+ * Корневой layout приложения. Устанавливает язык ru, тему фона,
+ * оборачивает контент в AuthProvider и отображает Header.
+ * @param children - Дочерние страницы и компоненты
+ */
 export default function RootLayout({ children }: { children: React.ReactNode }) {
   return (
     <html lang="ru">

+ 5 - 0
frontend/src/app/page.tsx

@@ -1,5 +1,10 @@
 import MapView from '@/components/MapView'
 
+/**
+ * Главная страница приложения. Отображает карту мест для фотосъёмок
+ * на весь экран. Пользователь может просматривать места, студии
+ * и взаимодействовать с картой.
+ */
 export default function HomePage() {
   return (
     <main className="relative h-screen w-screen overflow-hidden">

+ 5 - 0
frontend/src/app/places/add/page.tsx

@@ -2,6 +2,11 @@
 
 import PlaceForm from '@/components/PlaceForm'
 
+/**
+ * Страница добавления нового места для фотосъёмок.
+ * Использует компонент PlaceForm с типом 'place'.
+ * Отображается на отдельном маршруте /places/add.
+ */
 export default function AddPlacePage() {
   return (
     <div className="min-h-screen bg-[#0f172a] pt-16">

+ 6 - 0
frontend/src/app/services/add/page.tsx

@@ -5,6 +5,12 @@ import { useRouter } from 'next/navigation'
 import { api } from '@/lib/api'
 import type { Tag } from '@/types'
 
+/**
+ * Страница добавления новой услуги исполнителя (фотографа).
+ * Содержит форму с названием, описанием, ценой, длительностью,
+ * тегами и загрузкой портфолио. Изображения загружаются через
+ * presigned URL перед отправкой данных услуги.
+ */
 export default function AddServicePage() {
   const router = useRouter()
   const [tags, setTags] = useState<Tag[]>([])

+ 5 - 0
frontend/src/app/studios/add/page.tsx

@@ -2,6 +2,11 @@
 
 import PlaceForm from '@/components/PlaceForm'
 
+/**
+ * Страница добавления новой студии для фотосъёмок.
+ * Использует компонент PlaceForm с типом 'studio'.
+ * Отображается на отдельном маршруте /studios/add.
+ */
 export default function AddStudioPage() {
   return (
     <div className="min-h-screen bg-[#0f172a] pt-16">

+ 5 - 0
frontend/src/components/Header.tsx

@@ -3,6 +3,11 @@
 import Link from 'next/link'
 import { useAuth } from '@/hooks/useAuth'
 
+/**
+ * Шапка приложения. Отображает логотип PhotoPlaces и навигацию.
+ * Для авторизованных пользователей показывает email и кнопку выхода.
+ * Для гостей — ссылки на вход и регистрацию.
+ */
 export default function Header() {
   const { user, isLoading, logout } = useAuth()
 

+ 11 - 0
frontend/src/components/MapView.tsx

@@ -6,6 +6,11 @@ import { useAuth } from '@/hooks/useAuth'
 import type { Place } from '@/types'
 import { api } from '@/lib/api'
 
+/**
+ * Основной компонент карты. Инициализирует Яндекс.Карту, загружает места,
+ * отображает маркеры и показывает карточку места при клике.
+ * Для неавторизованных пользователей каждую минуту обновляет геолокацию.
+ */
 export default function MapView() {
   const containerRef = useRef<HTMLDivElement>(null)
   const mapRef = useRef<MapProvider | null>(null)
@@ -109,6 +114,12 @@ export default function MapView() {
   )
 }
 
+/**
+ * Модальная карточка места, отображаемая в нижней части экрана.
+ * Показывает название, адрес, описание, теги и рейтинг места.
+ * @param place - Данные выбранного места
+ * @param onClose - Функция закрытия карточки
+ */
 function PlaceCardModal({ place, onClose }: { place: Place; onClose: () => void }) {
   return (
     <div className="absolute bottom-0 left-0 right-0 z-20 max-h-[60vh] overflow-y-auto rounded-t-2xl bg-[#1e293b] p-6 shadow-xl">

+ 9 - 0
frontend/src/components/PlaceForm.tsx

@@ -5,10 +5,19 @@ import { useRouter } from 'next/navigation'
 import { api } from '@/lib/api'
 import type { Tag, Feature } from '@/types'
 
+/** Пропсы компонента формы создания места/студии */
 interface PlaceFormProps {
+  /** Тип создаваемого объекта: место или студия */
   type: 'place' | 'studio'
 }
 
+/**
+ * Форма создания нового места или студии.
+ * Собирает название, описание, адрес, координаты, теги, характеристики и обложку.
+ * Для студий дополнительно запрашивает цену за час и минимальное количество часов.
+ * Перед сохранением загружает обложку через presigned URL.
+ * @param type - Тип объекта: 'place' — место, 'studio' — студия
+ */
 export default function PlaceForm({ type }: PlaceFormProps) {
   const router = useRouter()
   const [tags, setTags] = useState<Tag[]>([])

+ 20 - 0
frontend/src/hooks/useAuth.tsx

@@ -4,6 +4,15 @@ import { createContext, useContext, useState, useCallback, useEffect, type React
 import type { User } from '@/types'
 import { api, ApiRequestError, setAccessToken, getAccessToken } from '@/lib/api'
 
+/**
+ * Состояние аутентификации, доступное через контекст
+ * @property user - Текущий пользователь или null
+ * @property isLoading - Флаг загрузки сессии
+ * @property login - Функция входа (email, пароль)
+ * @property register - Функция регистрации (email, пароль, роль)
+ * @property logout - Функция выхода
+ * @property refreshSession - Функция обновления сессии
+ */
 interface AuthState {
   user: User | null
   isLoading: boolean
@@ -15,6 +24,11 @@ interface AuthState {
 
 const AuthContext = createContext<AuthState | null>(null)
 
+/**
+ * Провайдер аутентификации. Оборачивает приложение в контекст AuthState.
+ * При монтировании пытается восстановить сессию через /auth/me.
+ * Предоставляет дочерним компонентам методы login, register, logout, refreshSession.
+ */
 export function AuthProvider({ children }: { children: ReactNode }) {
   const [user, setUser] = useState<User | null>(null)
   const [isLoading, setIsLoading] = useState(true)
@@ -60,6 +74,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
   )
 }
 
+/**
+ * Хук для доступа к контексту аутентификации.
+ * Должен использоваться внутри компонента AuthProvider.
+ * @returns Объект AuthState с пользователем, состоянием загрузки и методами auth
+ * @throws Ошибка, если хук вызван вне AuthProvider
+ */
 export function useAuth() {
   const ctx = useContext(AuthContext)
   if (!ctx) throw new Error('useAuth must be used within AuthProvider')

+ 30 - 0
frontend/src/lib/api.ts

@@ -3,14 +3,22 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1
 let accessToken: string | null = null
 let refreshPromise: Promise<string> | null = null
 
+/** Устанавливает access-токен для последующих запросов к API */
 export function setAccessToken(token: string | null) {
   accessToken = token
 }
 
+/** Возвращает текущий access-токен или null */
 export function getAccessToken(): string | null {
   return accessToken
 }
 
+/**
+ * Обновляет access-токен через cookie refresh-токена.
+ * Гарантирует единственный одновременный запрос на обновление.
+ * @returns Новый access-токен
+ * @throws Ошибка, если refresh-токен недействителен
+ */
 async function refreshAccessToken(): Promise<string> {
   if (refreshPromise) return refreshPromise
 
@@ -32,6 +40,17 @@ async function refreshAccessToken(): Promise<string> {
   }
 }
 
+/**
+ * Базовый метод для выполнения HTTP-запросов к API.
+ * Автоматически добавляет заголовки авторизации и JSON Content-Type.
+ * При получении 401 пытается обновить токен и повторяет запрос.
+ * @template T - Тип ожидаемого ответа
+ * @param path - Путь запроса (относительно base URL)
+ * @param options - Дополнительные опции fetch (метод, тело, сигнал и т.д.)
+ * @param isRetry - Флаг повторного запроса после обновления токена
+ * @returns Ответ, приведённый к типу T
+ * @throws {ApiRequestError} При HTTP-ошибке
+ */
 async function request<T>(
   path: string,
   options: RequestInit = {},
@@ -68,6 +87,12 @@ async function request<T>(
   return res.json()
 }
 
+/**
+ * Ошибка HTTP-запроса к API
+ * @property status - HTTP-статус код
+ * @property message - Сообщение об ошибке
+ * @property data - Дополнительные данные ошибки (опционально)
+ */
 export class ApiRequestError extends Error {
   constructor(
     public status: number,
@@ -79,16 +104,21 @@ export class ApiRequestError extends Error {
   }
 }
 
+/** Объект с методами для типизированных HTTP-запросов к API */
 export const api = {
+  /** GET-запрос */
   get: <T>(path: string, signal?: AbortSignal) =>
     request<T>(path, { method: 'GET', signal }),
 
+  /** POST-запрос с JSON-телом */
   post: <T>(path: string, body: unknown) =>
     request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
 
+  /** PATCH-запрос с JSON-телом */
   patch: <T>(path: string, body: unknown) =>
     request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
 
+  /** DELETE-запрос */
   delete: <T>(path: string) =>
     request<T>(path, { method: 'DELETE' }),
 }

+ 34 - 0
frontend/src/lib/map.ts

@@ -8,22 +8,46 @@ declare global {
   }
 }
 
+/**
+ * Абстракция над картографическим провайдером.
+ * Предоставляет единый интерфейс для управления картой, маркерами и подписки на события.
+ */
 export interface MapProvider {
+  /** Инициализирует карту в указанном контейнере */
   init(container: HTMLElement, center: [number, number], zoom: number): void
+  /** Уничтожает карту и очищает ресурсы */
   destroy(): void
+  /** Добавляет маркер на карту */
   addMarker(id: string, lat: number, lng: number, options?: MarkerOptions): void
+  /** Удаляет маркер с карты по идентификатору */
   removeMarker(id: string): void
+  /** Устанавливает центр карты */
   setCenter(lat: number, lng: number): void
+  /** Масштабирует карту, чтобы вместить указанные границы */
   fitBounds(bounds: [[number, number], [number, number]]): void
+  /** Подписывается на событие перемещения карты */
   onMove(cb: (center: [number, number], zoom: number, bounds: MapBounds) => void): void
 }
 
+/**
+ * Опции маркера на карте
+ * @property type - Тип маркера (определяет цвет и размер)
+ * @property title - Всплывающая подсказка (опционально)
+ * @property onClick - Обработчик клика по маркеру (опционально)
+ */
 export interface MarkerOptions {
   type?: 'place' | 'studio' | 'visitor'
   title?: string
   onClick?: () => void
 }
 
+/**
+ * Прямоугольные границы видимой области карты
+ * @property swLat - Широта юго-западного угла
+ * @property swLng - Долгота юго-западного угла
+ * @property neLat - Широта северо-восточного угла
+ * @property neLng - Долгота северо-восточного угла
+ */
 export interface MapBounds {
   swLat: number
   swLng: number
@@ -31,6 +55,11 @@ export interface MapBounds {
   neLng: number
 }
 
+/**
+ * Создаёт экземпляр MapProvider на основе Яндекс.Карт API v3.
+ * При первом вызове асинхронно загружает скрипт Яндекс.Карт.
+ * @returns Объект с интерфейсом MapProvider для управления картой
+ */
 export function createYandexMapProvider(): MapProvider {
   let map: any = null
   let ymaps3: any = null
@@ -150,6 +179,11 @@ export function createYandexMapProvider(): MapProvider {
   }
 }
 
+/**
+ * Загружает скрипт Яндекс.Карт API v3, если он ещё не загружен.
+ * Использует API-ключ из переменной окружения NEXT_PUBLIC_YANDEX_MAPS_API_KEY.
+ * @returns Promise, который разрешается после загрузки скрипта
+ */
 async function loadYandexScript(): Promise<void> {
   if (document.querySelector('script[src*="yandexmaps"]')) return
 

+ 118 - 0
frontend/src/types/index.ts

@@ -1,7 +1,22 @@
+/** Роль пользователя в системе: суперадмин, модератор, арендодатель, исполнитель или заказчик */
 export type UserRole = 'superadmin' | 'moderator' | 'landlord' | 'executor' | 'customer'
 
+/** Статус пользователя: активен, заблокирован или ожидает верификации */
 export type UserStatus = 'active' | 'banned' | 'pending_verification'
 
+/**
+ * Пользователь платформы
+ * @property id - Уникальный идентификатор
+ * @property email - Электронная почта
+ * @property role - Роль в системе
+ * @property status - Текущий статус
+ * @property name - Отображаемое имя (опционально)
+ * @property avatar_url - Ссылка на аватар (опционально)
+ * @property phone - Номер телефона (опционально)
+ * @property bio - Краткая биография (опционально)
+ * @property country - Страна (опционально)
+ * @property created_at - Дата регистрации
+ */
 export interface User {
   id: string
   email: string
@@ -15,15 +30,45 @@ export interface User {
   created_at: string
 }
 
+/** Тип объекта: обычное место или студия */
 export type PlaceType = 'place' | 'studio'
 
+/** Статус места: черновик, на модерации, опубликовано, отклонено или в архиве */
 export type PlaceStatus = 'draft' | 'pending_moderation' | 'published' | 'rejected' | 'archived'
 
+/**
+ * Географические координаты
+ * @property lat - Широта
+ * @property lng - Долгота
+ */
 export interface Coordinates {
   lat: number
   lng: number
 }
 
+/**
+ * Место или студия для фотосъёмок
+ * @property id - Уникальный идентификатор
+ * @property type - Тип объекта (место или студия)
+ * @property title - Название
+ * @property description - Описание (опционально)
+ * @property address - Адрес (опционально)
+ * @property coordinates - Географические координаты
+ * @property cover_image - URL обложки (опционально)
+ * @property images - Массив URL изображений
+ * @property access_info - Информация о доступе (опционально)
+ * @property rating - Рейтинг
+ * @property reviews_count - Количество отзывов
+ * @property tags - Список тегов
+ * @property features - Список характеристик
+ * @property status - Статус публикации
+ * @property owner - Владелец (id, name, avatar_url)
+ * @property pricing - Ценообразование для студий (опционально)
+ * @property booking_url - Ссылка на бронирование (опционально)
+ * @property executor_services - Услуги исполнителей (опционально)
+ * @property created_at - Дата создания
+ * @property updated_at - Дата обновления (опционально)
+ */
 export interface Place {
   id: string
   type: PlaceType
@@ -51,6 +96,15 @@ export interface Place {
   updated_at?: string
 }
 
+/**
+ * Услуга, предоставляемая исполнителем на месте съёмки
+ * @property id - Уникальный идентификатор
+ * @property title - Название услуги
+ * @property price - Стоимость
+ * @property currency - Валюта
+ * @property duration_minutes - Длительность в минутах (опционально)
+ * @property executor_name - Имя исполнителя
+ */
 export interface ExecutorService {
   id: string
   title: string
@@ -60,12 +114,25 @@ export interface ExecutorService {
   executor_name: string
 }
 
+/**
+ * Тег (стиль съёмки)
+ * @property id - Уникальный идентификатор (slug)
+ * @property name - Отображаемое название
+ * @property category - Категория тега (опционально)
+ */
 export interface Tag {
   id: string
   name: string
   category?: string
 }
 
+/**
+ * Характеристика места (напр. парковка, гримёрка)
+ * @property id - Уникальный идентификатор (slug)
+ * @property name - Отображаемое название
+ * @property category - Категория (опционально)
+ * @property icon - Иконка (опционально)
+ */
 export interface Feature {
   id: string
   name: string
@@ -73,6 +140,22 @@ export interface Feature {
   icon?: string
 }
 
+/**
+ * Услуга фотографа / исполнителя
+ * @property id - Уникальный идентификатор
+ * @property executor_id - Идентификатор исполнителя
+ * @property title - Название услуги
+ * @property description - Описание (опционально)
+ * @property price - Стоимость
+ * @property currency - Валюта
+ * @property duration_minutes - Длительность в минутах (опционально)
+ * @property tags - Связанные теги
+ * @property rating - Рейтинг
+ * @property reviews_count - Количество отзывов
+ * @property portfolio_images - Фото портфолио
+ * @property status - Статус публикации
+ * @property created_at - Дата создания
+ */
 export interface Service {
   id: string
   executor_id: string
@@ -89,6 +172,14 @@ export interface Service {
   created_at: string
 }
 
+/**
+ * Отзыв на место или услугу
+ * @property id - Уникальный идентификатор
+ * @property user - Автор отзыва (id, name, avatar_url)
+ * @property rating - Оценка
+ * @property text - Текст отзыва (опционально)
+ * @property created_at - Дата создания
+ */
 export interface Review {
   id: string
   user: Pick<User, 'id' | 'name' | 'avatar_url'>
@@ -97,6 +188,17 @@ export interface Review {
   created_at: string
 }
 
+/**
+ * Бронирование студии или места
+ * @property id - Уникальный идентификатор
+ * @property place_id - Идентификатор места
+ * @property start_time - Время начала
+ * @property end_time - Время окончания
+ * @property status - Статус бронирования
+ * @property total_price - Общая стоимость (опционально)
+ * @property currency - Валюта
+ * @property comment - Комментарий (опционально)
+ */
 export interface Booking {
   id: string
   place_id: string
@@ -108,12 +210,28 @@ export interface Booking {
   comment?: string
 }
 
+/**
+ * Ответ API с пагинацией на основе курсора
+ * @template T - Тип элементов списка
+ * @property data - Массив элементов
+ * @property next_cursor - Курсор для следующей страницы (опционально)
+ * @property has_more - Есть ли ещё элементы
+ */
 export interface PaginatedResponse<T> {
   data: T[]
   next_cursor?: string
   has_more: boolean
 }
 
+/**
+ * Структура ошибки API (соответствует RFC 7807 Problem Details)
+ * @property type - URI типа ошибки
+ * @property title - Краткое описание ошибки
+ * @property status - HTTP-статус
+ * @property detail - Подробное описание
+ * @property instance - URI экземпляра ошибки (опционально)
+ * @property errors - Ошибки валидации полей (опционально)
+ */
 export interface ApiError {
   type: string
   title: string