Переглянути джерело

Первая фаза. Подготовка.

foxtime 2 місяців тому
батько
коміт
9b4671a072

+ 165 - 0
.cursor/plans/photoplaces-85b6f1c5.plan.md

@@ -0,0 +1,165 @@
+<!-- 85b6f1c5-22b1-495a-8d62-be8a8df46170 b173a0dc-4243-4738-aa84-367a9bb51602 -->
+# Детальный план разработки сервиса PhotoPlaces
+
+## Технологический стек
+
+- **Frontend**: React + TypeScript + Leaflet
+- **Backend**: Node.js + Express + TypeScript
+- **База данных**: PostgreSQL + PostGIS
+- **Карты**: Leaflet с кастомными тайлами (для России)
+- **Аутентификация**: JWT + bcrypt
+- **Платежи**: ЮKassa (для России) + Stripe (для международных пользователей)
+
+## Архитектура проекта
+
+```
+photoplaces/
+├── frontend/                 # React приложение
+├── backend/                  # Node.js API
+├── database/                 # Миграции и сиды
+├── docs/                     # Документация
+└── deployment/               # Docker и конфиги
+```
+
+## Фаза 1: Подготовка и настройка инфраструктуры
+
+### 1.1 Инициализация проекта
+
+- Создать структуру репозитория
+- Настроить package.json для фронтенда и бэкенда
+- Конфигурировать TypeScript для обоих частей
+- Настроить линтеры и форматтеры (ESLint, Prettier)
+
+### 1.2 Настройка базы данных
+
+- Установить и настроить PostgreSQL с PostGIS
+- Создать миграции для основных таблиц:
+  - Пользователи (users) с ролями
+  - Места (places) с геометками
+  - Студии (studios) с бронированием
+  - Услуги (services) фотографов
+  - Теги и категории
+
+### 1.3 Базовый бэкенд
+
+- Настроить Express сервер
+- Реализовать базовые middleware (CORS, helmet, rate limiting)
+- Создать систему аутентификации JWT
+- Настроить подключение к БД
+
+## Фаза 2: Основная функциональность
+
+### 2.1 Система пользователей и ролей
+
+- Модели пользователей с ролями:
+  - Суперадминистратор
+  - Модератор
+  - Арендодатель
+  - Исполнитель
+  - Заказчик
+  - Гость
+- API endpoints для регистрации/авторизации
+- Middleware для проверки ролей
+
+### 2.2 Карта и геоданные
+
+- Интеграция Leaflet с кастомными тайлами
+- Хранение геоданных в PostGIS
+- API для работы с местами:
+  - Создание/чтение/обновление мест
+  - Геопространственные запросы
+  - Фильтрация по характеристикам
+
+### 2.3 Карточки мест и студий
+
+- Модели данных для разных типов карточек
+- Валидация и moderation система
+- Загрузка изображений (примеры фото)
+- Система тегов и характеристик
+
+### 2.4 Система бронирования
+
+- Модель бронирования студий
+- Календарь доступности
+- Уведомления и подтверждения
+
+## Фаза 3: Дополнительные функции
+
+### 3.1 Поиск и фильтрация
+
+- Полнотекстовый поиск по местам
+- Фильтрация по тегам, характеристикам, рейтингу
+- Геопространственный поиск (ближайшие места)
+
+### 3.2 Система оплаты
+
+- Интеграция с ЮKassa для России
+- Интеграция со Stripe для международных платежей
+- Подписки и одноразовые платежи
+- Система комиссий
+
+### 3.3 Административная панель
+
+- Управление пользователями
+- Модерация карточек
+- Статистика и аналитика
+- Настройки системы
+
+## Фаза 4: Тестирование и документация
+
+### 4.1 Тестирование
+
+- Unit tests для критического функционала
+- Integration tests для API
+- E2E tests для основных сценариев
+- Load testing для карт и поиска
+
+### 4.2 Документация
+
+- API документация (Swagger/OpenAPI)
+- Руководство для разработчиков
+- Инструкции по развертыванию
+- Комментарии в коде (JSDoc)
+
+## Фаза 5: Развертывание и мониторинг
+
+### 5.1 Деплоймент
+
+- Docker контейнеризация
+- CI/CD pipeline
+- Настройка окружений (dev/staging/prod)
+
+### 5.2 Мониторинг
+
+- Логирование и мониторинг ошибок
+- Метрики производительности
+- Система оповещений
+
+## Приоритеты разработки
+
+1. Базовая инфраструктура и аутентификация
+2. Система карт и геоданных
+3. Карточки мест и moderation система
+4. Система бронирования и оплаты
+5. Поиск и фильтрация
+6. Административная панель
+7. Документация и тестирование
+
+## Оценка времени
+
+- Фаза 1: 2-3 недели
+- Фаза 2: 4-6 недель
+- Фаза 3: 3-4 недели
+- Фаза 4: 2 недели
+- Фаза 5: 1 неделя
+
+**Общая оценка: 12-16 недель**
+
+## Риски и considerations
+
+- Геопространственные запросы могут быть resource-intensive
+- Модерация контента требует careful implementation
+- Мульти-провайдерская система оплаты
+- Поддержка разных картографических провайдеров по регионам
+
+Следующим шагом после подтверждения плана начну реализацию базовой инфраструктуры проекта.

+ 8 - 0
backend/.eslintignore

@@ -0,0 +1,8 @@
+node_modules/
+dist/
+*.config.js
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local

+ 30 - 0
backend/.eslintrc.json

@@ -0,0 +1,30 @@
+{
+  "env": {
+    "node": true,
+    "es2021": true
+  },
+  "extends": [
+    "eslint:recommended",
+    "@typescript-eslint/recommended"
+  ],
+  "parser": "@typescript-eslint/parser",
+  "parserOptions": {
+    "ecmaVersion": "latest",
+    "sourceType": "module"
+  },
+  "plugins": [
+    "@typescript-eslint"
+  ],
+  "rules": {
+    "@typescript-eslint/no-unused-vars": "error",
+    "@typescript-eslint/explicit-function-return-type": "off",
+    "@typescript-eslint/explicit-module-boundary-types": "off",
+    "@typescript-eslint/no-explicit-any": "warn",
+    "no-console": "warn",
+    "quotes": ["error", "single"],
+    "semi": ["error", "always"],
+    "indent": ["error", 2],
+    "comma-dangle": ["error", "never"]
+  },
+  "ignorePatterns": ["dist/", "node_modules/"]
+}

