upd
This commit is contained in:
395
START_PROMPT.md
Normal file
395
START_PROMPT.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 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 + перемещение влево
|
||||
- Состояние сохраняется при перезагрузке страницы (данные на сервере)
|
||||
- Все запросы через очередь с дедупликацией и батчингом
|
||||
Reference in New Issue
Block a user