# Документация для фронтенда: Генерация изображений для постов

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

В систему добавлена автоматическая генерация изображений для постов в социальных сетях. Теперь при создании маркетинговой стратегии для каждого поста автоматически генерируется уникальное изображение с помощью 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 (
        <div className="post-card">
            <h3>{post.theme}</h3>
            <p>{post.postText}</p>

            {imageUrl ? (
                <img
                    src={imageUrl}
                    alt={post.theme}
                    onError={(e) => {
                        // Fallback если изображение не загрузилось
                        e.target.style.display = 'none';
                    }}
                    // Если требуется авторизация, используйте fetch с заголовками
                />
            ) : (
                <div className="no-image-placeholder">Изображение не сгенерировано</div>
            )}

            <div className="hashtags">
                {post.hashtags.map((tag) => (
                    <span key={tag}>#{tag}</span>
                ))}
            </div>
        </div>
    );
};
```

### 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 ? (
            <img src={getImageUrl(post.imageFilename)} alt={post.theme} onError={handleImageError} />
        ) : (
            <div className="image-placeholder">
                <Icon name="image" />
                <span>Изображение недоступно</span>
            </div>
        )}
    </>
);
```

### 3. Оптимизация загрузки

Рекомендуется использовать lazy loading для изображений:

```javascript
<img src={post.imageUrl ? `${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}` : null} alt={post.theme} loading="lazy" className="post-image" />
```

### 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<PostCardProps> = ({ 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 (
    <div className='post-card'>
      <div className='post-header'>
        <span className='platform-badge'>{post.platform}</span>
        <span className='content-type'>{post.contentType}</span>
        <span className='publish-time'>{post.publishTime}</span>
      </div>

      <h3 className='post-theme'>{post.theme}</h3>

      {imageUrl && !imageError ? (
        <div className='post-image-container'>
          <img
            src={imageUrl}
            alt={post.theme}
            className='post-image'
            loading='lazy'
            onError={() => setImageError(true)}
          />
        </div>
      ) : (
        <div className='no-image-placeholder'>
          <svg width='64' height='64' viewBox='0 0 24 24' fill='none'>
            <path
              d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'
              fill='currentColor'
            />
          </svg>
          <p>Изображение не сгенерировано</p>
        </div>
      )}

      <p className='post-text'>{post.postText}</p>

      <div className='post-hashtags'>
        {post.hashtags.map((tag, index) => (
          <span key={index} className='hashtag'>
            {tag.startsWith('#') ? tag : `#${tag}`}
          </span>
        ))}
      </div>

      <div className='post-footer'>
        <span className='publish-date'>
          {new Date(post.publishDate).toLocaleDateString('ru-RU')}
        </span>
      </div>
    </div>
  );
};

export default PostCard;
```

### Vue компонент

```vue
<template>
    <div class="post-card">
        <div class="post-header">
            <span class="platform-badge">{{ post.platform }}</span>
            <span class="content-type">{{ post.contentType }}</span>
            <span class="publish-time">{{ post.publishTime }}</span>
        </div>

        <h3 class="post-theme">{{ post.theme }}</h3>

        <div v-if="imageUrl && !imageError" class="post-image-container">
            <img :src="imageUrl" :alt="post.theme" class="post-image" loading="lazy" @error="imageError = true" />
        </div>
        <div v-else class="no-image-placeholder">
            <Icon name="image" />
            <p>Изображение не сгенерировано</p>
        </div>

        <p class="post-text">{{ post.postText }}</p>

        <div class="post-hashtags">
            <span v-for="(tag, index) in post.hashtags" :key="index" class="hashtag">
                {{ tag.startsWith('#') ? tag : `#${tag}` }}
            </span>
        </div>

        <div class="post-footer">
            <span class="publish-date">
                {{ formatDate(post.publishDate) }}
            </span>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';

interface PostCalendarItem {
    publishDate: string;
    platform: string;
    contentType: string;
    theme: string;
    postText: string;
    hashtags: string[];
    publishTime: string;
    imageUrl?: string | null;
    imageFilename?: string | null;
}

const props = defineProps<{
    post: PostCalendarItem;
    apiBaseUrl: string;
}>();

const imageError = ref(false);

const imageUrl = computed(() => {
    if (!props.post.imageFilename) return null;
    return `${props.apiBaseUrl}/api/marketing/analysis/images/${props.post.imageFilename}`;
});

const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString('ru-RU');
};
</script>
```

---

## Важные замечания

### 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 && <img src={`${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}`} alt={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