+ 6 - 0
backend/.prettierignore

@@ -0,0 +1,6 @@
+node_modules/
+dist/
+package-lock.json
+yarn.lock
+*.md
+*.json

+ 9 - 0
backend/.prettierrc.json

@@ -0,0 +1,9 @@
+{
+  "semi": true,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "none",
+  "printWidth": 100,
+  "arrowParens": "avoid",
+  "endOfLine": "auto"
+}

+ 46 - 0
backend/env.example

@@ -0,0 +1,46 @@
+# Конфигурация базы данных PostgreSQL
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=photoplaces
+DB_USER=photoplaces_user
+DB_PASSWORD=photoplaces_password
+
+# Настройки сервера
+PORT=5000
+NODE_ENV=development
+
+# JWT секретный ключ для аутентификации
+JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
+JWT_EXPIRES_IN=7d
+
+# Настройки CORS для фронтенда
+CORS_ORIGIN=http://localhost:3000
+
+# Настройки загрузки файлов
+MAX_FILE_SIZE=10485760
+UPLOAD_PATH=./uploads
+
+# Настройки почты (для уведомлений)
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_USER=your_email@gmail.com
+SMTP_PASS=your_app_password
+
+# Платежные системы (будет настроено позже)
+# YOOKASSA_SHOP_ID=your_shop_id
+# YOOKASSA_SECRET_KEY=your_secret_key
+# STRIPE_SECRET_KEY=your_stripe_secret_key
+
+# Картографические сервисы (будет настроено позже)
+# MAP_TILE_PROVIDER=your_tile_provider_url
+# MAP_ATTRIBUTION=Map data © contributors
+
+# В продакшене обязательно:
+# - Изменить все пароли и секретные ключи
+# - Использовать переменные окружения вместо хардкода
+# - Настроить правильные CORS origins
+# - Использовать SSL/TLS
+
+# Важно: Этот файл env.example используется как шаблон.
+# Создайте файл .env и заполните реальными значениями.
+# НИКОГДА не коммитьте файл .env в репозиторий!

+ 50 - 0
backend/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "photoplaces-backend",
+  "version": "1.0.0",
+  "description": "Backend API для сервиса поиска мест для фотосессий",
+  "main": "dist/index.js",
+  "scripts": {
+    "dev": "ts-node-dev src/index.ts",
+    "build": "tsc",
+    "start": "node dist/index.js",
+    "lint": "eslint src/**/*.ts",
+    "lint:fix": "eslint src/**/*.ts --fix",
+    "test": "jest"
+  },
+  "keywords": ["photo", "places", "map", "api"],
+  "author": "PhotoPlaces Team",
+  "license": "MIT",
+  "dependencies": {
+    "express": "^4.18.2",
+    "cors": "^2.8.5",
+    "helmet": "^7.0.0",
+    "express-rate-limit": "^6.7.0",
+    "jsonwebtoken": "^9.0.1",
+    "bcryptjs": "^2.4.3",
+    "pg": "^8.11.0",
+    "pg-postgis": "^0.1.0",
+    "dotenv": "^16.3.1",
+    "joi": "^17.9.2",
+    "multer": "^1.4.5-lts.1",
+    "swagger-ui-express": "^4.6.2",
+    "yamljs": "^0.3.0"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.17",
+    "@types/cors": "^2.8.13",
+    "@types/jsonwebtoken": "^9.0.2",
+    "@types/bcryptjs": "^2.4.2",
+    "@types/pg": "^8.6.6",
+    "@types/multer": "^1.4.7",
+    "@types/node": "^20.4.2",
+    "typescript": "^5.1.6",
+    "ts-node-dev": "^2.0.0",
+    "@typescript-eslint/eslint-plugin": "^6.1.0",
+    "@typescript-eslint/parser": "^6.1.0",
+    "eslint": "^8.45.0",
+    "prettier": "^3.0.0",
+    "jest": "^29.6.1",
+    "@types/jest": "^29.5.3",
+    "ts-jest": "^29.1.1"
+  }
+}

+ 84 - 0
backend/src/config/server.ts

@@ -0,0 +1,84 @@
+import dotenv from 'dotenv';
+
+// Загрузка переменных окружения
+dotenv.config();
+
+// Интерфейс конфигурации сервера
+interface ServerConfig {
+  port: number;
+  nodeEnv: string;
+  corsOrigin: string;
+  jwtSecret: string;
+  jwtExpiresIn: string;
+  database: {
+    host: string;
+    port: number;
+    name: string;
+    user: string;
+    password: string;
+  };
+  rateLimit: {
+    windowMs: number;
+    max: number;
+  };
+  upload: {
+    maxFileSize: number;
+    path: string;
+  };
+}
+
+// Конфигурация сервера
+export const config: ServerConfig = {
+  port: parseInt(process.env.PORT || '5000'),
+  nodeEnv: process.env.NODE_ENV || 'development',
+  corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000',
+  jwtSecret: process.env.JWT_SECRET || 'your_fallback_jwt_secret_change_in_production',
+  jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
+  
+  database: {
+    host: process.env.DB_HOST || 'localhost',
+    port: parseInt(process.env.DB_PORT || '5432'),
+    name: process.env.DB_NAME || 'photoplaces',
+    user: process.env.DB_USER || 'photoplaces_user',
+    password: process.env.DB_PASSWORD || 'photoplaces_password'
+  },
+  
+  rateLimit: {
+    windowMs: 15 * 60 * 1000, // 15 минут
+    max: 100 // максимум 100 запросов с одного IP
+  },
+  
+  upload: {
+    maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'), // 10MB
+    path: process.env.UPLOAD_PATH || './uploads'
+  }
+};
+
+// Валидация конфигурации
+const validateConfig = (): void => {
+  const requiredEnvVars = [
+    'JWT_SECRET'
+  ];
+  
+  const missingVars = requiredEnvVars.filter(varName => 
+    !process.env[varName] || process.env[varName] === 'your_fallback_value'
+  );
+  
+  if (missingVars.length > 0 && config.nodeEnv === 'production') {
+    throw new Error(`Отсутствуют обязательные переменные окружения: ${missingVars.join(', ')}`);
+  }
+  
+  if (config.nodeEnv === 'production') {
+    console.warn('⚠️  Запуск в production режиме. Убедитесь, что все настройки корректны.');
+  }
+};
+
+// Выполнение валидации при импорте
+validateConfig();
+
+// Вспомогательные функции конфигурации
+export const isProduction = (): boolean => config.nodeEnv === 'production';
+export const isDevelopment = (): boolean => config.nodeEnv === 'development';
+
+// Экспорт конфигурации для использования в других модулях
+export default config;

