This commit is contained in:
xds
2026-03-22 12:40:33 +03:00
commit 28a5d51389
61 changed files with 6085 additions and 0 deletions

787
CLAUDE.md Normal file
View File

@@ -0,0 +1,787 @@
# 3D Print Calculator — Техническое задание на MVP
## Обзор проекта
Сервис 3D-печати на заказ с автоматическим расчётом стоимости. Клиент загружает 3D-модель (STL, 3MF, OBJ), выбирает материал, получает мгновенный расчёт цены и оформляет заказ. AI-ассистент помогает выбрать оптимальный материал под задачу.
**Бизнес-модель:** B2B — корпуса для электроники, функциональные запчасти, прототипы.
**Стек:** Python (FastAPI) + Vue 3 (Vite) + PostgreSQL + Nginx + Docker Compose.
**Хостинг:** VPS (Ubuntu 24), деплой через Docker Compose.
---
## Архитектура
```
┌─────────────────────────────────────────────┐
│ Nginx (reverse proxy) │
│ :80 → frontend, /api → backend │
└──────────┬──────────────┬───────────────────┘
│ │
┌──────────▼──────┐ ┌─────▼──────────────────┐
│ Vue 3 (Vite) │ │ FastAPI (Python) │
│ SPA, static │ │ REST API + WebSocket │
│ Port: 5173 │ │ Port: 8000 │
└─────────────────┘ └──────┬─────┬────────────┘
│ │
┌──────▼┐ ┌──▼───────────┐
│ PostgreSQL │ File Storage │
│ :5432 │ (local /uploads) │
└───────┘ └──────────────┘
```
### Контейнеры Docker Compose
1. **frontend** — Node 20 + Vite dev server (в проде — собранная статика через Nginx)
2. **backend** — Python 3.12 + FastAPI + Uvicorn
3. **db** — PostgreSQL 16
4. **nginx** — reverse proxy, SSL termination
---
## Backend (FastAPI)
### Структура проекта
```
backend/
├── app/
│ ├── main.py # FastAPI app, CORS, middleware
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # SQLAlchemy async engine + session
│ ├── models/
│ │ ├── material.py # Material ORM model
│ │ ├── order.py # Order ORM model
│ │ └── file_upload.py # Uploaded file metadata
│ ├── schemas/
│ │ ├── calculate.py # Request/Response для калькулятора
│ │ ├── material.py # Material schemas
│ │ └── order.py # Order schemas
│ ├── routers/
│ │ ├── calculate.py # POST /api/calculate
│ │ ├── materials.py # GET /api/materials
│ │ ├── orders.py # POST /api/orders
│ │ └── ai_advisor.py # POST /api/advisor
│ ├── services/
│ │ ├── file_parser.py # Парсинг STL/3MF/OBJ → геометрия
│ │ ├── price_engine.py # Расчёт стоимости
│ │ ├── ai_advisor.py # Интеграция с Claude API
│ │ └── telegram_notify.py # Уведомления в Telegram
│ └── seed/
│ └── materials.py # Начальные данные по материалам
├── requirements.txt
├── Dockerfile
└── alembic/ # Миграции БД
```
### API Endpoints
#### 1. POST /api/calculate
Основной endpoint калькулятора. Принимает файл и параметры, возвращает расчёт.
**Request:** `multipart/form-data`
- `file` (File, required) — 3D-модель (STL, 3MF, OBJ). Макс. размер: 50MB.
- `material_id` (int, required) — ID выбранного материала
- `infill_percent` (int, optional, default=30) — Процент заполнения (10-100)
- `layer_height_mm` (float, optional, default=0.2) — Высота слоя (0.08-0.4)
- `quantity` (int, optional, default=1) — Количество экземпляров (1-500)
- `post_processing` (str[], optional) — Постобработка: ["sanding", "painting", "threading"]
**Response:** `application/json`
```json
{
"success": true,
"file_info": {
"filename": "case_v3.stl",
"format": "stl",
"volume_cm3": 42.7,
"surface_area_cm2": 198.3,
"bounding_box_mm": {"x": 120.0, "y": 80.0, "z": 35.0},
"is_watertight": true,
"triangle_count": 12840
},
"calculation": {
"material": {
"id": 3,
"name": "PA (Nylon)",
"density_g_cm3": 1.14,
"price_per_gram": 50.0
},
"weight_grams": 48.7,
"material_cost_rub": 2435.0,
"print_time_hours": 4.2,
"time_cost_rub": 840.0,
"post_processing_cost_rub": 500.0,
"subtotal_rub": 3775.0,
"quantity": 2,
"quantity_discount_percent": 5,
"total_rub": 7172.5,
"estimated_days": 3
}
}
```
**Ошибки:**
- 400 — Неподдерживаемый формат файла
- 400 — Файл повреждён или не является 3D-моделью
- 400 — Модель не является водонепроницаемой (warning, не blocking)
- 413 — Файл слишком большой (>50MB)
#### 2. GET /api/materials
Список доступных материалов с характеристиками.
**Response:**
```json
[
{
"id": 1,
"name": "PLA",
"category": "basic",
"price_per_gram": 25.0,
"density_g_cm3": 1.24,
"properties": {
"max_temp_c": 60,
"strength": "medium",
"flexibility": "low",
"chemical_resistance": "low",
"food_safe": true
},
"description": "Базовый пластик, подходит для прототипов и декоративных изделий",
"color_options": ["white", "black", "gray", "red", "blue", "green", "natural"]
}
]
```
#### 3. POST /api/advisor
AI-ассистент для выбора материала. Принимает описание задачи, возвращает рекомендацию.
**Request:**
```json
{
"task_description": "Нужен корпус для уличного датчика температуры. Будет стоять на улице, диапазон температур от -30 до +50. Должен быть водонепроницаемым.",
"budget_preference": "optimal",
"file_info": {
"volume_cm3": 42.7,
"bounding_box_mm": {"x": 120, "y": 80, "z": 35}
}
}
```
**Response:**
```json
{
"recommended_material_id": 4,
"recommended_material_name": "PETG",
"reasoning": "Для уличного корпуса датчика рекомендую PETG: термостойкость до +80°C, морозостойкость до -40°C, хорошая UV-стойкость и водонепроницаемость. ABS тоже подошёл бы, но PETG проще в печати и не требует закрытой камеры.",
"alternatives": [
{
"material_id": 2,
"name": "ABS",
"why": "Выше ударопрочность, но требует постобработки для герметичности"
},
{
"material_id": 5,
"name": "ASA",
"why": "Лучшая UV-стойкость, но дороже"
}
]
}
```
**Реализация:** Отправляем запрос к Claude API (модель claude-sonnet-4-20250514) с системным промптом, содержащим каталог материалов и их свойства. Ключ API хранится в переменной окружения `ANTHROPIC_API_KEY`.
#### 4. POST /api/orders
Оформление заказа.
**Request:**
```json
{
"calculation_id": "uuid-of-saved-calculation",
"client_name": "Иван Петров",
"client_phone": "+79001234567",
"client_email": "ivan@example.com",
"client_company": "ООО Технопарк",
"delivery_method": "pickup",
"comment": "Нужно к пятнице, нанести резьбу M4 в двух отверстиях"
}
```
**Response:**
```json
{
"order_id": "ORD-2026-0042",
"status": "pending",
"total_rub": 7172.5,
"estimated_ready_date": "2026-04-02"
}
```
**Side effect:** Отправляет уведомление в Telegram-бот владельца с деталями заказа.
### Сервис парсинга файлов (file_parser.py)
Используемые библиотеки:
- `trimesh` — основной парсер. Читает STL (binary + ASCII), OBJ, 3MF, PLY, GLTF.
- `numpy-stl` — запасной вариант для STL, если trimesh не справился.
Что извлекаем из файла:
```python
import trimesh
def parse_3d_file(file_path: str, file_extension: str) -> FileInfo:
"""
Парсит 3D-файл и возвращает геометрические характеристики.
Поддерживаемые форматы: .stl, .3mf, .obj
STEP-файлы (.step, .stp) — в v2 (требует cadquery/OCP).
"""
mesh = trimesh.load(file_path, file_type=file_extension)
# Если 3MF содержит несколько тел — объединяем
if isinstance(mesh, trimesh.Scene):
mesh = trimesh.util.concatenate(mesh.dump())
return FileInfo(
volume_cm3=mesh.volume / 1000, # mm³ → cm³
surface_area_cm2=mesh.area / 100, # mm² → cm²
bounding_box_mm={
"x": mesh.bounding_box.extents[0],
"y": mesh.bounding_box.extents[1],
"z": mesh.bounding_box.extents[2],
},
is_watertight=mesh.is_watertight,
triangle_count=len(mesh.faces),
)
```
### Сервис расчёта цены (price_engine.py)
```python
def calculate_price(
file_info: FileInfo,
material: Material,
infill_percent: int = 30,
layer_height_mm: float = 0.2,
quantity: int = 1,
post_processing: list[str] = [],
) -> Calculation:
"""
Формула:
1. effective_volume = volume_cm3 * (infill_percent / 100) * 0.7 + volume_cm3 * 0.3
(70% объёма масштабируется по infill, 30% — стенки, всегда 100%)
2. weight_g = effective_volume * material.density_g_cm3
3. material_cost = weight_g * material.price_per_gram
4. print_time_h = estimate_print_time(file_info, layer_height_mm, material)
5. time_cost = print_time_h * TIME_RATE_PER_HOUR # ~200 руб/час
6. post_processing_cost = sum стоимостей выбранных операций
7. subtotal = material_cost + time_cost + post_processing_cost
8. total = subtotal * quantity * (1 - volume_discount(quantity))
"""
```
**Оценка времени печати (упрощённая):**
```python
def estimate_print_time(file_info, layer_height_mm, material):
"""
Упрощённая оценка без полного слайсинга.
layers = bounding_box_z / layer_height
volume_per_layer = volume_cm3 / layers * 1000 # mm³
time_per_layer = volume_per_layer / material.flow_rate_mm3_s / 60 # минуты
travel_time_per_layer ≈ 0.3 мин (константа для Bambu Lab)
total = layers * (time_per_layer + travel_time) + setup_time
"""
```
**Скидки за количество:**
- 1 шт — 0%
- 2-5 шт — 5%
- 6-20 шт — 10%
- 21-100 шт — 15%
- 101-500 шт — 20%
**Стоимость постобработки:**
- `sanding` (шлифовка) — 300 руб/шт
- `painting` (покраска) — 500 руб/шт
- `threading` (нарезка резьбы) — 200 руб/отверстие
- `acetone_smoothing` (ацетоновая обработка, только ABS) — 400 руб/шт
### Справочник материалов (seed data)
```python
MATERIALS = [
{
"name": "PLA",
"category": "basic",
"density_g_cm3": 1.24,
"price_per_gram": 25.0,
"flow_rate_mm3_s": 15.0,
"max_temp_c": 60,
"min_temp_c": -20,
"strength": "medium",
"flexibility": "low",
"chemical_resistance": "low",
"uv_resistance": "low",
"food_safe": True,
"description": "Базовый пластик. Лёгкий в печати, хорошая детализация. Для прототипов и декора.",
},
{
"name": "PETG",
"category": "basic",
"density_g_cm3": 1.27,
"price_per_gram": 28.0,
"flow_rate_mm3_s": 12.0,
"max_temp_c": 80,
"min_temp_c": -40,
"strength": "high",
"flexibility": "medium",
"chemical_resistance": "medium",
"uv_resistance": "medium",
"food_safe": True,
"description": "Универсальный инженерный пластик. Прочный, химстойкий, подходит для улицы.",
},
{
"name": "ABS",
"category": "basic",
"density_g_cm3": 1.04,
"price_per_gram": 25.0,
"flow_rate_mm3_s": 12.0,
"max_temp_c": 100,
"min_temp_c": -30,
"strength": "high",
"flexibility": "low",
"chemical_resistance": "medium",
"uv_resistance": "low",
"food_safe": False,
"description": "Термостойкий, ударопрочный. Требует закрытой камеры. Обрабатывается ацетоном.",
},
{
"name": "PA (Nylon)",
"category": "engineering",
"density_g_cm3": 1.14,
"price_per_gram": 50.0,
"flow_rate_mm3_s": 10.0,
"max_temp_c": 120,
"min_temp_c": -40,
"strength": "very_high",
"flexibility": "medium",
"chemical_resistance": "high",
"uv_resistance": "medium",
"food_safe": False,
"description": "Инженерный пластик. Высокая прочность, износостойкость. Для шестерён, креплений.",
},
{
"name": "PC (Поликарбонат)",
"category": "engineering",
"density_g_cm3": 1.20,
"price_per_gram": 60.0,
"flow_rate_mm3_s": 8.0,
"max_temp_c": 140,
"min_temp_c": -40,
"strength": "very_high",
"flexibility": "low",
"chemical_resistance": "high",
"uv_resistance": "high",
"food_safe": False,
"description": "Максимальная термостойкость и прочность. Для корпусов, работающих при высоких температурах.",
},
{
"name": "TPU",
"category": "engineering",
"density_g_cm3": 1.21,
"price_per_gram": 40.0,
"flow_rate_mm3_s": 6.0,
"max_temp_c": 80,
"min_temp_c": -30,
"strength": "medium",
"flexibility": "very_high",
"chemical_resistance": "high",
"uv_resistance": "medium",
"food_safe": False,
"description": "Эластичный пластик, аналог резины. Для прокладок, амортизаторов, гибких деталей.",
},
{
"name": "PA-CF (Нейлон + углеволокно)",
"category": "composite",
"density_g_cm3": 1.18,
"price_per_gram": 75.0,
"flow_rate_mm3_s": 8.0,
"max_temp_c": 150,
"min_temp_c": -40,
"strength": "extreme",
"flexibility": "low",
"chemical_resistance": "very_high",
"uv_resistance": "high",
"food_safe": False,
"description": "Композит с углеволокном. Максимальная жёсткость и прочность. Замена алюминия.",
},
]
```
### AI Advisor — системный промпт
Файл `app/services/ai_advisor.py` использует Anthropic Python SDK:
```python
SYSTEM_PROMPT = """
Ты — эксперт по 3D-печати из инженерных пластиков по технологии FDM.
Твоя задача — рекомендовать оптимальный материал для печати на основе описания задачи клиента.
Доступные материалы:
{materials_json}
Правила:
1. Всегда рекомендуй один основной материал и 1-2 альтернативы.
2. Учитывай: температурный режим, механические нагрузки, химическое воздействие, UV, влажность.
3. Если задача не подходит для FDM-печати (слишком мелкие детали, высокая точность) — честно скажи об этом.
4. Отвечай кратко, по делу, на русском языке.
5. Если клиент не указал критичные параметры — задай уточняющие вопросы.
Формат ответа — строго JSON:
{
"recommended_material_id": <int>,
"reasoning": "<обоснование на русском>",
"alternatives": [{"material_id": <int>, "name": "<str>", "why": "<причина>"}],
"questions": ["<вопрос, если нужна доп. информация>"] // пустой массив если вопросов нет
}
"""
```
### Telegram-уведомления (telegram_notify.py)
При создании заказа отправляем сообщение в Telegram-бот владельца:
```python
import httpx
async def notify_new_order(order: Order):
"""Отправляет уведомление о новом заказе в Telegram."""
text = (
f"🆕 Новый заказ #{order.order_id}\n"
f"Клиент: {order.client_name}\n"
f"Телефон: {order.client_phone}\n"
f"Материал: {order.material_name}\n"
f"Сумма: {order.total_rub}\n"
f"Комментарий: {order.comment or ''}"
)
await httpx.AsyncClient().post(
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID, "text": text}
)
```
Переменные окружения: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`.
---
## Frontend (Vue 3 + Vite)
### Структура проекта
```
frontend/
├── src/
│ ├── App.vue
│ ├── main.js
│ ├── router/
│ │ └── index.js # Vue Router
│ ├── stores/
│ │ ├── calculator.js # Pinia store — состояние калькулятора
│ │ └── materials.js # Pinia store — материалы
│ ├── api/
│ │ └── client.js # Axios instance, базовые запросы
│ ├── views/
│ │ ├── CalculatorView.vue # Главная страница с калькулятором
│ │ ├── MaterialsView.vue # Каталог материалов
│ │ └── OrderView.vue # Форма заказа
│ ├── components/
│ │ ├── FileUploader.vue # Drag-and-drop загрузка файла
│ │ ├── MaterialPicker.vue # Выбор материала (карточки)
│ │ ├── PrintSettings.vue # Настройки печати (infill, layer)
│ │ ├── PriceResult.vue # Отображение расчёта
│ │ ├── AiAdvisor.vue # Чат с AI-ассистентом
│ │ └── OrderForm.vue # Форма заказа
│ └── assets/
│ └── styles/
│ └── main.css # Tailwind CSS
├── index.html
├── vite.config.js
├── tailwind.config.js
├── package.json
└── Dockerfile
```
### Зависимости
```json
{
"dependencies": {
"vue": "^3.5",
"vue-router": "^4.4",
"pinia": "^2.2",
"axios": "^1.7"
},
"devDependencies": {
"vite": "^6.0",
"@vitejs/plugin-vue": "^5.1",
"tailwindcss": "^3.4",
"autoprefixer": "^10.4",
"postcss": "^8.4"
}
}
```
### Страницы и маршруты
| Путь | Компонент | Описание |
|------|-----------|----------|
| `/` | CalculatorView | Главная: загрузка файла → материал → настройки → цена |
| `/materials` | MaterialsView | Каталог материалов с фильтрами |
| `/order/:calcId` | OrderView | Форма оформления заказа |
### Компонент FileUploader.vue
Требования:
- Drag-and-drop зона + кнопка «Выбрать файл»
- Принимает: `.stl`, `.3mf`, `.obj` (валидация по расширению на фронте)
- Максимальный размер: 50 MB
- Отображает имя файла, размер и иконку формата после загрузки
- Показывает прогресс-бар при отправке на сервер
- При ошибке парсинга — человекочитаемое сообщение
### Компонент MaterialPicker.vue
Требования:
- Отображает материалы карточками (не dropdown)
- Карточка содержит: название, цену за грамм, ключевые свойства (иконки)
- Разделение на категории: «Базовые», «Инженерные», «Композитные»
- Выбранный материал подсвечивается
- Кнопка «Помочь выбрать» открывает AI-ассистент
### Компонент PrintSettings.vue
Требования:
- Слайдер: заполнение (10%-100%, шаг 10%, default 30%)
- Слайдер: высота слоя (0.08-0.4mm, шаг 0.04, default 0.2)
- Поле: количество (1-500, default 1)
- Чекбоксы: постобработка (шлифовка, покраска, резьба, ацетон)
- Подсказки при наведении: как параметр влияет на результат
### Компонент PriceResult.vue
Требования:
- Показывает разбивку: материал, время, постобработка, скидка, итого
- Крупно отображает итоговую цену
- Показывает примерный срок изготовления
- Кнопка «Оформить заказ» → переход на /order/:calcId
- Кнопка «Скачать расчёт (PDF)» — v2, пока не реализуем
### Компонент AiAdvisor.vue
Требования:
- Модальное окно или выдвижная панель справа
- Текстовое поле для описания задачи
- Кнопка «Получить рекомендацию»
- Отображает рекомендацию: основной материал + альтернативы с обоснованием
- Кнопка «Выбрать» рядом с каждой рекомендацией — применяет материал в калькулятор
### Дизайн
- Стиль: минималистичный, светлая тема, Tailwind CSS
- Акцентный цвет: #2563EB (синий) — кнопки, выделения
- Шрифт: Inter (Google Fonts)
- Адаптивность: mobile-first, работает на телефоне
- Тёмная тема: v2 (не в MVP)
---
## База данных (PostgreSQL)
### Таблицы
**materials**
```sql
CREATE TABLE materials (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL, -- basic, engineering, composite
density_g_cm3 FLOAT NOT NULL,
price_per_gram FLOAT NOT NULL,
flow_rate_mm3_s FLOAT NOT NULL,
max_temp_c INT,
min_temp_c INT,
strength VARCHAR(20), -- low, medium, high, very_high, extreme
flexibility VARCHAR(20),
chemical_resistance VARCHAR(20),
uv_resistance VARCHAR(20),
food_safe BOOLEAN DEFAULT FALSE,
description TEXT,
color_options JSONB DEFAULT '[]',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
```
**calculations**
```sql
CREATE TABLE calculations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_name VARCHAR(255) NOT NULL,
file_format VARCHAR(10) NOT NULL,
file_path VARCHAR(500),
volume_cm3 FLOAT NOT NULL,
surface_area_cm2 FLOAT,
bounding_box JSONB,
is_watertight BOOLEAN,
triangle_count INT,
material_id INT REFERENCES materials(id),
infill_percent INT DEFAULT 30,
layer_height_mm FLOAT DEFAULT 0.2,
quantity INT DEFAULT 1,
post_processing JSONB DEFAULT '[]',
weight_grams FLOAT,
material_cost_rub FLOAT,
time_cost_rub FLOAT,
post_processing_cost_rub FLOAT,
total_rub FLOAT,
estimated_days INT,
created_at TIMESTAMP DEFAULT NOW()
);
```
**orders**
```sql
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_id VARCHAR(20) UNIQUE NOT NULL, -- ORD-2026-0001
calculation_id UUID REFERENCES calculations(id),
client_name VARCHAR(200) NOT NULL,
client_phone VARCHAR(20) NOT NULL,
client_email VARCHAR(200),
client_company VARCHAR(200),
delivery_method VARCHAR(50) DEFAULT 'pickup', -- pickup, delivery
comment TEXT,
status VARCHAR(30) DEFAULT 'pending', -- pending, confirmed, printing, ready, delivered, cancelled
total_rub FLOAT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## Docker Compose
```yaml
version: "3.8"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: print3d
POSTGRES_USER: print3d
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
backend:
build: ./backend
environment:
DATABASE_URL: postgresql+asyncpg://print3d:${DB_PASSWORD}@db:5432/print3d
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
UPLOAD_DIR: /app/uploads
volumes:
- uploads:/app/uploads
depends_on:
- db
ports:
- "8000:8000"
frontend:
build: ./frontend
ports:
- "5173:5173"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/certs:/etc/nginx/certs
depends_on:
- backend
- frontend
volumes:
pgdata:
uploads:
```
---
## Переменные окружения (.env)
```
DB_PASSWORD=<strong_password>
ANTHROPIC_API_KEY=<claude_api_key>
TELEGRAM_BOT_TOKEN=<telegram_bot_token>
TELEGRAM_CHAT_ID=<your_telegram_chat_id>
```
---
## Порядок реализации
### Фаза 1 — Backend core (3-4 дня)
1. Инициализация FastAPI проекта, Docker, PostgreSQL
2. Модели SQLAlchemy + Alembic миграции
3. Seed данных по материалам
4. Сервис парсинга файлов (trimesh)
5. Сервис расчёта цены (price engine)
6. Endpoints: POST /api/calculate, GET /api/materials
### Фаза 2 — Frontend core (3-4 дня)
1. Инициализация Vue 3 + Vite + Tailwind
2. FileUploader компонент
3. MaterialPicker компонент
4. PrintSettings + PriceResult компоненты
5. Интеграция с API (Pinia stores + Axios)
### Фаза 3 — AI + Orders (2-3 дня)
1. AI Advisor — интеграция Claude API
2. AiAdvisor компонент (фронт)
3. OrderForm компонент + POST /api/orders
4. Telegram-уведомления
### Фаза 4 — Деплой (1-2 дня)
1. Docker Compose конфигурация
2. Nginx конфиг (reverse proxy + SSL)
3. Деплой на VPS
4. Тестирование end-to-end
---
## Что НЕ входит в MVP (v2)
- 3D-превью модели в браузере (Three.js + STLLoader)
- Парсинг STEP-файлов (требует cadquery / OpenCASCADE)
- Личный кабинет клиента
- Онлайн-оплата (ЮKassa / Stripe)
- История заказов
- Тёмная тема
- Скачивание расчёта в PDF
- Мультиязычность
- SEO-оптимизация (SSR / pre-rendering)