Files
marketing-parser/FRONTEND_API_CHANGES.md
T
2025-12-03 19:33:49 +05:00

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 и предоставление пользователям большего контроля над выбором параметров анализа. При правильной реализации эти изменения не должны вызвать проблем, но требуют обновления фронтенд кода.
Если у вас возникнут вопросы или проблемы при интеграции, обратитесь к команде бэкенда.