+ 80 - 0
backend/src/index.ts

@@ -0,0 +1,80 @@
+import express from 'express';
+import cors from 'cors';
+import helmet from 'helmet';
+import rateLimit from 'express-rate-limit';
+import dotenv from 'dotenv';
+
+// Загрузка переменных окружения
+dotenv.config();
+
+// Импорт конфигурации
+import { config } from '@/config/server';
+
+// Импорт middleware
+import { errorHandler } from '@/middleware/errorHandler';
+import { requestLogger } from '@/middleware/logger';
+
+// Импорт роутов
+import { healthRouter } from '@/routes/health';
+
+// Создание Express приложения
+const app = express();
+
+// Базовые middleware
+app.use(helmet()); // Защита заголовков
+app.use(cors({
+  origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
+  credentials: true
+}));
+
+// Лимитер запросов
+const limiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 минут
+  max: 100, // максимум 100 запросов с одного IP
+  message: 'Слишком много запросов с этого IP, попробуйте позже.'
+});
+app.use(limiter);
+
+// Парсинг JSON
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true }));
+
+// Логирование запросов
+app.use(requestLogger);
+
+// Основные роуты
+app.use('/api/health', healthRouter);
+
+// Обработка 404
+app.use('*', (req, res) => {
+  res.status(404).json({
+    success: false,
+    message: 'Маршрут не найден',
+    path: req.originalUrl
+  });
+});
+
+// Обработчик ошибок
+app.use(errorHandler);
+
+// Запуск сервера
+const PORT = process.env.PORT || 5000;
+
+app.listen(PORT, () => {
+  console.log(`🚀 Сервер запущен на порту ${PORT}`);
+  console.log(`📊 Окружение: ${process.env.NODE_ENV || 'development'}`);
+  console.log(`🌐 CORS разрешен для: ${process.env.CORS_ORIGIN || 'http://localhost:3000'}`);
+});
+
+// Обработка graceful shutdown
+process.on('SIGINT', () => {
+  console.log('\n🛑 Получен SIGINT. Останавливаем сервер...');
+  process.exit(0);
+});
+
+process.on('SIGTERM', () => {
+  console.log('\n🛑 Получен SIGTERM. Останавливаем сервер...');
+  process.exit(0);
+});
+
+export default app;

+ 99 - 0
backend/src/middleware/errorHandler.ts

@@ -0,0 +1,99 @@
+import { Request, Response, NextFunction } from 'express';
+
+// Интерфейс для ошибок приложения
+interface AppError extends Error {
+  statusCode?: number;
+  isOperational?: boolean;
+}
+
+// Класс для обработки ошибок приложения
+export class AppError extends Error {
+  public statusCode: number;
+  public isOperational: boolean;
+
+  constructor(message: string, statusCode: number = 500) {
+    super(message);
+    this.statusCode = statusCode;
+    this.isOperational = true;
+
+    Error.captureStackTrace(this, this.constructor);
+  }
+}
+
+// Middleware для обработки ошибок
+export const errorHandler = (
+  err: AppError,
+  req: Request,
+  res: Response,
+  next: NextFunction
+): void => {
+  let error = { ...err };
+  error.message = err.message;
+
+  // Логирование ошибки в development
+  if (process.env.NODE_ENV === 'development') {
+    console.error('❌ Ошибка:', {
+      message: err.message,
+      stack: err.stack,
+      url: req.originalUrl,
+      method: req.method,
+      ip: req.ip,
+      body: req.body,
+      query: req.query
+    });
+  }
+
+  // Обработка ошибок Mongoose (если будем использовать MongoDB)
+  // if (err.name === 'CastError') {
+  //   const message = 'Ресурс не найден';
+  //   error = new AppError(message, 404);
+  // }
+
+  // Обработка ошибок валидации
+  if (err.name === 'ValidationError') {
+    const message = 'Некорректные данные';
+    error = new AppError(message, 400);
+  }
+
+  // Обработка ошибок JWT
+  if (err.name === 'JsonWebTokenError') {
+    const message = 'Неверный токен';
+    error = new AppError(message, 401);
+  }
+
+  if (err.name === 'TokenExpiredError') {
+    const message = 'Токен истек';
+    error = new AppError(message, 401);
+  }
+
+  // Обработка ошибок базы данных
+  if (err.name === 'DatabaseError') {
+    const message = 'Ошибка базы данных';
+    error = new AppError(message, 500);
+  }
+
+  // Ответ клиенту
+  res.status(error.statusCode || 500).json({
+    success: false,
+    message: error.message || 'Внутренняя ошибка сервера',
+    ...(process.env.NODE_ENV === 'development' && { stack: error.stack })
+  });
+};
+
+// Middleware для обработки 404
+export const notFoundHandler = (req: Request, res: Response, next: NextFunction): void => {
+  const error = new AppError(`Маршрут не найден - ${req.originalUrl}`, 404);
+  next(error);
+};
+
+// Функция для создания ошибок
+export const createError = (message: string, statusCode: number = 500): AppError => {
+  return new AppError(message, statusCode);
+};
+
+// Асинхронная обработка ошибок для async/await
+export const catchAsync = (fn: Function) => {
+  return (req: Request, res: Response, next: NextFunction) => {
+    fn(req, res, next).catch(next);
+  };
+};

+ 118 - 0
backend/src/middleware/logger.ts

