Files
internetlab-test-task/PROMPT.md
2026-06-22 09:31:44 +03:00

457 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TMC Test Task — Claude Code Prompt
> Запусти этот промт из корня проекта (например `~/projects/dev-landing`)
---
## Цель
Создать fullstack-приложение: **backend API на NestJS** + **лендинг на Nuxt 3**.
Монорепозиторий с двумя папками: `backend/` и `frontend/`.
Работай итеративно: сначала scaffold → потом реализация по фичам. Не переходи к следующему шагу, пока текущий не работает.
---
## Структура репозитория
```
internetlab-test-task/
├── backend/
│ ├── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── contact/
│ │ │ ├── contact.module.ts
│ │ │ ├── contact.controller.ts
│ │ │ ├── contact.service.ts
│ │ │ ├── dto/
│ │ │ │ └── create-contact.dto.ts
│ │ │ └── interfaces/
│ │ │ └── contact.interface.ts
│ │ ├── ai/
│ │ │ ├── ai.module.ts
│ │ │ └── ai.service.ts
│ │ ├── mail/
│ │ │ ├── mail.module.ts
│ │ │ └── mail.service.ts
│ │ ├── health/
│ │ │ └── health.controller.ts
│ │ ├── metrics/
│ │ │ ├── metrics.module.ts
│ │ │ ├── metrics.controller.ts
│ │ │ └── metrics.service.ts
│ │ ├── logger/
│ │ │ └── logger.service.ts
│ │ └── storage/
│ │ └── file-storage.service.ts
│ ├── data/
│ │ ├── logs/ ← логи запросов (создаётся автоматически)
│ │ ├── metrics.json ← статистика обращений
│ │ └── rate-limit/ ← данные rate limiting
│ ├── .env.example
│ ├── tsconfig.json
│ └── package.json
└── frontend/
├── app/
│ ├── pages/
│ │ └── index.vue
│ ├── components/
│ │ ├── HeroSection.vue
│ │ ├── AboutSection.vue
│ │ ├── SkillsSection.vue
│ │ ├── ProjectsSection.vue
│ │ └── ContactForm.vue
│ ├── composables/
│ │ └── useContact.ts
│ └── assets/
│ └── styles/
│ └── main.scss
├── .env.example
├── nuxt.config.ts
└── package.json
```
---
## BACKEND
### Технологии
- **Runtime**: Node.js + TypeScript
- **Framework**: NestJS 10 + Fastify adapter (`@nestjs/platform-fastify`)
- **Package manager**: pnpm
- **Validation**: `class-validator` + `class-transformer`
- **Docs**: `@nestjs/swagger`
- **Rate limiting**: `@nestjs/throttler`
- **Mail**: `nodemailer`
- **Logging**: `winston`
- **AI**: `openai` (OpenAI-compatible SDK, используем Groq baseURL)
- **Config**: `@nestjs/config` + `dotenv`
### package.json (backend)
```json
{
"name": "dev-landing-backend",
"scripts": {
"dev": "nest start --watch",
"build": "nest build",
"start": "node dist/main.js",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\""
}
}
```
Зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/platform-fastify`, `@nestjs/config`, `@nestjs/swagger`, `@nestjs/throttler`, `class-validator`, `class-transformer`, `reflect-metadata`, `rxjs`, `nodemailer`, `winston`, `openai`, `uuid`
Dev: `@nestjs/cli`, `@nestjs/schematics`, `@types/node`, `@types/nodemailer`, `@types/uuid`, `typescript`, `ts-node`, `eslint`, `@antfu/eslint-config`
### Переменные окружения (`.env.example`)
```env
# Server
PORT=3001
NODE_ENV=development
# CORS
FRONTEND_URL=http://localhost:3000
# Mail (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASS=your-app-password
MAIL_FROM=your@gmail.com
MAIL_TO=owner@gmail.com
# AI (Groq)
GROQ_API_KEY=your-groq-api-key
GROQ_MODEL=llama-3.3-70b-versatile
# Rate limiting
THROTTLE_TTL=60000
THROTTLE_LIMIT=5
```
### Архитектура и реализация
#### `main.ts`
- NestJS + Fastify adapter
- Включить `ValidationPipe` глобально (с `transform: true`, `whitelist: true`)
- Настроить CORS: разрешить `FRONTEND_URL` из env
- Настроить Swagger: `DocumentBuilder` → title, description, version → `SwaggerModule.setup('api/docs', app, document)`
- Порт из `process.env.PORT ?? 3001`
#### `app.module.ts`
Импортировать: `ConfigModule.forRoot({ isGlobal: true })`, `ThrottlerModule.forRootAsync(...)`, `ContactModule`, `HealthModule`, `MetricsModule`
#### DTO (`create-contact.dto.ts`)
```typescript
export class CreateContactDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
@ApiProperty({ example: "Иван Иванов" })
name: string;
@IsString()
@IsNotEmpty()
@Matches(/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/)
@ApiProperty({ example: "+7 999 123-45-67" })
phone: string;
@IsEmail()
@ApiProperty({ example: "ivan@example.com" })
email: string;
@IsString()
@IsOptional()
@MaxLength(1000)
@ApiProperty({ example: "Хочу обсудить проект", required: false })
comment?: string;
}
```
#### `ContactController` (`POST /api/contact`)
- Декоратор `@UseGuards(ThrottlerGuard)`
- Принимает `CreateContactDto`
- Вызывает `contactService.handleContact(dto)`
- Возвращает `{ success: true, message: string, ai: { sentiment, category, autoReply } }`
- Обработка ошибок: если mail упал — вернуть 500 с понятным сообщением; если AI упал — продолжить без AI (graceful fallback)
- Все ошибки логировать через `LoggerService`
#### `ContactService`
Метод `handleContact(dto: CreateContactDto)`:
1. Логировать входящий запрос через `LoggerService` (в файл)
2. Вызвать `aiService.analyzeContact(dto)` — получить `{ sentiment, category, autoReply }`. Если упал — `null`, продолжить
3. Вызвать `mailService.sendOwnerNotification(dto, aiResult)`
4. Вызвать `mailService.sendUserConfirmation(dto, aiResult)`
5. Обновить метрики через `metricsService.increment(category)`
6. Вернуть результат
#### `AiService`
Используй `openai` SDK с Groq baseURL:
```typescript
import OpenAI from "openai";
const groq = new OpenAI({
apiKey: process.env.GROQ_API_KEY,
baseURL: "https://api.groq.com/openai/v1",
});
```
Метод `analyzeContact(dto)`:
- Промпт (системный): "Ты — аналитик обращений. Отвечай ТОЛЬКО валидным JSON без markdown и пояснений."
- Промпт (пользователь): передать имя, email, комментарий
- Ожидаемый JSON-ответ от модели:
```json
{
"sentiment": "positive" | "neutral" | "negative",
"category": "question" | "partnership" | "job" | "spam" | "other",
"autoReply": "текст автоответа на русском языке (2-3 предложения)"
}
```
- Graceful fallback: обернуть в `try/catch`, при ошибке логировать и вернуть `null`
- Таймаут запроса: 10 секунд
#### `MailService`
`sendOwnerNotification(dto, aiResult)`:
- Тема: `Новое обращение от ${dto.name}`
- HTML-письмо: имя, телефон, email, комментарий, блок AI-анализа (если есть)
`sendUserConfirmation(dto, aiResult)`:
- Тема: `Спасибо за обращение, ${dto.name}!`
- HTML-письмо: подтверждение получения + `autoReply` от AI (если есть) или дефолтный текст
#### `LoggerService`
Использовать `winston`:
- Транспорты: Console + File (`data/logs/requests-YYYY-MM-DD.log`)
- Формат файла: JSON с `timestamp`, `level`, `message`, `meta`
- Метод `logRequest(method, path, body, response, duration)` — логировать каждый запрос
- Метод `logError(error, context)` — логировать ошибки
#### `FileStorageService`
Простой сервис для работы с JSON-файлами:
- `read<T>(filepath): T | null`
- `write<T>(filepath, data: T): void`
- Все операции синхронные (`fs.readFileSync` / `writeFileSync`)
- Автосоздание директории если не существует
#### `MetricsService`
Хранить данные в `data/metrics.json`:
```json
{
"total": 0,
"byCategory": {
"question": 0,
"partnership": 0,
"job": 0,
"spam": 0,
"other": 0
},
"bySentiment": { "positive": 0, "neutral": 0, "negative": 0 },
"lastUpdated": "ISO timestamp"
}
```
`MetricsController` → `GET /api/metrics` → вернуть текущие метрики
#### `HealthController` → `GET /api/health`
```json
{
"status": "ok",
"timestamp": "ISO",
"uptime": 12345,
"version": "1.0.0"
}
```
#### Rate Limiting
`ThrottlerModule` с настройками из env: `ttl: THROTTLE_TTL (60000ms)`, `limit: THROTTLE_LIMIT (5)`.
Применять `ThrottlerGuard` к `POST /api/contact`.
При превышении лимита — NestJS вернёт 429 автоматически.
#### Глобальный error handler
Создать `AllExceptionsFilter` (`@Catch()`):
- Логировать все необработанные ошибки через `LoggerService`
- Возвращать единообразный JSON: `{ statusCode, message, timestamp, path }`
- Зарегистрировать глобально в `main.ts`
#### Swagger
- Доступен по `GET /api/docs`
- Все эндпоинты задокументированы с `@ApiOperation`, `@ApiResponse`, `@ApiBody`
- DTO с `@ApiProperty` декораторами
---
## FRONTEND
### Технологии
- **Framework**: Nuxt 3
- **UI**: `@nuxt/ui` (последняя версия)
- **Styling**: Tailwind CSS (через `@nuxt/ui`) + SASS
- **Package manager**: pnpm
- **Lint**: `@antfu/eslint-config`
- **HTTP**: встроенный `$fetch` / `useFetch` Nuxt
### `nuxt.config.ts`
```typescript
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
css: ["~/assets/styles/main.scss"],
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? "http://localhost:3001/api",
},
},
});
```
### Переменные окружения (`.env.example`)
```env
NUXT_PUBLIC_API_BASE=http://localhost:3001/api
```
### Страница (`pages/index.vue`)
Лендинг разработчика. Секции по порядку:
1. **HeroSection** — имя, должность, краткое описание, кнопка "Связаться"
2. **AboutSection** — несколько предложений о себе
3. **SkillsSection** — карточки со скиллами (NestJS, Nuxt, TypeScript, etc.)
4. **ProjectsSection** — карточки проектов (можно placeholder данные)
5. **ContactForm** — форма обратной связи
Навбар: фиксированный вверху, ссылки-якоря на секции. Плавный скролл через CSS `scroll-behavior: smooth`.
### `ContactForm.vue`
Поля формы: Имя (required), Телефон (required), Email (required), Комментарий (textarea, optional).
Логика:
- Валидация на клиенте перед отправкой (не пустые обязательные поля, формат email)
- `POST` на `${apiBase}/contact`
- Состояния: `idle → loading → success | error`
- При `success`: показать сообщение + `autoReply` от AI если есть
- При `error`: показать текст ошибки из ответа
- При 429 (rate limit): показать "Слишком много запросов, попробуйте позже"
### `composables/useContact.ts`
```typescript
export function useContact() {
const config = useRuntimeConfig();
const loading = ref(false);
const error = ref<string | null>(null);
const success = ref(false);
const aiReply = ref<string | null>(null);
async function submit(data: ContactFormData) {
loading.value = true;
error.value = null;
try {
const res = await $fetch(`${config.public.apiBase}/contact`, {
method: "POST",
body: data,
});
success.value = true;
aiReply.value = res.ai?.autoReply ?? null;
} catch (e: any) {
if (e.status === 429)
error.value = "Слишком много запросов. Попробуйте через минуту.";
else error.value = e.data?.message ?? "Что-то пошло не так";
} finally {
loading.value = false;
}
}
return { loading, error, success, aiReply, submit };
}
```
### Дизайн
Тёмная тема (dark mode по умолчанию). Современный минималистичный стиль разработчика.
Использовать компоненты `@nuxt/ui`: `UButton`, `UInput`, `UTextarea`, `UCard`, `UBadge`, `UAlert`.
Анимации появления секций при скролле через CSS `@keyframes` + `IntersectionObserver`.
---
## Порядок выполнения
### Backend
1. `mkdir backend && cd backend` → `pnpm init` → установить зависимости → scaffold через `nest new . --package-manager pnpm` или вручную
2. Настроить `tsconfig.json`, `.env` из `.env.example`
3. Создать `FileStorageService`, `LoggerService` (базовые утилиты)
4. Создать `AiService` → протестировать вызов Groq
5. Создать `MailService` → протестировать отправку (можно с `SMTP_HOST=smtp.ethereal.email` для теста)
6. Создать `ContactModule`: DTO → Controller → Service (с AI + mail + logging)
7. Создать `MetricsModule` и `HealthController`
8. Настроить `ThrottlerModule`, `AllExceptionsFilter`, Swagger
9. Проверить: `pnpm dev` → `http://localhost:3001/api/docs` открывается
### Frontend
1. `pnpm dlx nuxi@latest init frontend` → выбрать нужные модули
2. Установить `@nuxt/ui`, `sass`
3. Настроить `nuxt.config.ts`, `.env`
4. Создать `useContact` composable
5. Сверстать секции лендинга (Hero → About → Skills → Projects → Contact)
6. Реализовать `ContactForm.vue` с полной логикой
7. Проверить: форма отправляет запрос, показывает AI-ответ
---
## Важные детали
- `data/` директория в `backend/` — создавать автоматически при старте если не существует
- Метрики и rate-limit данные — хранить в файлах, не в памяти (чтобы переживали рестарт)
- Логи — один файл в день, имя формата `requests-2025-01-15.log`
- AI fallback — если `GROQ_API_KEY` не задан или запрос упал, ContactService продолжает работу (отправляет письма без AI-анализа), в ответе `ai: null`
- Mail fallback — если SMTP не настроен (пустой `SMTP_HOST`), MailService логирует письмо в консоль вместо отправки (для удобства разработки)
- CORS — только `FRONTEND_URL` из env, не `*`
- Все секреты только через env, никаких хардкодов
---
## Что должно работать в итоге
- `http://localhost:3001/api/docs` — Swagger UI со всеми эндпоинтами
- `POST http://localhost:3001/api/contact` — принимает форму, анализирует AI, отправляет письма, логирует
- `GET http://localhost:3001/api/health` — статус сервиса
- `GET http://localhost:3001/api/metrics` — статистика обращений
- `http://localhost:3000` — лендинг с формой, которая работает с бэком
- Форма показывает AI-автоответ после успешной отправки
- Rate limiting: больше 5 запросов в минуту → 429
- Логи пишутся в `backend/data/logs/`