Files
tmc-test-task/START_PROMPT.md
Oscar 09e19d7df0 upd
2026-06-03 20:24:59 +03:00

15 KiB
Raw Blame History

TMC Test Task — Claude Code Prompt

Запусти этот промт из корня A:\my-apps\tmc-test-task


Контекст и цель

Ты создаёшь fullstack-приложение для тестового задания. В текущей директории создай две папки: backend и frontend. Начни с бэкенда, затем фронтенд. Работай итеративно — сначала scaffold, потом реализация по фичам.


Структура репозитория

tmc-test-task/
├── backend/
│   ├── src/
│   │   ├── index.ts
│   │   ├── routes/
│   │   │   └── items.ts
│   │   ├── services/
│   │   │   ├── itemsStore.ts      ← in-memory хранилище
│   │   │   └── queue.ts           ← очередь с дедупликацией
│   │   ├── middleware/
│   │   │   └── batcher.ts         ← батчинг запросов
│   │   └── swagger.ts
│   ├── eslint.config.js
│   ├── tsconfig.json
│   └── package.json
└── frontend/
    ├── app/
    │   ├── components/
    │   │   ├── LeftPanel.vue
    │   │   └── RightPanel.vue
    │   ├── composables/
    │   │   └── useItems.ts
    │   ├── services/              ← сюда генерится axios-клиент
    │   └── app.vue
    ├── eslint.config.js
    ├── nuxt.config.ts
    └── package.json

BACKEND

Технологии

  • Runtime: Node.js + TypeScript
  • Framework: Express.js
  • Package manager: pnpm
  • Dev runner: tsx watch
  • Build: tsc && tsc-alias
  • Lint: @antfu/eslint-config
  • Docs: swagger-jsdoc + swagger-ui-express

package.json (backend)

{
  "name": "tmc-backend",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc && tsc-alias",
    "start": "node dist/index.js",
    "lint": "eslint ."
  }
}

Зависимости: express, swagger-jsdoc, swagger-ui-express, cors Dev: tsx, tsc-alias, typescript, @types/node, @types/express, @types/swagger-jsdoc, @types/swagger-ui-express, @types/cors, eslint, @antfu/eslint-config

tsconfig.json (backend)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

eslint.config.js (backend)

import antfu from '@antfu/eslint-config'

export default antfu({
  typescript: true,
  node: true,
  formatters: true,
  rules: {
    'no-console': 'warn',
    'node/prefer-global/process': 'off',
  },
})

Бизнес-логика

In-memory хранилище (itemsStore.ts)

  • При старте приложения инициализировать массив allItems с ID от 1 до 1 000 000
  • selectedIds: Set<number> — множество выбранных ID
  • orderedSelected: number[] — массив для сохранения порядка drag&drop
  • Методы:
    • getItems(page, limit, search) — возвращает НЕвыбранные элементы с пагинацией и фильтрацией по ID (строковый поиск по ID)
    • getSelectedItems(page, limit, search) — возвращает выбранные в порядке orderedSelected
    • addItem(id) — добавить новый элемент (id может быть любым числом)
    • selectItem(id) — переместить в правое окно
    • deselectItem(id) — вернуть в левое окно
    • reorderSelected(ids: number[]) — сохранить новый порядок

Очередь с дедупликацией (queue.ts)

Реализовать RequestQueue класс:

  • Очередь принимает задачи типа: 'add' | 'select' | 'deselect' | 'reorder'
  • Дедупликация: для add, select, deselect — ключ дедупликации это ${type}:${id}. Если такая задача уже в очереди (не выполнена), повторно не добавлять
  • Гарантия: одно значение не будет добавлено повторно (проверять и в store)
  • Батчинг:
    • Задачи add — флашатся раз в 10 секунд (или при накоплении 100 задач)
    • Задачи select, deselect, reorder — флашатся раз в 1 секунду
  • При флаше — выполнять все задачи из батча атомарно

