1029 lines
32 KiB
Markdown
1029 lines
32 KiB
Markdown
# Документация изменений 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<MarketingAnalysisRequest>({
|
|
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 (
|
|
<form onSubmit={handleSubmit}>
|
|
{/* Поле: Целевая аудитория */}
|
|
<div>
|
|
<label>Целевая аудитория *</label>
|
|
|
|
<div>
|
|
<label>Гендер:</label>
|
|
{VALID_GENDERS.map((gender) => (
|
|
<label key={gender}>
|
|
<input
|
|
type='checkbox'
|
|
checked={
|
|
formData.targetAudience.genders?.includes(gender) || false
|
|
}
|
|
onChange={(e) => {
|
|
const genders = formData.targetAudience.genders || [];
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
genders: [...genders, gender],
|
|
},
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
genders: genders.filter((g) => g !== gender),
|
|
},
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
{gender}
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<label>Возраст:</label>
|
|
{VALID_AGE_RANGES.map((age) => (
|
|
<label key={age}>
|
|
<input
|
|
type='checkbox'
|
|
checked={
|
|
formData.targetAudience.ageRanges?.includes(age) || false
|
|
}
|
|
onChange={(e) => {
|
|
const ageRanges = formData.targetAudience.ageRanges || [];
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
ageRanges: [...ageRanges, age],
|
|
},
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
ageRanges: ageRanges.filter((a) => a !== age),
|
|
},
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
{age}
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<label>Тип:</label>
|
|
{VALID_TYPES.map((type) => (
|
|
<label key={type}>
|
|
<input
|
|
type='checkbox'
|
|
checked={formData.targetAudience.types?.includes(type) || false}
|
|
onChange={(e) => {
|
|
const types = formData.targetAudience.types || [];
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
types: [...types, type],
|
|
},
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
targetAudience: {
|
|
...formData.targetAudience,
|
|
types: types.filter((t) => t !== type),
|
|
},
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
{type}
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
{/* Отображение выбранных значений */}
|
|
<div>
|
|
<strong>Выбрано:</strong>
|
|
{formData.targetAudience.genders &&
|
|
formData.targetAudience.genders.length > 0 && (
|
|
<span> Гендер: {formData.targetAudience.genders.join(', ')}</span>
|
|
)}
|
|
{formData.targetAudience.ageRanges &&
|
|
formData.targetAudience.ageRanges.length > 0 && (
|
|
<span>
|
|
{' '}
|
|
Возраст: {formData.targetAudience.ageRanges.join(', ')}
|
|
</span>
|
|
)}
|
|
{formData.targetAudience.types &&
|
|
formData.targetAudience.types.length > 0 && (
|
|
<span> Тип: {formData.targetAudience.types.join(', ')}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Поле: Регион */}
|
|
<div>
|
|
<label>Регион *</label>
|
|
<div>
|
|
<label>
|
|
<input
|
|
type='checkbox'
|
|
checked={formData.region.length === VALID_REGIONS.length}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
region: [...VALID_REGIONS],
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
region: [],
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
Все регионы
|
|
</label>
|
|
</div>
|
|
<div>
|
|
{VALID_REGIONS.map((region) => (
|
|
<label key={region}>
|
|
<input
|
|
type='checkbox'
|
|
checked={formData.region.includes(region)}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
region: [...formData.region, region],
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
region: formData.region.filter((r) => r !== region),
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
{region}
|
|
</label>
|
|
))}
|
|
</div>
|
|
{/* Отображение выбранных регионов */}
|
|
<div>
|
|
<strong>Выбрано регионов: {formData.region.length}</strong>
|
|
{formData.region.length > 0 && (
|
|
<div>{formData.region.join(', ')}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Поле: Тип анализа */}
|
|
<div>
|
|
<label>Тип анализа *</label>
|
|
<div>
|
|
<label>
|
|
<input
|
|
type='checkbox'
|
|
checked={
|
|
formData.analysisType.length === VALID_ANALYSIS_TYPES.length
|
|
}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
analysisType: [...VALID_ANALYSIS_TYPES],
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
analysisType: [],
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
Все типы анализа
|
|
</label>
|
|
</div>
|
|
<div>
|
|
{VALID_ANALYSIS_TYPES.map((type) => (
|
|
<label key={type}>
|
|
<input
|
|
type='checkbox'
|
|
checked={formData.analysisType.includes(type)}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
setFormData({
|
|
...formData,
|
|
analysisType: [...formData.analysisType, type],
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
analysisType: formData.analysisType.filter(
|
|
(t) => t !== type
|
|
),
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
{type === 'РЫНОК' && ' Рынок'}
|
|
{type === 'КОНКУРЕНТЫ' && ' Конкуренты'}
|
|
{type === 'ЦА' && ' Целевая аудитория'}
|
|
{type === 'КАНАЛЫ' && ' Каналы'}
|
|
{type === 'SWOT' && ' SWOT'}
|
|
</label>
|
|
))}
|
|
</div>
|
|
{/* Отображение выбранных типов */}
|
|
<div>
|
|
<strong>Выбрано типов: {formData.analysisType.length}</strong>
|
|
{formData.analysisType.length > 0 && (
|
|
<div>{formData.analysisType.join(', ')}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Остальные поля формы */}
|
|
{/* ... */}
|
|
|
|
<button type='submit'>Создать анализ</button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
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: Обновить компоненты формы
|
|
|
|
- Замените `<textarea>` для целевой аудитории на чекбоксы
|
|
- Замените `<select>` для региона на множественный выбор (чекбоксы)
|
|
- Замените `<select>` для типа анализа на множественный выбор (чекбоксы)
|
|
|
|
### Шаг 3: Обновить обработку ответа
|
|
|
|
Добавьте проверку на массив в ответе API:
|
|
|
|
```typescript
|
|
// Старый код
|
|
const analysisId = response.data.analysisId;
|
|
navigate(`/analysis/${analysisId}`);
|
|
|
|
// Новый код
|
|
const data = response.data;
|
|
if (Array.isArray(data)) {
|
|
// Обработка множественных анализов
|
|
const analysisIds = data.map((item) => item.analysisId);
|
|
navigate(`/analyses?ids=${analysisIds.join(',')}`);
|
|
} else {
|
|
// Обработка одного анализа
|
|
navigate(`/analysis/${data.analysisId}`);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Важные замечания
|
|
|
|
1. **Обратная совместимость**: Старый формат данных больше не поддерживается. Все запросы должны использовать новый формат.
|
|
|
|
2. **Множественные анализы**: При выборе нескольких типов анализа создаются отдельные записи. Каждый анализ обрабатывается независимо и имеет свой статус.
|
|
|
|
3. **Валидация**: Обязательно проверяйте на фронтенде, что выбрана хотя бы одна опция в каждом обязательном поле.
|
|
|
|
4. **Отображение выбранных значений**: Рекомендуется показывать пользователю все выбранные значения в читаемом формате.
|
|
|
|
5. **UX рекомендации**:
|
|
|
|
- Добавьте опцию "Выбрать все" для регионов и типов анализа
|
|
- Показывайте количество выбранных элементов
|
|
- Используйте чекбоксы вместо select для множественного выбора
|
|
|
|
6. **Обработка ошибок**: API вернет ошибку валидации, если:
|
|
- Не выбрана ни одна опция в целевую аудиторию
|
|
- Не выбран ни один регион
|
|
- Не выбран ни один тип анализа
|
|
- Использованы недопустимые значения
|
|
|
|
---
|
|
|
|
## 12. Примеры ошибок валидации
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"message": "Ошибка валидации",
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "Ошибка валидации входных данных",
|
|
"details": {
|
|
"targetAudience": "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types",
|
|
"region": "Поле 'region' обязательно для заполнения. Выберите хотя бы один регион",
|
|
"analysisType": "Поле 'analysisType' обязательно для заполнения. Выберите хотя бы один тип анализа"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Константы для использования
|
|
|
|
```typescript
|
|
// Константы для целевой аудитории
|
|
export const VALID_GENDERS = ['Женщины', 'Мужчины'] as const;
|
|
export const VALID_AGE_RANGES = [
|
|
'20-40',
|
|
'25-45',
|
|
'18-25',
|
|
'40-60',
|
|
'60+',
|
|
] as const;
|
|
export const VALID_AUDIENCE_TYPES = [
|
|
'Семьи',
|
|
'Молодёжь',
|
|
'Все подряд',
|
|
] as const;
|
|
|
|
// Константы для типов анализа
|
|
export const VALID_ANALYSIS_TYPES = [
|
|
'РЫНОК',
|
|
'КОНКУРЕНТЫ',
|
|
'ЦА',
|
|
'КАНАЛЫ',
|
|
'SWOT',
|
|
] as const;
|
|
|
|
// Константы для регионов
|
|
export const VALID_REGIONS = [
|
|
'Алматы',
|
|
'Астана',
|
|
'Шымкент',
|
|
'Караганда',
|
|
'Актобе',
|
|
'Тараз',
|
|
'Павлодар',
|
|
'Усть-Каменогорск',
|
|
'Семей',
|
|
'Костанай',
|
|
'Кызылорда',
|
|
'Уральск',
|
|
'Петропавловск',
|
|
'Атырау',
|
|
'Актау',
|
|
'Туркестан',
|
|
'Кокшетау',
|
|
'Талдыкорган',
|
|
'Экибастуз',
|
|
'Рудный',
|
|
] as const;
|
|
|
|
// Маппинг типов анализа для отображения
|
|
export const ANALYSIS_TYPE_LABELS: Record<string, string> = {
|
|
РЫНОК: 'Рынок',
|
|
КОНКУРЕНТЫ: 'Конкуренты',
|
|
ЦА: 'Целевая аудитория',
|
|
КАНАЛЫ: 'Каналы',
|
|
SWOT: 'SWOT',
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Заключение
|
|
|
|
Все изменения направлены на улучшение гибкости API и предоставление пользователям большего контроля над выбором параметров анализа. При правильной реализации эти изменения не должны вызвать проблем, но требуют обновления фронтенд кода.
|
|
|
|
Если у вас возникнут вопросы или проблемы при интеграции, обратитесь к команде бэкенда.
|