diff --git a/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md b/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md new file mode 100644 index 0000000..e201db1 --- /dev/null +++ b/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md @@ -0,0 +1,548 @@ +# Документация для фронтенда: Генерация изображений для постов + +## Обзор изменений + +В систему добавлена автоматическая генерация изображений для постов в социальных сетях. Теперь при создании маркетинговой стратегии для каждого поста автоматически генерируется уникальное изображение с помощью OpenAI DALL-E API. Изображения сохраняются в MinIO и прикрепляются к постам при публикации в Facebook. + +--- + +## Новые поля в API + +### 1. PostCalendarItem (Календарь постов) + +В объекте `PostCalendarItem` добавлены два новых поля для работы с изображениями: + +| Поле | Тип | Описание | Обязательное | +| --------------- | ------ | ------------------------------------------------------------ | ------------ | +| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет | +| `imageFilename` | string | Имя файла изображения | Нет | + +**Важно:** + +- Поля могут быть `null`, если генерация изображения не удалась +- В этом случае пост публикуется без изображения +- Оба поля содержат одинаковое значение (имя файла) +- Для получения изображения используйте API эндпоинт `/api/marketing/analysis/images/{imageFilename}` + +### 2. PostingTask (Задачи публикации) + +В объекте `PostingTask` также добавлены поля для изображений: + +| Поле | Тип | Описание | Обязательное | +| --------------- | ------ | ------------------------------------------------------------ | ------------ | +| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет | +| `imageFilename` | string | Имя файла изображения | Нет | + +--- + +## Изменения в API эндпоинтах + +### GET `/api/marketing/strategy/{strategyId}` + +Ответ теперь включает поля изображений в каждом элементе календаря постов. + +#### Пример ответа + +```json +{ + "success": true, + "data": { + "strategyId": "507f1f77bcf86cd799439011", + "analysisId": "507f1f77bcf86cd799439012", + "status": "completed", + "strategy": { + "postCalendar": [ + { + "publishDate": "2024-01-15T10:00:00", + "platform": "Facebook", + "contentType": "пост", + "theme": "Презентация нового продукта", + "postText": "Мы рады представить наш новый продукт...", + "hashtags": ["#новинка", "#продукт", "#маркетинг"], + "publishTime": "10:00", + "imageUrl": "post_image_1705312800000_1234567890.png", + "imageFilename": "post_image_1705312800000_1234567890.png" + } + ] + } + } +} +``` + +### GET `/api/marketing/analysis/{analysisId}/strategy` + +Аналогично, ответ включает поля изображений в календаре постов. + +--- + +## Получение изображений + +### Через бэкенд API + +Все изображения должны получаться через бэкенд API. Прямой доступ к MinIO с фронтенда не предусмотрен. + +#### Эндпоинт для получения изображения + +**GET** `/api/marketing/analysis/images/{imageFilename}` + +Возвращает изображение поста в формате PNG. + +#### Параметры пути + +| Параметр | Тип | Описание | +| --------------- | ------ | --------------------- | +| `imageFilename` | string | Имя файла изображения | + +#### Заголовки запроса + +| Заголовок | Тип | Обязательный | Описание | +| --------------- | ------ | ------------ | ------------------------------------ | +| `Authorization` | string | Да | JWT токен в формате `Bearer {token}` | + +#### Пример запроса + +```javascript +const imageUrl = `/api/marketing/analysis/images/${post.imageFilename}`; + +fetch(imageUrl, { + headers: { + Authorization: `Bearer ${token}` + } +}) + .then((response) => { + if (response.ok) { + return response.blob(); + } + throw new Error('Failed to load image'); + }) + .then((blob) => { + const imageObjectUrl = URL.createObjectURL(blob); + // Используйте imageObjectUrl для отображения + }); +``` + +#### Пример ответа + +- **Успешный ответ (200 OK):** + + - Content-Type: `image/png` + - Body: бинарные данные изображения PNG + +- **Ошибка 401 Unauthorized:** + + - Токен отсутствует или невалиден + +- **Ошибка 404 Not Found:** + + - Изображение не найдено на сервере + +- **Ошибка 500 Internal Server Error:** + - Внутренняя ошибка сервера при загрузке изображения + +#### Кэширование + +Сервер возвращает заголовок `Cache-Control: public, max-age=3600`, что позволяет браузеру кэшировать изображения на 1 час. + +--- + +## Рекомендации по отображению + +### 1. Проверка наличия изображения + +Всегда проверяйте наличие изображения перед отображением: + +```javascript +// Пример на JavaScript/TypeScript +const PostCard = ({ post, apiBaseUrl, authToken }) => { + const hasImage = post.imageUrl && post.imageFilename; + const imageUrl = hasImage ? `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}` : null; + + return ( +
+