@@ -0,0 +1,118 @@
+import { Request, Response, NextFunction } from 'express';
+
+// Цвета для консоли
+const colors = {
+  reset: '\x1b[0m',
+  bright: '\x1b[1m',
+  dim: '\x1b[2m',
+  red: '\x1b[31m',
+  green: '\x1b[32m',
+  yellow: '\x1b[33m',
+  blue: '\x1b[34m',
+  magenta: '\x1b[35m',
+  cyan: '\x1b[36m'
+};
+
+// Получение цвета для статус кода
+const getStatusColor = (status: number): string => {
+  if (status >= 500) return colors.red;
+  if (status >= 400) return colors.yellow;
+  if (status >= 300) return colors.cyan;
+  if (status >= 200) return colors.green;
+  return colors.dim;
+};
+
+// Получение цвета для метода
+const getMethodColor = (method: string): string => {
+  switch (method) {
+    case 'GET': return colors.green;
+    case 'POST': return colors.blue;
+    case 'PUT': return colors.yellow;
+    case 'DELETE': return colors.red;
+    case 'PATCH': return colors.magenta;
+    default: return colors.dim;
+  }
+};
+
+// Форматирование времени
+const formatTime = (ms: number): string => {
+  if (ms < 10) return `${ms}ms`;
+  if (ms < 1000) return `${colors.green}${ms}ms${colors.reset}`;
+  if (ms < 5000) return `${colors.yellow}${ms}ms${colors.reset}`;
+  return `${colors.red}${ms}ms${colors.reset}`;
+};
+
+// Middleware для логирования запросов
+export const requestLogger = (req: Request, res: Response, next: NextFunction): void => {
+  const start = Date.now();
+  
+  // Пропускаем health check запросы
+  if (req.path === '/api/health') {
+    return next();
+  }
+
+  // Логирование входящего запроса
+  if (process.env.NODE_ENV === 'development') {
+    console.log(
+      `${colors.dim}${new Date().toISOString()}${colors.reset} ` +
+      `${getMethodColor(req.method)}${req.method.padEnd(6)}${colors.reset} ` +
+      `${colors.bright}${req.path}${colors.reset}` +
+      (Object.keys(req.query).length > 0 ? ` ${colors.dim}query: ${JSON.stringify(req.query)}${colors.reset}` : '') +
+      (Object.keys(req.body).length > 0 ? ` ${colors.dim}body: ${JSON.stringify(req.body)}${colors.reset}` : '')
+    );
+  }
+
+  // Логирование ответа
+  res.on('finish', () => {
+    const duration = Date.now() - start;
+    const statusColor = getStatusColor(res.statusCode);
+    
+    console.log(
+      `${colors.dim}${new Date().toISOString()}${colors.reset} ` +
+      `${getMethodColor(req.method)}${req.method.padEnd(6)}${colors.reset} ` +
+      `${statusColor}${res.statusCode}${colors.reset} ` +
+      `${colors.bright}${req.path}${colors.reset} ` +
+      `${formatTime(duration)} ` +
+      `${colors.dim}${res.get('Content-Length') || 0}b${colors.reset}`
+    );
+  });
+
+  next();
+};
+
+// Функция для логирования ошибок
+export const errorLogger = (error: Error, context?: string): void => {
+  console.error(
+    `${colors.red}${new Date().toISOString()} [ERROR]${colors.reset} ` +
+    `${context ? `[${context}] ` : ''}` +
+    `${error.message}${colors.reset}` +
+    (error.stack ? `\n${colors.dim}${error.stack}${colors.reset}` : '')
+  );
+};
+
+// Функция для логирования информации
+export const infoLogger = (message: string, context?: string): void => {
+  console.log(
+    `${colors.blue}${new Date().toISOString()} [INFO]${colors.reset} ` +
+    `${context ? `[${context}] ` : ''}` +
+    `${message}${colors.reset}`
+  );
+};
+
+// Функция для логирования предупреждений
+export const warnLogger = (message: string, context?: string): void => {
+  console.warn(
+    `${colors.yellow}${new Date().toISOString()} [WARN]${colors.reset} ` +
+    `${context ? `[${context}] ` : ''}` +
+    `${message}${colors.reset}`
+  );
+};
+
+// Функция для логирования успешных операций
+export const successLogger = (message: string, context?: string): void => {
+  console.log(
+    `${colors.green}${new Date().toISOString()} [SUCCESS]${colors.reset} ` +
+    `${context ? `[${context}] ` : ''}` +
+    `${message}${colors.reset}`
+  );
+};

+ 109 - 0
backend/src/routes/health.ts