Swagger (swagger.ts)

Настроить swagger-jsdoc + swagger-ui-express:

  • Документация доступна по GET /docs
  • JSON-схема по GET /docs/json (нужен для генерации клиента во фронте)
  • Описать все модели и ответы

API Routes

Все роуты с JSDoc-комментариями для swagger. Базовый путь: /api

GET /api/items

Возвращает НЕвыбранные элементы (левое окно).

Query params:

  • page (number, default: 1)
  • limit (number, default: 20, max: 20)
  • search (string, optional) — фильтр по ID (поиск подстроки в строке ID)

Response:

{
  "data": [{ "id": 1 }, { "id": 2 }],
  "total": 999980,
  "page": 1,
  "limit": 20,
  "hasMore": true
}

GET /api/items/selected

Возвращает выбранные элементы (правое окно) в сохранённом порядке.

Query params:

  • page (number, default: 1)
  • limit (number, default: 20, max: 20)
  • search (string, optional)

Response: аналогично выше.

POST /api/items/add

Добавить элемент в очередь на добавление.

Body:

{ "id": 1000001 }

Response:

{ "queued": true, "id": 1000001 }

Ошибки: 400 если id не передан или не число.

POST /api/items/select

Добавить элемент в очередь на выбор (переместить вправо).

Body:

{ "id": 42 }

Response:

{ "queued": true }

POST /api/items/deselect

Вернуть элемент в левое окно.

Body:

{ "id": 42 }

PUT /api/items/reorder

Сохранить новый порядок выбранных элементов после drag&drop.

Body:

{ "ids": [5, 3, 1, 42] }

Response:

{ "queued": true }

CORS и порт

  • Порт: 1337
  • CORS: разрешить http://localhost:3000 (фронт на nuxt)
  • Swagger UI: GET /docs, JSON: GET /docs/json

FRONTEND

Технологии

  • Framework: Nuxt 3
  • UI: @nuxt/ui (последняя версия)
  • Styling: Tailwind CSS (через @nuxt/ui), SASS
  • Package manager: pnpm
  • Lint: @antfu/eslint-config + vue-tsc
  • API клиент: axios + автогенерация через swagger-typescript-api

package.json (frontend) — scripts

{
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "preview": "nuxt preview",
    "lint": "eslint .",
    "typecheck": "vue-tsc --noEmit",
    "gen:api": "npx swagger-typescript-api generate --path http://localhost:1337/docs/json --output ./app/services --name api.ts --axios --unwrap-response-data"
  }
}

Зависимости: @nuxt/ui, axios, vue-draggable-next (drag&drop) Dev: eslint, @antfu/eslint-config, vue-tsc, typescript, sass, swagger-typescript-api

nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  compatibilityDate: '2024-11-01',
  devtools: { enabled: true },
  css: ['~/assets/styles/main.scss'],
  runtimeConfig: {
    public: {
      apiBase: process.env.API_BASE ?? 'http://localhost:1337/api',
    },
  },
})

eslint.config.js (frontend)

import antfu from '@antfu/eslint-config'

export default antfu({
  typescript: true,
  vue: true,
  formatters: true,
  rules: {
    'no-console': 'warn',
  },
})

UI и функционал

Главная страница (app/app.vue или app/pages/index.vue)

Разметка: два равных контейнера в ряд (50/50 или flex/grid), высота — весь экран минус header.

Левое окно (компонент LeftPanel.vue)

  • Заголовок: "All items"
  • Инпут поиска: фильтрация по ID (debounce 300ms)
  • Кнопка "Add item": открывает modal/popover с инпутом для ввода ID → POST /api/items/add
  • Список элементов:
    • Каждый элемент: #ID + кнопка "→" для выбора (POST /api/items/select)
    • Инфинити-скролл: IntersectionObserver на sentinel-элементе в конце списка, подгружать по 20 элементов
    • При поиске — сбрасывать страницу, начинать с 1
    • hasMore из ответа API определяет, показывать ли sentinel

