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

396 lines
15 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
> Запусти этот промт из корня `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)
```json
{
"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)
```json
{
"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)
```js
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:
```json
{
"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:
```json
{ "id": 1000001 }
```
Response:
```json
{ "queued": true, "id": 1000001 }
```
Ошибки: 400 если `id` не передан или не число.
#### `POST /api/items/select`
Добавить элемент в очередь на выбор (переместить вправо).
Body:
```json
{ "id": 42 }
```
Response:
```json
{ "queued": true }
```
#### `POST /api/items/deselect`
Вернуть элемент в левое окно.
Body:
```json
{ "id": 42 }
```
#### `PUT /api/items/reorder`
Сохранить новый порядок выбранных элементов после drag&drop.
Body:
```json
{ "ids": [5, 3, 1, 42] }
```
Response:
```json
{ "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
```json
{
"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
```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)
```js
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 + перемещение влево
- Состояние сохраняется при перезагрузке страницы (данные на сервере)
- Все запросы через очередь с дедупликацией и батчингом