Files
marketing/docs/FRONTEND_API_CHANGES.md
T
2025-12-05 20:00:11 +05:00

32 KiB

Документация изменений API для фронтенда

Обзор изменений

В API маркетингового анализа были внесены изменения для поддержки множественного выбора в трех полях:

  1. Целевая аудитория (targetAudience) - теперь структурированный JSON объект вместо строки
  2. Тип анализа (analysisType) - теперь массив строк вместо одной строки
  3. Регион (region) - теперь массив строк вместо одной строки

Важно: При выборе нескольких типов анализа API создает отдельные записи анализа для каждого типа. В ответе возвращается массив анализов.


1. Изменения в поле targetAudience

Старый формат (устарел)

targetAudience: string;
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"

Новый формат

targetAudience: {
  genders?: ('Женщины' | 'Мужчины')[];
  ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[];
  types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[];
}

Примеры

Минимальный выбор (только гендер):

{
  "targetAudience": {
    "genders": ["Женщины"]
  }
}

Полный выбор:

{
  "targetAudience": {
    "genders": ["Женщины", "Мужчины"],
    "ageRanges": ["20-40", "25-45"],
    "types": ["Семьи", "Молодёжь"]
  }
}

Только возраст и тип:

{
  "targetAudience": {
    "ageRanges": ["25-45"],
    "types": ["Молодёжь"]
  }
}

Валидация

  • Хотя бы одно поле (genders, ageRanges, types) должно быть заполнено
  • Каждое поле должно содержать непустой массив
  • Все значения должны быть из допустимого списка

Допустимые значения

  • genders: "Женщины", "Мужчины"
  • ageRanges: "20-40", "25-45", "18-25", "40-60", "60+"
  • types: "Семьи", "Молодёжь", "Все подряд"

2. Изменения в поле analysisType

Старый формат (устарел)

analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
"analysisType": "РЫНОК"

Новый формат

analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[]

Примеры

Один тип анализа:

{
  "analysisType": ["РЫНОК"]
}

Несколько типов анализа:

{
  "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
}

Все типы анализа:

{
  "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
}

Важно: Поведение при множественном выборе

При выборе нескольких типов анализа:

  • API создает отдельную запись анализа для каждого типа
  • Каждый анализ обрабатывается независимо
  • В ответе возвращается массив с информацией о каждом созданном анализе
  • Каждый анализ имеет свой analysisId и статус

Валидация

  • Массив должен содержать минимум один элемент
  • Все значения должны быть из допустимого списка

Допустимые значения

  • "РЫНОК" - Анализ рынка
  • "КОНКУРЕНТЫ" - Анализ конкурентов
  • "ЦА" - Анализ целевой аудитории
  • "КАНАЛЫ" - Анализ маркетинговых каналов
  • "SWOT" - SWOT-анализ

3. Изменения в поле region

Старый формат (устарел)

region: string;
"region": "Алматы"

Новый формат

region: string[]

Примеры

Один регион:

{
  "region": ["Алматы"]
}

Несколько регионов:

{
  "region": ["Алматы", "Астана", "Шымкент"]
}

Все регионы:

{
  "region": [
    "Алматы",
    "Астана",
    "Шымкент",
    "Караганда",
    "Актобе",
    "Тараз",
    "Павлодар",
    "Усть-Каменогорск",
    "Семей",
    "Костанай",
    "Кызылорда",
    "Уральск",
    "Петропавловск",
    "Атырау",
    "Актау",
    "Туркестан",
    "Кокшетау",
    "Талдыкорган",
    "Экибастуз",
    "Рудный"
  ]
}

Валидация

  • Массив должен содержать минимум один элемент
  • Все значения должны быть из допустимого списка городов Казахстана

Допустимые значения

Всего 20 городов:

  • "Алматы", "Астана", "Шымкент", "Караганда", "Актобе", "Тараз", "Павлодар", "Усть-Каменогорск", "Семей", "Костанай", "Кызылорда", "Уральск", "Петропавловск", "Атырау", "Актау", "Туркестан", "Кокшетау", "Талдыкорган", "Экибастуз", "Рудный"

4. Изменения в ответе API

Эндпоинт: POST /api/marketing/analysis/start

Старый формат ответа (один анализ)

{
  "success": true,
  "message": "Анализ запущен успешно",
  "data": {
    "analysisId": "507f1f77bcf86cd799439011",
    "status": "processing",
    "estimatedCompletion": "2024-01-15T10:30:00",
    "message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
  }
}

Новый формат ответа

Если выбран один тип анализа:

{
  "success": true,
  "message": "Анализ запущен успешно",
  "data": {
    "analysisId": "507f1f77bcf86cd799439011",
    "status": "processing",
    "estimatedCompletion": "2024-01-15T10:30:00",
    "message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
  }
}

Если выбрано несколько типов анализа:

{
  "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: Один тип анализа

{
  "businessNiche": "E-commerce платформы",
  "product": "Разработка мобильных приложений",
  "targetAudience": {
    "genders": ["Женщины"],
    "ageRanges": ["25-45"],
    "types": ["Молодёжь"]
  },
  "region": ["Алматы"],
  "goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
  "detailLevel": "СТАНДАРТНО",
  "strongSide": "Опытная команда, быстрая доставка",
  "weakSide": "Ограниченный маркетинговый бюджет",
  "analysisType": ["РЫНОК"]
}

Пример 2: Несколько типов анализа

{
  "businessNiche": "E-commerce платформы",
  "product": "Разработка мобильных приложений",
  "targetAudience": {
    "genders": ["Женщины", "Мужчины"],
    "ageRanges": ["25-45", "40-60"],
    "types": ["Семьи", "Молодёжь"]
  },
  "region": ["Алматы", "Астана", "Шымкент"],
  "goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
  "detailLevel": "СТАНДАРТНО",
  "strongSide": "Опытная команда, быстрая доставка",
  "weakSide": "Ограниченный маркетинговый бюджет",
  "analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
}

6. 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 компонент с формой

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

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. Валидация на фронтенде

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 интерфейсы

Замените старые интерфейсы на новые:

// Старый код
interface MarketingAnalysisRequest {
  targetAudience: string;
  region: string;
  analysisType: string;
}

// Новый код
interface MarketingAnalysisRequest {
  targetAudience: TargetAudience;
  region: string[];
  analysisType: string[];
}

Шаг 2: Обновить компоненты формы

  • Замените <textarea> для целевой аудитории на чекбоксы
  • Замените <select> для региона на множественный выбор (чекбоксы)
  • Замените <select> для типа анализа на множественный выбор (чекбоксы)

Шаг 3: Обновить обработку ответа

Добавьте проверку на массив в ответе API:

// Старый код
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. Примеры ошибок валидации

{
  "success": false,
  "message": "Ошибка валидации",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Ошибка валидации входных данных",
    "details": {
      "targetAudience": "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types",
      "region": "Поле 'region' обязательно для заполнения. Выберите хотя бы один регион",
      "analysisType": "Поле 'analysisType' обязательно для заполнения. Выберите хотя бы один тип анализа"
    }
  }
}

13. Константы для использования

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

Если у вас возникнут вопросы или проблемы при интеграции, обратитесь к команде бэкенда.