init
This commit is contained in:
456
PROMPT.md
Normal file
456
PROMPT.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# 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/`
|
||||
Reference in New Issue
Block a user