{post.theme}

+

{post.postText}

+ + {imageUrl ? ( + {post.theme} { + // Fallback если изображение не загрузилось + e.target.style.display = 'none'; + }} + // Если требуется авторизация, используйте fetch с заголовками + /> + ) : ( +
Изображение не сгенерировано
+ )} + +
+ {post.hashtags.map((tag) => ( + #{tag} + ))} +
+
+ ); +}; +``` + +### 2. Обработка ошибок загрузки + +Всегда предусматривайте fallback для случаев, когда: + +- Изображение не сгенерировано (`imageUrl` = `null`) +- Изображение не найдено на сервере +- Ошибка при загрузке изображения + +```javascript +const [imageError, setImageError] = useState(false); + +const getImageUrl = (imageFilename) => { + if (!imageFilename) return null; + return `${API_BASE_URL}/api/marketing/analysis/images/${imageFilename}`; +}; + +const handleImageError = () => { + setImageError(true); +}; + +return ( + <> + {post.imageUrl && !imageError ? ( + {post.theme} + ) : ( +
+ + Изображение недоступно +
+ )} + +); +``` + +### 3. Оптимизация загрузки + +Рекомендуется использовать lazy loading для изображений: + +```javascript +{post.theme} +``` + +### 4. Размеры изображений + +Изображения генерируются в размере **1024x1024 пикселей** (квадратные). При отображении учитывайте это при настройке CSS: + +```css +.post-image { + width: 100%; + max-width: 512px; + height: auto; + border-radius: 8px; + object-fit: cover; +} +``` + +--- + +## Примеры использования + +### React компонент для отображения поста + +```typescript +import React, { useState } from 'react'; + +interface PostCalendarItem { + publishDate: string; + platform: string; + contentType: string; + theme: string; + postText: string; + hashtags: string[]; + publishTime: string; + imageUrl?: string | null; + imageFilename?: string | null; +} + +interface PostCardProps { + post: PostCalendarItem; + apiBaseUrl: string; +} + +const PostCard: React.FC = ({ post, apiBaseUrl }) => { + const [imageError, setImageError] = useState(false); + + const getImageUrl = (): string | null => { + if (!post.imageFilename) return null; + return `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}`; + }; + + const imageUrl = getImageUrl(); + + return ( +
+
+ {post.platform} + {post.contentType} + {post.publishTime} +
+ +

{post.theme}

+ + {imageUrl && !imageError ? ( +
+ {post.theme} setImageError(true)} + /> +
+ ) : ( +
+ + + +

Изображение не сгенерировано

+
+ )} + +

{post.postText}

+ +
+ {post.hashtags.map((tag, index) => ( + + {tag.startsWith('#') ? tag : `#${tag}`} + + ))} +
+ +
+ + {new Date(post.publishDate).toLocaleDateString('ru-RU')} + +
+
+ ); +}; + +export default PostCard; +``` + +### Vue компонент + +```vue + + + +``` + +--- + +## Важные замечания + +### 1. Генерация изображений + +- Изображения генерируются **автоматически** при создании стратегии +- Процесс генерации может занять время (обычно 10-30 секунд на изображение) +- Если генерация не удалась, пост все равно будет создан, но без изображения + +### 2. Хранение изображений + +- Все изображения хранятся на бэкенде +- Формат изображений: **PNG** +- Размер изображений: **1024x1024 пикселей** +- Имя файла уникально для каждого поста +- Доступ к изображениям только через API эндпоинт + +### 3. Публикация постов + +- При публикации поста в Facebook изображение автоматически прикрепляется +- Если изображение отсутствует, пост публикуется только с текстом +- Это не влияет на успешность публикации + +### 4. Обратная совместимость + +- Старые стратегии, созданные до добавления этой функции, не будут иметь изображений +- Поля `imageUrl` и `imageFilename` будут `null` для таких постов +- Фронтенд должен корректно обрабатывать `null` значения + +--- + +## Конфигурация + +Для работы с изображениями вам понадобится базовый URL вашего API: + +```typescript +// config.ts +export const API_CONFIG = { + baseUrl: 'http://your-backend-url' // URL вашего бэкенда + // Например: 'http://localhost:8080' или 'https://api.example.com' +}; +``` + +--- + +## Миграция существующего кода + +Если у вас уже есть компоненты для отображения постов, обновите их следующим образом: + +1. **Добавьте проверку наличия изображения:** + + ```typescript + const hasImage = post.imageUrl && post.imageFilename; + ``` + +2. **Добавьте отображение изображения:** + + ```jsx + { + hasImage && {post.theme}; + } + ``` + +3. **Обновите типы/интерфейсы:** + ```typescript + interface PostCalendarItem { + // ... существующие поля + imageUrl?: string | null; + imageFilename?: string | null; + } + ``` + +--- + +## Поддержка + +При возникновении проблем: + +1. Проверьте, что `imageUrl` и `imageFilename` не `null` +2. Убедитесь, что используете правильный API эндпоинт: `/api/marketing/analysis/images/{imageFilename}` +3. Проверьте, что JWT токен валиден и передается в заголовке `Authorization` +4. Проверьте консоль браузера на наличие ошибок сети или авторизации +5. Убедитесь, что используете правильный базовый URL API + +--- + +## Пример полного ответа API + +```json +{ + "success": true, + "data": { + "strategyId": "507f1f77bcf86cd799439011", + "analysisId": "507f1f77bcf86cd799439012", + "status": "completed", + "createdAt": "2024-01-15T10:00:00", + "completedAt": "2024-01-15T10:05:00", + "durationWeeks": 4, + "priorityPlatforms": ["Facebook", "Instagram"], + "strategy": { + "weeklyPlans": [ + { + "weekNumber": 1, + "mainThemes": ["Презентация продукта", "Преимущества"], + "contentRecommendations": "Создавайте контент...", + "priorityPlatforms": ["Facebook"] + } + ], + "postCalendar": [ + { + "publishDate": "2024-01-16T10:00:00", + "platform": "Facebook", + "contentType": "пост", + "theme": "Презентация нового продукта", + "postText": "Мы рады представить наш новый продукт, который поможет вам...", + "hashtags": ["#новинка", "#продукт", "#маркетинг"], + "publishTime": "10:00", + "imageUrl": "post_image_1705312800000_1234567890.png", + "imageFilename": "post_image_1705312800000_1234567890.png" + }, + { + "publishDate": "2024-01-18T14:00:00", + "platform": "Instagram", + "contentType": "сторис", + "theme": "Преимущества продукта", + "postText": "Узнайте о главных преимуществах нашего продукта...", + "hashtags": ["#преимущества", "#качество"], + "publishTime": "14:00", + "imageUrl": null, + "imageFilename": null + } + ] + } + } +} +``` + +Обратите внимание, что второй пост не имеет изображения (`imageUrl` и `imageFilename` равны `null`). Это нормальная ситуация, если генерация изображения не удалась. + +--- + +**Дата обновления:** 2024-01-15 +**Версия API:** 1.0 diff --git a/docs/FRONTEND_API_CHANGES.md b/docs/FRONTEND_API_CHANGES.md new file mode 100644 index 0000000..56f923a --- /dev/null +++ b/docs/FRONTEND_API_CHANGES.md @@ -0,0 +1,1028 @@ +# Документация изменений API для фронтенда + +## Обзор изменений + +В API маркетингового анализа были внесены изменения для поддержки множественного выбора в трех полях: + +1. **Целевая аудитория** (`targetAudience`) - теперь структурированный JSON объект вместо строки +2. **Тип анализа** (`analysisType`) - теперь массив строк вместо одной строки +3. **Регион** (`region`) - теперь массив строк вместо одной строки + +**Важно**: При выборе нескольких типов анализа API создает отдельные записи анализа для каждого типа. В ответе возвращается массив анализов. + +--- + +## 1. Изменения в поле `targetAudience` + +### Старый формат (устарел) + +```typescript +targetAudience: string; +``` + +```json +"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет" +``` + +### Новый формат + +```typescript +targetAudience: { + genders?: ('Женщины' | 'Мужчины')[]; + ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[]; + types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[]; +} +``` + +### Примеры + +**Минимальный выбор (только гендер):** + +```json +{ + "targetAudience": { + "genders": ["Женщины"] + } +} +``` + +**Полный выбор:** + +```json +{ + "targetAudience": { + "genders": ["Женщины", "Мужчины"], + "ageRanges": ["20-40", "25-45"], + "types": ["Семьи", "Молодёжь"] + } +} +``` + +**Только возраст и тип:** + +```json +{ + "targetAudience": { + "ageRanges": ["25-45"], + "types": ["Молодёжь"] + } +} +``` + +### Валидация + +- Хотя бы одно поле (`genders`, `ageRanges`, `types`) должно быть заполнено +- Каждое поле должно содержать непустой массив +- Все значения должны быть из допустимого списка + +### Допустимые значения + +- **genders**: `"Женщины"`, `"Мужчины"` +- **ageRanges**: `"20-40"`, `"25-45"`, `"18-25"`, `"40-60"`, `"60+"` +- **types**: `"Семьи"`, `"Молодёжь"`, `"Все подряд"` + +--- + +## 2. Изменения в поле `analysisType` + +### Старый формат (устарел) + +```typescript +analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT'; +``` + +```json +"analysisType": "РЫНОК" +``` + +### Новый формат + +```typescript +analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[] +``` + +### Примеры + +**Один тип анализа:** + +```json +{ + "analysisType": ["РЫНОК"] +} +``` + +**Несколько типов анализа:** + +```json +{ + "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"] +} +``` + +**Все типы анализа:** + +```json +{ + "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"] +} +``` + +### Важно: Поведение при множественном выборе + +При выборе нескольких типов анализа: + +- API создает **отдельную запись анализа** для каждого типа +- Каждый анализ обрабатывается независимо +- В ответе возвращается **массив** с информацией о каждом созданном анализе +- Каждый анализ имеет свой `analysisId` и статус + +### Валидация + +- Массив должен содержать минимум один элемент +- Все значения должны быть из допустимого списка + +### Допустимые значения + +- `"РЫНОК"` - Анализ рынка +- `"КОНКУРЕНТЫ"` - Анализ конкурентов +- `"ЦА"` - Анализ целевой аудитории +- `"КАНАЛЫ"` - Анализ маркетинговых каналов +- `"SWOT"` - SWOT-анализ + +--- + +## 3. Изменения в поле `region` + +### Старый формат (устарел) + +```typescript +region: string; +``` + +```json +"region": "Алматы" +``` + +### Новый формат + +```typescript +region: string[] +``` + +### Примеры + +**Один регион:** + +```json +{ + "region": ["Алматы"] +} +``` + +**Несколько регионов:** + +```json +{ + "region": ["Алматы", "Астана", "Шымкент"] +} +``` + +**Все регионы:** + +```json +{ + "region": [ + "Алматы", + "Астана", + "Шымкент", + "Караганда", + "Актобе", + "Тараз", + "Павлодар", + "Усть-Каменогорск", + "Семей", + "Костанай", + "Кызылорда", + "Уральск", + "Петропавловск", + "Атырау", + "Актау", + "Туркестан", + "Кокшетау", + "Талдыкорган", + "Экибастуз", + "Рудный" + ] +} +``` + +### Валидация + +- Массив должен содержать минимум один элемент +- Все значения должны быть из допустимого списка городов Казахстана + +### Допустимые значения + +Всего 20 городов: + +- `"Алматы"`, `"Астана"`, `"Шымкент"`, `"Караганда"`, `"Актобе"`, `"Тараз"`, `"Павлодар"`, `"Усть-Каменогорск"`, `"Семей"`, `"Костанай"`, `"Кызылорда"`, `"Уральск"`, `"Петропавловск"`, `"Атырау"`, `"Актау"`, `"Туркестан"`, `"Кокшетау"`, `"Талдыкорган"`, `"Экибастуз"`, `"Рудный"` + +--- + +## 4. Изменения в ответе API + +### Эндпоинт: `POST /api/marketing/analysis/start` + +### Старый формат ответа (один анализ) + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "507f1f77bcf86cd799439011", + "status": "processing", + "estimatedCompletion": "2024-01-15T10:30:00", + "message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут." + } +} +``` + +### Новый формат ответа + +**Если выбран один тип анализа:** + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "507f1f77bcf86cd799439011", + "status": "processing", + "estimatedCompletion": "2024-01-15T10:30:00", + "message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут." + } +} +``` + +**Если выбрано несколько типов анализа:** + +```json +{ + "success": true, + "message": "Создано анализов: 3. Все анализы запущены успешно.", + "data": [ + { + "analysisId": "507f1f77bcf86cd799439011", + "status": "processing", + "estimatedCompletion": "2024-01-15T10:30:00", + "message": "Анализ типа 'РЫНОК' запущен успешно. Результаты будут готовы в течение 5-10 минут." + }, + { + "analysisId": "507f1f77bcf86cd799439012", + "status": "processing", + "estimatedCompletion": "2024-01-15T10:30:00", + "message": "Анализ типа 'КОНКУРЕНТЫ' запущен успешно. Результаты будут готовы в течение 5-10 минут." + }, + { + "analysisId": "507f1f77bcf86cd799439013", + "status": "processing", + "estimatedCompletion": "2024-01-15T10:30:00", + "message": "Анализ типа 'ЦА' запущен успешно. Результаты будут готовы в течение 5-10 минут." + } + ] +} +``` + +--- + +## 5. Полный пример запроса + +### Пример 1: Один тип анализа + +```json +{ + "businessNiche": "E-commerce платформы", + "product": "Разработка мобильных приложений", + "targetAudience": { + "genders": ["Женщины"], + "ageRanges": ["25-45"], + "types": ["Молодёжь"] + }, + "region": ["Алматы"], + "goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев", + "detailLevel": "СТАНДАРТНО", + "strongSide": "Опытная команда, быстрая доставка", + "weakSide": "Ограниченный маркетинговый бюджет", + "analysisType": ["РЫНОК"] +} +``` + +### Пример 2: Несколько типов анализа + +```json +{ + "businessNiche": "E-commerce платформы", + "product": "Разработка мобильных приложений", + "targetAudience": { + "genders": ["Женщины", "Мужчины"], + "ageRanges": ["25-45", "40-60"], + "types": ["Семьи", "Молодёжь"] + }, + "region": ["Алматы", "Астана", "Шымкент"], + "goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев", + "detailLevel": "СТАНДАРТНО", + "strongSide": "Опытная команда, быстрая доставка", + "weakSide": "Ограниченный маркетинговый бюджет", + "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"] +} +``` + +--- + +## 6. TypeScript интерфейсы + +```typescript +// Интерфейс для целевой аудитории +interface TargetAudience { + genders?: ('Женщины' | 'Мужчины')[]; + ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[]; + types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[]; +} + +// Интерфейс запроса +interface MarketingAnalysisRequest { + businessNiche: string; + product: string; + targetAudience: TargetAudience; + region: string[]; + goal: string; + detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО'; + strongSide?: string; + weakSide?: string; + analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[]; +} + +// Интерфейс ответа +interface MarketingAnalysisResponse { + analysisId: string; + status: string; + estimatedCompletion: string; + message: string; +} + +// Тип ответа API (может быть один объект или массив) +type StartAnalysisResponse = + | MarketingAnalysisResponse + | MarketingAnalysisResponse[]; +``` + +--- + +## 7. Примеры реализации на фронтенде + +### React компонент с формой + +```tsx +import React, { useState } from 'react'; + +const VALID_GENDERS = ['Женщины', 'Мужчины'] as const; +const VALID_AGE_RANGES = ['20-40', '25-45', '18-25', '40-60', '60+'] as const; +const VALID_TYPES = ['Семьи', 'Молодёжь', 'Все подряд'] as const; +const VALID_ANALYSIS_TYPES = [ + 'РЫНОК', + 'КОНКУРЕНТЫ', + 'ЦА', + 'КАНАЛЫ', + 'SWOT', +] as const; +const VALID_REGIONS = [ + 'Алматы', + 'Астана', + 'Шымкент', + 'Караганда', + 'Актобе', + 'Тараз', + 'Павлодар', + 'Усть-Каменогорск', + 'Семей', + 'Костанай', + 'Кызылорда', + 'Уральск', + 'Петропавловск', + 'Атырау', + 'Актау', + 'Туркестан', + 'Кокшетау', + 'Талдыкорган', + 'Экибастуз', + 'Рудный', +] as const; + +const MarketingAnalysisForm: React.FC = () => { + const [formData, setFormData] = useState({ + businessNiche: '', + product: '', + targetAudience: { + genders: [], + ageRanges: [], + types: [], + }, + region: [], + goal: '', + detailLevel: 'СТАНДАРТНО', + strongSide: '', + weakSide: '', + analysisType: [], + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Валидация: проверяем, что выбрана хотя бы одна опция в каждом обязательном поле + if ( + formData.targetAudience.genders?.length === 0 && + formData.targetAudience.ageRanges?.length === 0 && + formData.targetAudience.types?.length === 0 + ) { + alert('Выберите хотя бы одну опцию для целевой аудитории'); + return; + } + + if (formData.region.length === 0) { + alert('Выберите хотя бы один регион'); + return; + } + + if (formData.analysisType.length === 0) { + alert('Выберите хотя бы один тип анализа'); + return; + } + + try { + const response = await fetch('/api/marketing/analysis/start', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getAuthToken()}`, + }, + body: JSON.stringify(formData), + }); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error?.message || 'Ошибка при создании анализа'); + } + + // Обработка ответа (может быть массив или один объект) + if (Array.isArray(result.data)) { + console.log(`Создано ${result.data.length} анализов`); + result.data.forEach( + (analysis: MarketingAnalysisResponse, index: number) => { + console.log(`Анализ ${index + 1}: ${analysis.analysisId}`); + // Перенаправление на страницу с результатами для каждого анализа + } + ); + } else { + console.log('Анализ создан:', result.data.analysisId); + // Перенаправление на страницу с результатами + } + } catch (error) { + console.error('Ошибка:', error); + alert('Не удалось создать анализ. Попробуйте позже.'); + } + }; + + return ( +
+ {/* Поле: Целевая аудитория */} +
+ + +
+ + {VALID_GENDERS.map((gender) => ( + + ))} +
+ +
+ + {VALID_AGE_RANGES.map((age) => ( + + ))} +
+ +
+ + {VALID_TYPES.map((type) => ( + + ))} +
+ + {/* Отображение выбранных значений */} +
+ Выбрано: + {formData.targetAudience.genders && + formData.targetAudience.genders.length > 0 && ( + Гендер: {formData.targetAudience.genders.join(', ')} + )} + {formData.targetAudience.ageRanges && + formData.targetAudience.ageRanges.length > 0 && ( + + {' '} + Возраст: {formData.targetAudience.ageRanges.join(', ')} + + )} + {formData.targetAudience.types && + formData.targetAudience.types.length > 0 && ( + Тип: {formData.targetAudience.types.join(', ')} + )} +
+
+ + {/* Поле: Регион */} +
+ +
+ +
+
+ {VALID_REGIONS.map((region) => ( + + ))} +
+ {/* Отображение выбранных регионов */} +
+ Выбрано регионов: {formData.region.length} + {formData.region.length > 0 && ( +
{formData.region.join(', ')}
+ )} +
+
+ + {/* Поле: Тип анализа */} +
+ +
+ +
+
+ {VALID_ANALYSIS_TYPES.map((type) => ( + + ))} +
+ {/* Отображение выбранных типов */} +
+ Выбрано типов: {formData.analysisType.length} + {formData.analysisType.length > 0 && ( +
{formData.analysisType.join(', ')}
+ )} +
+
+ + {/* Остальные поля формы */} + {/* ... */} + + +
+ ); +}; + +export default MarketingAnalysisForm; +``` + +--- + +## 8. Обработка ответа API + +```typescript +async function handleAnalysisResponse(response: any) { + if (!response.success) { + throw new Error(response.error?.message || 'Ошибка при создании анализа'); + } + + const data = response.data; + + // Проверяем, является ли ответ массивом + if (Array.isArray(data)) { + // Множественные анализы + console.log(`Создано ${data.length} анализов`); + + // Можно показать уведомление пользователю + showNotification( + `Создано ${data.length} анализов. Они будут обработаны параллельно.` + ); + + // Сохраняем все ID анализов + const analysisIds = data.map( + (item: MarketingAnalysisResponse) => item.analysisId + ); + + // Можно перенаправить на страницу со списком анализов + navigate(`/analyses?ids=${analysisIds.join(',')}`); + + // Или создать задачи для отслеживания статуса каждого анализа + data.forEach((analysis: MarketingAnalysisResponse) => { + startPollingAnalysisStatus(analysis.analysisId); + }); + } else { + // Один анализ + console.log('Анализ создан:', data.analysisId); + + // Перенаправление на страницу с результатами + navigate(`/analysis/${data.analysisId}`); + + // Запуск отслеживания статуса + startPollingAnalysisStatus(data.analysisId); + } +} +``` + +--- + +## 9. Валидация на фронтенде + +```typescript +function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): { + isValid: boolean; + errors: string[]; +} { + const errors: string[] = []; + + // Валидация целевой аудитории + const hasGenders = + data.targetAudience.genders && data.targetAudience.genders.length > 0; + const hasAgeRanges = + data.targetAudience.ageRanges && data.targetAudience.ageRanges.length > 0; + const hasTypes = + data.targetAudience.types && data.targetAudience.types.length > 0; + + if (!hasGenders && !hasAgeRanges && !hasTypes) { + errors.push( + 'Выберите хотя бы одну опцию для целевой аудитории (гендер, возраст или тип)' + ); + } + + // Валидация регионов + if (!data.region || data.region.length === 0) { + errors.push('Выберите хотя бы один регион'); + } + + // Валидация типов анализа + if (!data.analysisType || data.analysisType.length === 0) { + errors.push('Выберите хотя бы один тип анализа'); + } + + // Валидация других полей + if (!data.businessNiche || data.businessNiche.trim().length < 3) { + errors.push('Ниша бизнеса должна содержать минимум 3 символа'); + } + + if (!data.product || data.product.trim().length < 3) { + errors.push('Продукт/услуга должна содержать минимум 3 символа'); + } + + if (!data.goal || data.goal.trim().length < 10) { + errors.push('Цель должна содержать минимум 10 символов'); + } + + return { + isValid: errors.length === 0, + errors, + }; +} +``` + +--- + +## 10. Миграция существующего кода + +### Шаг 1: Обновить TypeScript интерфейсы + +Замените старые интерфейсы на новые: + +```typescript +// Старый код +interface MarketingAnalysisRequest { + targetAudience: string; + region: string; + analysisType: string; +} + +// Новый код +interface MarketingAnalysisRequest { + targetAudience: TargetAudience; + region: string[]; + analysisType: string[]; +} +``` + +### Шаг 2: Обновить компоненты формы + +- Замените `