← На главную

Тестовое задание

Polza Agency — Технический специалист / вайбкодер

1

Выгрузка → PostgreSQL

Из data_pack.zip (page_001.json … page_020.json, ~1000 компаний) все записи загружены в PostgreSQL. Реализована дедупликация, проставлены индексы.

  • Скрипт загрузки: `scripts/load_data.ts` — читает JSON, дедуплицирует по названию, загружает в БД
  • Схема: таблица `companies` с полями: id, name, category, city, address, rating, reviews_count, site, phone, created_at
  • Индексы: на name (для поиска), city (для фильтрации), category (для группировок)
  • Docker Compose: `docker-compose.yml` — PostgreSQL + приложение одной командой
  • SQL-запросы в блоке ниже
2

Страница /companies

Next.js App Router страница с серверным рендерингом. Таблица компаний из PostgreSQL с поиском по названию и фильтром по городу.

  • Server Component: `src/app/companies/page.tsx` — фетчит данные и города напрямую из БД
  • Client Component: `CompaniesClient.tsx` — интерактивная таблица с поиском и фильтрацией
  • Поиск через URL-параметры (?q=&city=) — работают навигация, рефреши и расшаривание результатов
  • Адаптивный дизайн: колонки скрываются на мобильных устройствах
3

Данные с сюрпризом (review.csv)

Анализ review.csv на аномалии. Все несоответствия задокументированы в ANOMALIES.md — 31 инцидент.

  • Найдены: невалидные рейтинги, битая кодировка (mojibake), дубликаты ID, сдвиг колонок, опечатки
  • Полный отчёт с таблицами и цветовой кодировкой по severity
4

Вайбкод / LLM-стек

Процесс разработки, инструменты, методология и архитектурные решения.

  • IDE: Claude Code + Cursor, агентские сценарии на живом репозитории
  • Модели: Claude Sonnet 4.5 / DeepSeek V4 Flash — под задачу
  • Подход: AI генерирует код, я проверяю каждый шаг, тестирую и коммичу
  • Все изменения в git с осмысленными сообщениями, каждая фича в своей ветке
5

SEO-оптимизация

Полный набор SEO-базовых мета-тегов, структурированных данных и файлов для индексации.

  • Open Graph / Twitter Cards на всех страницах с og:image 1200×630
  • JSON-LD структурированные данные (Organization + BreadcrumbList)
  • robots.txt + sitemap.xml + уникальные canonical URL
  • Уникальные title/description для каждой страницы (главная, /companies, /privacy, /docs, /anomalies)
  • favicon.svg + lang="ru"

Архитектурные решения

Почему Next.js + TypeScript, а не Python?

В ТЗ не было жёсткого требования по языку для ETL-скрипта, но вся платформа Polza Agency построена на Next.js + TypeScript + PostgreSQL. Выбор TypeScript для скрипта загрузки данных и всего приложения — это осознанное архитектурное решение:

  • Единый стек — скрипты загрузки, сайт и API пишутся на одном языке. Не нужно переключать контекст между Python и TypeScript.
  • Типизация — TypeScript даёт статическую проверку типов на всём протяжении: от парсинга JSON до UI-компонентов.
  • Server Components — данные фетчатся напрямую в серверном компоненте, без отдельного API-слоя. Меньше кода, быстрее работа.
  • Docker Compose — и БД, и приложение поднимаются одной командой. Не нужно ставить Python, Node.js или настраивать окружение вручную.
  • Единый CI/CD — один набор зависимостей для всего проекта: npm install, npm run build, npm start.

Прочие важные моменты

  • Безопасность — .env в .gitignore, секреты не попадают в репозиторий. Промпт-инъекции и SQL-инъекции проверены (не найдены).
  • Воспроизводимость — docker-compose.yml поднимает PostgreSQL, скрипт load_data.ts загружает данные. README с командой запуска.
  • Дедупликация — компании сопоставляются по названию, дубликаты исключаются на этапе загрузки.
  • ETL-обработка аномалий — автоматическое выявление и исправление битой кодировки, нормализация городов, приведение типов.

SQL-запросы

1. Топ-5 категорий по числу компаний

SELECT category, COUNT(*) AS count
FROM companies
GROUP BY category
ORDER BY count DESC
LIMIT 5;

2. Средний рейтинг по городам (компании с 10+ отзывами)

SELECT city, AVG(rating) AS avg_rating
FROM companies
WHERE reviews_count >= 10
GROUP BY city
ORDER BY avg_rating DESC;

3. Доля компаний с сайтом по категориям

SELECT
  category,
  COUNT(*) AS total,
  COUNT(site) FILTER (WHERE site IS NOT NULL) AS with_site,
  ROUND(
    100.0 * COUNT(site) FILTER (WHERE site IS NOT NULL) / COUNT(*),
    1
  ) AS pct_with_site
FROM companies
GROUP BY category
ORDER BY pct_with_site DESC;