Правое окно (компонент RightPanel.vue)

  • Заголовок: "Selected items"
  • Инпут поиска: фильтрация по ID (debounce 300ms)
  • Список с drag&drop (библиотека vue-draggable-next):
    • Drag&drop работает для видимого (в том числе отфильтрованного) списка
    • После drop: PUT /api/items/reorder с полным новым порядком
    • Каждый элемент: drag-handle иконка + #ID + кнопка "←" для возврата (POST /api/items/deselect)
    • Инфинити-скролл аналогично левому окну

Composable (composables/useItems.ts)

Создать composable useItems() с методами:

  • fetchLeft(reset?) — загрузить/дозагрузить левую панель
  • fetchRight(reset?) — загрузить/дозагрузить правую панель
  • selectItem(id) — выбрать элемент
  • deselectItem(id) — вернуть
  • reorderSelected(ids) — сохранить порядок
  • addItem(id) — добавить новый элемент
  • Реактивные состояния: leftItems, rightItems, leftSearch, rightSearch, leftLoading, rightLoading

Использовать axios для запросов. После каждой мутации (select/deselect/add) — перезагрузить обе панели (reset + fetch).

Сохранение состояния

На сервере хранится выбор и порядок (in-memory). Фронт при монтировании делает fetchLeft() и fetchRight() — данные восстанавливаются из сервера. Поиск не сохраняется (сбрасывается при перезагрузке страницы — это правильное поведение).


Порядок выполнения

  1. Создать директории backend/ и frontend/
  2. Backend:
    1. Инициализировать pnpm init + установить зависимости
    2. Создать tsconfig.json, eslint.config.js
    3. Реализовать itemsStore.ts
    4. Реализовать queue.ts
    5. Реализовать роуты в routes/items.ts
    6. Настроить swagger в swagger.ts
    7. Собрать всё в index.ts
    8. Проверить pnpm dev — должен стартовать на порту 1337
  3. Frontend:
    1. pnpm dlx nuxi@latest init frontend (или вручную scaffold)
    2. Установить зависимости
    3. Создать nuxt.config.ts, eslint.config.js
    4. Создать структуру папок
    5. Реализовать composable useItems.ts
    6. Реализовать LeftPanel.vue
    7. Реализовать RightPanel.vue
    8. Собрать главную страницу
    9. Добавить скрипт gen:api и сгенерировать клиент (бэк должен быть запущен)

Важные детали реализации

  • Инфинити-скролл: не забудь disconnect() observer при unmount
  • Дедупликация в очереди: если POST /api/items/select вызывается дважды для одного ID — второй запрос игнорируется до выполнения первого
  • При инициализации 1М элементов — не держать в памяти объекты, только числа (массив number[] весит ~8MB, приемлемо)
  • Типы из swagger: после gen:api в app/services/api.ts будет класс Api с axios. Использовать его в composable через const api = new Api({ baseURL: config.public.apiBase })
  • CORS: обязательно, иначе фронт не достучится до бэка
  • Батчинг 10 сек для add — при разработке можно уменьшить до 3 сек для теста, но в финальной версии должно быть 10 сек
  • reorder через очередь: drag&drop может вызываться часто, дедуплицировать по типу (последний вызов побеждает, предыдущие отменяются — реализовать как "debounced queue" для reorder)

Что должно работать в итоге

  • http://localhost:1337/docs — Swagger UI с описанием всех эндпоинтов
  • http://localhost:1337/docs/json — OpenAPI JSON (для генерации клиента)
  • http://localhost:3000 — фронт с двумя панелями
  • Левая панель: поиск + инфинити-скролл + добавление + перемещение вправо
  • Правая панель: поиск + инфинити-скролл + drag&drop + перемещение влево
  • Состояние сохраняется при перезагрузке страницы (данные на сервере)
  • Все запросы через очередь с дедупликацией и батчингом