@@ -0,0 +1,109 @@
+import { Router } from 'express';
+import { catchAsync } from '@/middleware/errorHandler';
+
+const router = Router();
+
+// Health check endpoint
+router.get(
+  '/',
+  catchAsync(async (req, res) => {
+    // Здесь можно добавить проверки:
+    // - Подключение к базе данных
+    // - Доступность внешних сервисов
+    // - Использование памяти/CPU
+    
+    const healthCheck = {
+      status: 'OK',
+      timestamp: new Date().toISOString(),
+      uptime: process.uptime(),
+      environment: process.env.NODE_ENV || 'development',
+      version: process.env.npm_package_version || '1.0.0',
+      services: {
+        database: 'connected', // Заглушка, будет реализовано позже
+        // storage: 'connected',
+        // cache: 'connected'
+      }
+    };
+
+    res.status(200).json({
+      success: true,
+      data: healthCheck,
+      message: 'Сервер работает нормально'
+    });
+  })
+);
+
+// Detailed health check with more information
+router.get(
+  '/detailed',
+  catchAsync(async (req, res) => {
+    const detailedHealth = {
+      status: 'OK',
+      timestamp: new Date().toISOString(),
+      uptime: process.uptime(),
+      environment: process.env.NODE_ENV || 'development',
+      nodeVersion: process.version,
+      memory: {
+        rss: `${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)}MB`,
+        heapTotal: `${(process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2)}MB`,
+        heapUsed: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`,
+        external: `${(process.memoryUsage().external / 1024 / 1024).toFixed(2)}MB`
+      },
+      cpu: {
+        usage: process.cpuUsage()
+      },
+      platform: process.platform,
+      arch: process.arch
+    };
+
+    res.status(200).json({
+      success: true,
+      data: detailedHealth,
+      message: 'Детальная информация о состоянии сервера'
+    });
+  })
+);
+
+// Endpoint для проверки готовности сервера (используется в Docker/K8s)
+router.get(
+  '/readiness',
+  catchAsync(async (req, res) => {
+    // Здесь можно добавить реальные проверки готовности
+    // Например, проверка подключения к БД
+    
+    const readiness = {
+      ready: true,
+      timestamp: new Date().toISOString(),
+      checks: [
+        {
+          name: 'database',
+          status: 'healthy',
+          message: 'Database connection established'
+        },
+        {
+          name: 'api',
+          status: 'healthy',
+          message: 'API is responding'
+        }
+      ]
+    };
+
+    res.status(200).json(readiness);
+  })
+);
+
+// Endpoint для проверки жизнеспособности (liveness probe)
+router.get(
+  '/liveness',
+  catchAsync(async (req, res) => {
+    const liveness = {
+      alive: true,
+      timestamp: new Date().toISOString(),
+      uptime: process.uptime()
+    };
+
+    res.status(200).json(liveness);
+  })
+);
+
+export { router as healthRouter };

+ 46 - 0
backend/tsconfig.json

@@ -0,0 +1,46 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "commonjs",
+    "lib": ["ES2020"],
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "removeComments": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "noImplicitThis": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "moduleResolution": "node",
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"],
+      "@/config/*": ["src/config/*"],
+      "@/controllers/*": ["src/controllers/*"],
+      "@/middleware/*": ["src/middleware/*"],
+      "@/models/*": ["src/models/*"],
+      "@/routes/*": ["src/routes/*"],
+      "@/utils/*": ["src/utils/*"]
+    },
+    "allowSyntheticDefaultImports": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist",
+    "**/*.test.ts"
+  ]
+}

+ 100 - 0
database/README.md

@@ -0,0 +1,100 @@
+# Настройка базы данных PhotoPlaces
+
+## Требования
+- Docker и Docker Compose
+- PostgreSQL 15+ с PostGIS 3.3+
+
+## Быстрый старт с Docker
+
+1. Убедитесь, что установлен Docker и Docker Compose
+2. Перейдите в директорию `database/`
+3. Запустите контейнер:
+   ```bash
+   docker-compose up -d
+   ```
+
+4. База данных будет доступна по адресу:
+   - Хост: `localhost`
+   - Порт: `5432`
+   - База: `photoplaces`
+   - Пользователь: `photoplaces_user`
+   - Пароль: `photoplaces_password`
+
+## Ручная установка PostgreSQL с PostGIS
+
+### Для Windows:
+1. Скачайте и установите PostgreSQL с официального сайта
+2. Установите расширение PostGIS через Stack Builder
+3. Создайте базу данных:
+   ```sql
+   CREATE DATABASE photoplaces;
+   ```
+
+4. Подключитесь к базе и выполните:
+   ```sql
+   CREATE EXTENSION postgis;
+   ```
+
+5. Выполните SQL скрипт инициализации:
+   ```bash
+   psql -U postgres -d photoplaces -f init.sql
+   ```
+
+### Для Linux (Ubuntu/Debian):
+```bash
+# Установка PostgreSQL и PostGIS
+sudo apt update
+sudo apt install postgresql postgresql-contrib postgis
+
+# Создание базы данных
+sudo -u postgres createdb photoplaces
+
+# Включение PostGIS
+sudo -u postgres psql -d photoplaces -c "CREATE EXTENSION postgis;"
+
+# Выполнение скрипта инициализации
+sudo -u postgres psql -d photoplaces -f init.sql
+```
+
+## Структура базы данных
+
+### Основные таблицы:
+- `users` - Пользователи системы с ролями
+- `places` - Места для фотосессий с геоданными
+- `place_images` - Изображения мест
+- `services` - Услуги фотографов
+- `bookings` - Бронирования
+
+### Геоданные:
+- Используется тип `GEOGRAPHY(Point, 4326)` для хранения координат
+- Координаты хранятся в формате WGS84 (широта/долгота)
+- Для работы с геоданными используется расширение PostGIS
+
+## Миграции
+
+Для управления миграциями базы данных будет использоваться:
+- `node-pg-migrate` или подобный инструмент
+- Миграции будут храниться в `database/migrations/`
+
+## Резервное копирование
+
+```bash
+# Создание бэкапа
+pg_dump -U photoplaces_user -d photoplaces -f backup.sql
+
+# Восстановление из бэкапа
+psql -U photoplaces_user -d photoplaces -f backup.sql
+```
+
+## Мониторинг и оптимизация
+
+- Используйте `EXPLAIN ANALYZE` для анализа запросов
+- Регулярно делайте `VACUUM ANALYZE` для обслуживания БД
+- Настройте мониторинг через pgAdmin или аналогичные инструменты
+
+## Безопасность
+
+- Никогда не используйте дефолтные пароли в продакшене
+- Настройте SSL соединение с базой данных
+- Ограничьте доступ к БД только с trusted источников
+- Регулярно обновляйте PostgreSQL и PostGIS

+ 24 - 0
database/docker-compose.yml

@@ -0,0 +1,24 @@
+version: '3.8'
+
+services:
+  postgres:
+    image: postgis/postgis:15-3.3
+    container_name: photoplaces-db
+    environment:
+      POSTGRES_DB: photoplaces
+      POSTGRES_USER: photoplaces_user
+      POSTGRES_PASSWORD: photoplaces_password
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U photoplaces_user -d photoplaces"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+
+volumes:
+  postgres_data:

+ 108 - 0
database/init.sql

@@ -0,0 +1,108 @@
+-- Инициализация базы данных PhotoPlaces
+-- Создание расширения PostGIS для работы с геоданными
+CREATE EXTENSION IF NOT EXISTS postgis;
+
+-- Создание таблицы пользователей
+CREATE TABLE users (
+    id SERIAL PRIMARY KEY,
+    email VARCHAR(255) UNIQUE NOT NULL,
+    password_hash VARCHAR(255) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'moderator', 'landlord', 'performer', 'customer', 'guest')),
+    is_verified BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Создание таблицы мест для фотосессий
+CREATE TABLE places (
+    id SERIAL PRIMARY KEY,
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    location GEOGRAPHY(Point, 4326) NOT NULL,
+    address TEXT,
+    type VARCHAR(20) NOT NULL CHECK (type IN ('place', 'studio')),
+    owner_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
+    is_approved BOOLEAN DEFAULT FALSE,
+    rating DECIMAL(3,2) DEFAULT 0.00,
+    price_per_hour DECIMAL(10,2),
+    max_capacity INTEGER,
+    amenities JSONB,
+    tags VARCHAR(255)[],
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Создание таблицы изображений мест
+CREATE TABLE place_images (
+    id SERIAL PRIMARY KEY,
+    place_id INTEGER REFERENCES places(id) ON DELETE CASCADE,
+    image_url VARCHAR(500) NOT NULL,
+    is_primary BOOLEAN DEFAULT FALSE,
+    uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Создание таблицы услуг фотографов
+CREATE TABLE services (
+    id SERIAL PRIMARY KEY,
+    performer_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    price DECIMAL(10,2) NOT NULL,
+    duration_hours INTEGER,
+    style_tags VARCHAR(255)[],
+    rating DECIMAL(3,2) DEFAULT 0.00,
+    is_available BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Создание таблицы бронирований
+CREATE TABLE bookings (
+    id SERIAL PRIMARY KEY,
+    place_id INTEGER REFERENCES places(id) ON DELETE CASCADE,
+    customer_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
+    start_time TIMESTAMP WITH TIME ZONE NOT NULL,
+    end_time TIMESTAMP WITH TIME ZONE NOT NULL,
+    total_price DECIMAL(10,2) NOT NULL,
+    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled', 'completed')),
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Создание индексов для оптимизации запросов
+CREATE INDEX idx_places_location ON places USING GIST(location);
+CREATE INDEX idx_places_type ON places(type);
+CREATE INDEX idx_places_approved ON places(is_approved);
+CREATE INDEX idx_users_role ON users(role);
+CREATE INDEX idx_bookings_dates ON bookings(start_time, end_time);
+CREATE INDEX idx_services_performer ON services(performer_id);
+
+-- Комментарии к таблицам для документации
+COMMENT ON TABLE users IS 'Таблица пользователей системы с различными ролями';
+COMMENT ON TABLE places IS 'Таблица мест для фотосессий с геометрическими координатами';
+COMMENT ON TABLE place_images IS 'Таблица изображений для мест';
+COMMENT ON TABLE services IS 'Таблица услуг фотографов';
+COMMENT ON TABLE bookings IS 'Таблица бронирований мест';
+
+-- Создание триггера для автоматического обновления updated_at
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+    NEW.updated_at = CURRENT_TIMESTAMP;
+    RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_places_updated_at BEFORE UPDATE ON places
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_services_updated_at BEFORE UPDATE ON services
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_bookings_updated_at BEFORE UPDATE ON bookings
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

+ 86 - 0
database/migrations/000-migration-guide.md

@@ -0,0 +1,86 @@
+# Руководство по миграциям базы данных
+
+## Структура миграций
+
+Миграции хранятся в директории `database/migrations/` и нумеруются в порядке выполнения:
+- `001-initial-schema.sql` - Основная схема базы данных
+- `002-seed-data.sql` - Тестовые данные для разработки
+- `003-...` - Последующие миграции
+
+## Правила создания миграций
+
+1. **Нумерация**: Каждая миграция должна начинаться с трехзначного номера
+2. **Имена файлов**: Используйте описательные имена в snake_case
+3. **Комментарии**: Добавляйте комментарии к каждой миграции
+4. **Транзакции**: Все изменения должны быть обернуты в BEGIN/COMMIT
+5. **Обратная совместимость**: Миграции должны быть обратимыми
+
+## Порядок выполнения миграций
+
+Миграции выполняются в порядке их нумерации. Система миграций должна:
+
+1. Создать таблицу `migrations` для отслеживания выполненных миграций
+2. Выполнять миграции по порядку
+3. Записывать выполненные миграции в таблицу `migrations`
+4. Предоставлять возможность отката миграций
+
+## Пример структуры таблицы миграций
+
+```sql
+CREATE TABLE migrations (
+    id SERIAL PRIMARY KEY,
+    name VARCHAR(255) NOT NULL,
+    executed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+## Инструменты для управления миграциями
+
+Рекомендуемые инструменты:
+- **node-pg-migrate** - для Node.js приложений
+- **Flyway** - Java-based миграции
+- **Liquibase** - кроссплатформенное решение
+- **Собственный скрипт** - простой bash/Python скрипт
+
+## Рекомендации по разработке
+
+1. **Тестирование**: Всегда тестируйте миграции на dev окружении
+2. **Резервное копирование**: Делайте бэкап перед выполнением миграций
+3. **Версионирование**: Храните миграции в системе контроля версий
+4. **Документация**: Описывайте изменения в комментариях
+5. **Обратные миграции**: Создавайте скрипты для отката изменений
+
+## Безопасность
+
+- Никогда не храните чувствительные данные в миграциях
+- Используйте переменные окружения для конфиденциальной информации
+- Проверяйте миграции на наличие SQL инъекций
+
+## Пример использования с node-pg-migrate
+
+1. Установите node-pg-migrate:
+   ```bash
+   npm install -g node-pg-migrate
+   ```
+
+2. Создайте новую миграцию:
+   ```bash
+   node-pg-migrate create add-new-feature
+   ```
+
+3. Выполните миграции:
+   ```bash
+   node-pg-migrate up
+   ```
+
+4. Откатите миграции:
+   ```bash
+   node-pg-migrate down
+   ```
+
+## Важные замечания
+
+- Миграции должны быть идемпотентными (можно выполнять multiple times)
+- Избегайте DROP операций в миграциях
+- Всегда добавляйте соответствующие индексы
+- Тестируйте производительность миграций на больших объемах данных

+ 130 - 0
database/migrations/001-initial-schema.sql

@@ -0,0 +1,130 @@
+-- Миграция 001: Инициализация основной схемы базы данных
+-- Автор: PhotoPlaces Team
+-- Дата: 2025-12-04
+
+BEGIN;
+
+-- Создание расширения PostGIS для работы с геоданными
+CREATE EXTENSION IF NOT EXISTS postgis;
+
+-- Таблица пользователей системы
+CREATE TABLE users (
+    id SERIAL PRIMARY KEY,
+    email VARCHAR(255) UNIQUE NOT NULL,
+    password_hash VARCHAR(255) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'moderator', 'landlord', 'performer', 'customer', 'guest')),
+    is_verified BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE users IS 'Таблица пользователей системы с различными ролями';
+COMMENT ON COLUMN users.role IS 'Роль пользователя: superadmin, moderator, landlord, performer, customer, guest';
+
+-- Таблица мест для фотосессий
+CREATE TABLE places (
+    id SERIAL PRIMARY KEY,
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    location GEOGRAPHY(Point, 4326) NOT NULL,
+    address TEXT,
+    type VARCHAR(20) NOT NULL CHECK (type IN ('place', 'studio')),
+    owner_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
+    is_approved BOOLEAN DEFAULT FALSE,
+    rating DECIMAL(3,2) DEFAULT 0.00,
+    price_per_hour DECIMAL(10,2),
+    max_capacity INTEGER,
+    amenities JSONB,
+    tags VARCHAR(255)[],
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE places IS 'Таблица мест для фотосессий с геометрическими координатами';
+COMMENT ON COLUMN places.location IS 'Географические координаты места в формате WGS84';
+COMMENT ON COLUMN places.type IS 'Тип места: place (место) или studio (студия)';
+
+-- Таблица изображений мест
+CREATE TABLE place_images (
+    id SERIAL PRIMARY KEY,
+    place_id INTEGER REFERENCES places(id) ON DELETE CASCADE,
+    image_url VARCHAR(500) NOT NULL,
+    is_primary BOOLEAN DEFAULT FALSE,
+    uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE place_images IS 'Таблица изображений для мест';
+
+-- Таблица услуг фотографов
+CREATE TABLE services (
+    id SERIAL PRIMARY KEY,
+    performer_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    price DECIMAL(10,2) NOT NULL,
+    duration_hours INTEGER,
+    style_tags VARCHAR(255)[],
+    rating DECIMAL(3,2) DEFAULT 0.00,
+    is_available BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE services IS 'Таблица услуг фотографов';
+
+-- Таблица бронирований
+CREATE TABLE bookings (
+    id SERIAL PRIMARY KEY,
+    place_id INTEGER REFERENCES places(id) ON DELETE CASCADE,
+    customer_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
+    start_time TIMESTAMP WITH TIME ZONE NOT NULL,
+    end_time TIMESTAMP WITH TIME ZONE NOT NULL,
+    total_price DECIMAL(10,2) NOT NULL,
+    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled', 'completed')),
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE bookings IS 'Таблица бронирований мест';
+
+-- Создание индексов для оптимизации запросов
+CREATE INDEX idx_places_location ON places USING GIST(location);
+CREATE INDEX idx_places_type ON places(type);
+CREATE INDEX idx_places_approved ON places(is_approved);
+CREATE INDEX idx_users_role ON users(role);
+CREATE INDEX idx_bookings_dates ON bookings(start_time, end_time);
+CREATE INDEX idx_services_performer ON services(performer_id);
+CREATE INDEX idx_place_images_place ON place_images(place_id);
+
+-- Функция для автоматического обновления updated_at
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+    NEW.updated_at = CURRENT_TIMESTAMP;
+    RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Триггеры для автоматического обновления updated_at
+CREATE TRIGGER update_users_updated_at 
+    BEFORE UPDATE ON users
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_places_updated_at 
+    BEFORE UPDATE ON places
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_services_updated_at 
+    BEFORE UPDATE ON services
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_bookings_updated_at 
+    BEFORE UPDATE ON bookings
+    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+COMMIT;
+
+-- Комментарий к миграции
+COMMENT ON MIGRATION 001 IS 'Инициализация основной схемы базы данных с таблицами пользователей, мест, услуг и бронирований';

+ 47 - 0
database/migrations/002-seed-data.sql

@@ -0,0 +1,47 @@
+-- Миграция 002: Тестовые данные для разработки
+-- Автор: PhotoPlaces Team
+-- Дата: 2025-12-04
+-- Важно: Эти данные только для разработки! Не использовать в продакшене.
+
+BEGIN;
+
+-- Создание тестовых пользователей с хешированными паролями (пароль: 'password123')
+INSERT INTO users (email, password_hash, first_name, last_name, role, is_verified) VALUES
+('superadmin@photoplaces.ru', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Иван', 'Петров', 'superadmin', TRUE),
+('moderator@photoplaces.ru', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Мария', 'Сидорова', 'moderator', TRUE),
+('landlord@studio.ru', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Алексей', 'Кузнецов', 'landlord', TRUE),
+('performer@photo.ru', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Екатерина', 'Орлова', 'performer', TRUE),
+('customer@mail.ru', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Дмитрий', 'Васильев', 'customer', TRUE);
+
+-- Создание тестовых мест для фотосессий (Москва)
+INSERT INTO places (title, description, location, address, type, owner_id, is_approved, rating, price_per_hour, max_capacity, amenities, tags) VALUES
+('Студия "Свет и Тень"', 'Просторная фотостудия с естественным светом и профессиональным оборудованием', ST_GeographyFromText('POINT(37.6173 55.7558)'), 'ул. Тверская, 25, Москва', 'studio', 3, TRUE, 4.8, 2500.00, 10, '{"backdrops": ["белый", "черный", "зеленый"], "lighting": ["софтбоксы", "вспышки"], "equipment": ["штативы", "рефлекторы"]}', '{"портрет", "предметка", "профессиональная"}'),
+('Парк Горького - Аллея роз', 'Красивая аллея с розами, идеальное место для романтических фотосессий', ST_GeographyFromText('POINT(37.6056 55.7287)'), 'Крымский Вал, 9, Москва', 'place', 5, TRUE, 4.7, NULL, NULL, '{"natural_light": true, "scenery": ["цветы", "аллея", "парк"]}', '{"романтика", "природа", "лето"}'),
+('Лофт "Красный Октябрь"', 'Стильный лофт с индустриальным интерьером на территории бывшей фабрики', ST_GeographyFromText('POINT(37.6115 55.7410)'), 'Берсеневская наб., 6, Москва', 'studio', 3, TRUE, 4.9, 3500.00, 15, '{"interior": ["кирпичные стены", "бетон", "металл"], "lighting": ["естественный свет", "студийный свет"], "props": ["мебель винтаж", "индустриальные элементы"]}', '{"лофт", "индустриальный", "урбан"}'),
+('ВДНХ - Фонтаны', 'Величественные фонтаны ВДНХ, отличное место для архитектурных и портретных съемок', ST_GeographyFromText('POINT(37.6325 55.8276)'), 'просп. Мира, 119, Москва', 'place', 5, FALSE, 4.5, NULL, NULL, '{"water_features": true, "architecture": ["советская", "монументальная"], "crowd_level": "medium"}', '{"архитектура", "история", "фонтан"}');
+
+-- Тестовые изображения для мест
+INSERT INTO place_images (place_id, image_url, is_primary) VALUES
+(1, '/images/studios/studio1-1.jpg', TRUE),
+(1, '/images/studios/studio1-2.jpg', FALSE),
+(2, '/images/places/park1-1.jpg', TRUE),
+(3, '/images/studios/loft1-1.jpg', TRUE),
+(3, '/images/studios/loft1-2.jpg', FALSE),
+(4, '/images/places/vdnh1-1.jpg', TRUE);
+
+-- Тестовые услуги фотографов
+INSERT INTO services (performer_id, title, description, price, duration_hours, style_tags, rating) VALUES
+(4, 'Портретная фотосессия', 'Профессиональная портретная съемка в студии или на локации', 5000.00, 2, '{"портрет", "студия", "профессиональный"}', 4.8),
+(4, 'Свадебная фотосессия', 'Полное сопровождение свадебного дня, репортажная и постановочная съемка', 25000.00, 8, '{"свадьба", "репортаж", "романтика"}', 4.9),
+(4, 'Предметная фотосъемка', 'Съемка товаров для интернет-магазинов и каталогов', 3000.00, 1, '{"предметка", "коммерческая", "e-commerce"}', 4.7);
+
+-- Тестовые бронирования
+INSERT INTO bookings (place_id, customer_id, start_time, end_time, total_price, status) VALUES
+(1, 5, '2025-12-10 10:00:00+03', '2025-12-10 12:00:00+03', 5000.00, 'confirmed'),
+(1, 5, '2025-12-12 14:00:00+03', '2025-12-12 16:00:00+03', 5000.00, 'pending'),
+(3, 5, '2025-12-15 11:00:00+03', '2025-12-15 14:00:00+03', 10500.00, 'confirmed');
+
+COMMIT;
+
+-- Комментарий к миграции
+COMMENT ON MIGRATION 002 IS 'Тестовые данные для разработки: пользователи, места, услуги и бронирования';

+ 8 - 0
frontend/.eslintignore

@@ -0,0 +1,8 @@
+node_modules/
+dist/
+*.config.js
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local

+ 44 - 0
frontend/.eslintrc.json

@@ -0,0 +1,44 @@
+{
+  "env": {
+    "browser": true,
+    "es2021": true
+  },
+  "extends": [
+    "eslint:recommended",
+    "@typescript-eslint/recommended",
+    "plugin:react/recommended",
+    "plugin:react-hooks/recommended"
+  ],
+  "parser": "@typescript-eslint/parser",
+  "parserOptions": {
+    "ecmaVersion": "latest",
+    "sourceType": "module",
+    "ecmaFeatures": {
+      "jsx": true
+    }
+  },
+  "plugins": [
+    "react",
+    "react-hooks",
+    "@typescript-eslint"
+  ],
+  "rules": {
+    "react/react-in-jsx-scope": "off",
+    "react/prop-types": "off",
+    "@typescript-eslint/no-unused-vars": "error",
+    "@typescript-eslint/explicit-function-return-type": "off",
+    "@typescript-eslint/explicit-module-boundary-types": "off",
+    "@typescript-eslint/no-explicit-any": "warn",
+    "no-console": "warn",
+    "quotes": ["error", "single"],
+    "semi": ["error", "always"],
+    "indent": ["error", 2],
+    "comma-dangle": ["error", "never"]
+  },
+  "settings": {
+    "react": {
+      "version": "detect"
+    }
+  },
+  "ignorePatterns": ["dist/", "node_modules/"]
+}

+ 6 - 0
frontend/.prettierignore

@@ -0,0 +1,6 @@
+node_modules/
+dist/
+package-lock.json
+yarn.lock
+*.md
+*.json

+ 9 - 0
frontend/.prettierrc.json

@@ -0,0 +1,9 @@
+{
+  "semi": true,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "none",
+  "printWidth": 100,
+  "arrowParens": "avoid",
+  "endOfLine": "auto"
+}

+ 47 - 0
frontend/package.json

@@ -0,0 +1,47 @@
+{
+  "name": "photoplaces-frontend",
+  "version": "1.0.0",
+  "description": "Frontend приложение для сервиса поиска мест для фотосессий",
+  "main": "src/index.tsx",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview",
+    "lint": "eslint src/**/*.{ts,tsx}",
+    "lint:fix": "eslint src/**/*.{ts,tsx} --fix"
+  },
+  "keywords": ["react", "typescript", "map", "photo", "places"],
+  "author": "PhotoPlaces Team",
+  "license": "MIT",
+  "dependencies": {
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-router-dom": "^6.14.1",
+    "leaflet": "^1.9.4",
+    "react-leaflet": "^4.2.1",
+    "axios": "^1.4.0",
+    "react-query": "^3.39.3",
+    "react-hook-form": "^7.45.4",
+    "react-hot-toast": "^2.4.1",
+    "lucide-react": "^0.263.1",
+    "tailwindcss": "^3.3.3",
+    "@headlessui/react": "^1.7.17"
+  },
+  "devDependencies": {
+    "@types/react": "^18.2.15",
+    "@types/react-dom": "^18.2.7",
+    "@types/leaflet": "^1.9.4",
+    "@vitejs/plugin-react": "^4.0.3",
+    "vite": "^4.4.5",
+    "typescript": "^5.1.6",
+    "@typescript-eslint/eslint-plugin": "^6.1.0",
+    "@typescript-eslint/parser": "^6.1.0",
+    "eslint": "^8.45.0",
+    "eslint-plugin-react": "^7.33.0",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "prettier": "^3.0.0",
+    "tailwindcss": "^3.3.3",
+    "autoprefixer": "^10.4.14",
+    "postcss": "^8.4.27"
+  }
+}

+ 31 - 0
frontend/tsconfig.json

@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"],
+      "@/components/*": ["src/components/*"],
+      "@/pages/*": ["src/pages/*"],
+      "@/hooks/*": ["src/hooks/*"],
+      "@/utils/*": ["src/utils/*"],
+      "@/types/*": ["src/types/*"],
+      "@/services/*": ["src/services/*"]
+    }
+  },
+  "include": ["src"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
frontend/tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 32 - 0
frontend/vite.config.ts

@@ -0,0 +1,32 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+  plugins: [react()],
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './src'),
+      '@/components': path.resolve(__dirname, './src/components'),
+      '@/pages': path.resolve(__dirname, './src/pages'),
+      '@/hooks': path.resolve(__dirname, './src/hooks'),
+      '@/utils': path.resolve(__dirname, './src/utils'),
+      '@/types': path.resolve(__dirname, './src/types'),
+      '@/services': path.resolve(__dirname, './src/services')
+    }
+  },
+  server: {
+    port: 3000,
+    open: true,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:5000',
+        changeOrigin: true
+      }
+    }
+  },
+  build: {
+    outDir: 'dist',
+    sourcemap: true
+  }
+})