Files
test-calculator/ARCHITECTURE.md
2026-02-17 20:40:09 +03:00

321 lines
11 KiB
Markdown
Raw 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.

# Архитектура Calculator
## Дерево файлов
```
calculator/
├── service.yaml # Конфиг для ci-templates (тип, домен, ресурсы)
├── .drone.yml # CI/CD пайплайн (копия base.drone.yml)
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── public/
│ └── favicon.svg
└── src/
├── main.tsx # Точка входа, рендер <App />
├── App.tsx # Роутинг: /login → /calculator
├── App.css # Глобальные стили (reset, body, центрирование)
├── context/
│ └── AuthContext.tsx # React Context + Provider для псевдоавторизации
├── hooks/
│ ├── useCalculator.ts # Стейт-машина калькулятора (вся логика)
│ └── useKeyboard.ts # Слушатель клавиатуры → маппинг на действия
├── utils/
│ └── calculate.ts # Чистые функции вычислений
├── pages/
│ ├── LoginPage.tsx # Страница входа
│ ├── LoginPage.module.css
│ ├── CalculatorPage.tsx # Страница калькулятора (шапка + калькулятор)
│ └── CalculatorPage.module.css
└── components/
├── Calculator/
│ ├── Calculator.tsx # Обёртка: Display + Keypad
│ └── Calculator.module.css
├── Display/
│ ├── Display.tsx # Выражение + результат
│ └── Display.module.css
├── Keypad/
│ ├── Keypad.tsx # Сетка кнопок
│ └── Keypad.module.css
└── Button/
├── Button.tsx # Кнопка калькулятора
└── Button.module.css
```
## Схема компонентов
```
<App>
<AuthProvider> ← context/AuthContext.tsx
<Routes>
/login → <LoginPage /> ← Форма логина
/ → <ProtectedRoute> ← Редирект на /login если нет юзера
<CalculatorPage />
├── Header (логин + выход)
└── <Calculator />
├── <Display /> ← expression + result
└── <Keypad /> ← сетка <Button />
</ProtectedRoute>
</Routes>
</AuthProvider>
</App>
```
## Слои приложения
### 1. AuthContext — псевдоавторизация
```
AuthContext {
user: string | null // текущий логин
login(name: string): void // сохранить в localStorage, установить user
logout(): void // очистить localStorage, user = null
}
```
- При инициализации читает `localStorage.getItem('calculator_user')`
- `login()` — записывает в localStorage, обновляет стейт
- `logout()` — удаляет из localStorage, обновляет стейт
- `ProtectedRoute` — обёртка, редиректит на `/login` если `user === null`
### 2. useCalculator — стейт-машина
Управляет всем состоянием калькулятора. Чистая логика, без побочных эффектов.
```
State {
display: string // текущее отображаемое число ("0")
expression: string // строка выражения ("12 + 3")
operator: string | null // текущий оператор
prevValue: number | null // предыдущее значение
waitingForOperand: boolean
lastResult: boolean // только что получили результат
}
Actions (dispatch):
inputDigit(digit: string)
inputDecimal()
inputOperator(op: string)
calculate()
clear()
backspace()
toggleSign()
percent()
```
Логика переходов:
```
[Ввод цифры]
waitingForOperand=true → display = digit, waiting = false
waitingForOperand=false → display = display + digit (макс 16 символов)
[Оператор (+, -, *, /)]
prevValue=null → сохранить display как prevValue, запомнить оператор
prevValue!=null → вычислить промежуточный результат, показать его
[Равно]
Вычислить prevValue {operator} display, показать результат
[C] → полный сброс к начальному состоянию
[Backspace] → удалить последний символ из display (минимум "0")
[+/-] → сменить знак display
[%] → display = display / 100
```
### 3. useKeyboard — слушатель клавиатуры
```
useKeyboard(actions: CalculatorActions): Map<string, string>
```
- Вешает `keydown` listener на `window`
- Маппинг клавиш → действий:
| key | action |
|-----|--------|
| `0`-`9` | `inputDigit(key)` |
| `.` | `inputDecimal()` |
| `+` `-` `*` `/` | `inputOperator(key)` |
| `Enter`, `=` | `calculate()` |
| `Escape` | `clear()` |
| `Backspace` | `backspace()` |
| `%` | `percent()` |
- Возвращает `activeKey` (текущая нажатая клавиша) для подсветки кнопки
- `preventDefault()` для `/` (чтобы не открывался поиск в браузере)
### 4. calculate.ts — вычисления
```typescript
function calculate(a: number, operator: string, b: number): number
```
- Чистая функция, без стейта
- Обработка деления на ноль → возвращает `Infinity` (дисплей покажет "Error")
- Округление для борьбы с погрешностью float (0.1 + 0.2)
### 5. Компоненты
**Display** — два блока:
- `expression` — мелкий шрифт, выражение сверху
- `result` — крупный шрифт, текущее число/результат снизу
- Auto-fit: `font-size` уменьшается при длине > 10 символов
**Keypad** — CSS Grid 4x5:
```
| C | +/- | % | ÷ |
| 7 | 8 | 9 | × |
| 4 | 5 | 6 | |
| 1 | 2 | 3 | + |
| 0 (span 2) | . | = |
```
**Button** — три варианта стиля:
- `default` — цифры (тёмно-серый фон)
- `function` — C, +/-, % (светло-серый фон)
- `operator` — +, -, ×, ÷, = (акцентный цвет)
- Пропс `active` — подсветка при нажатии с клавиатуры
## Адаптивность
```css
/* Калькулятор */
max-width: 360px; /* ограничение на больших экранах */
width: calc(100vw - 32px); /* на маленьких — почти вся ширина */
margin: 0 auto;
/* Центрирование на экране */
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* Кнопки */
min-height: 56px; /* тач-friendly */
font-size: clamp(16px, 4vw, 20px);
/* Дисплей результата */
font-size: clamp(28px, 8vw, 40px);
```
## Поток данных
```
[Keyboard Event] [Button Click]
│ │
▼ ▼
useKeyboard ──── action ────→ useCalculator (useReducer)
│ │
│ activeKey │ state
▼ ▼
<Keypad> ◄──── props ──── <Calculator>
│ │
▼ ▼
<Button active?> <Display expression, result>
```
Клавиатура и клики вызывают одни и те же actions хука `useCalculator`. Хук `useKeyboard` дополнительно пробрасывает `activeKey` для визуальной подсветки.
## Деплой и CI/CD
### Инфраструктура
```
Домен: test-calculator.vigdorov.ru
Namespace: test-calculator
Тип: web-frontend (SPA → nginx)
Registry: registry.vigdorov.ru/library/test-calculator
Ingress: Traefik + wildcard-cert
```
### Пайплайн (Drone CI)
```
git push (master)
[prepare] Клонирует ci-templates → .ci/
Парсит service.yaml
Копирует react.Dockerfile + spa.conf
Генерирует .ci/env
[build] Kaniko собирает образ:
- Stage 1: node:20-alpine → npm ci → npm run build
- Stage 2: nginx:alpine + dist + spa.conf
Push в Harbor: registry.vigdorov.ru/library/test-calculator:<sha>
[deploy] Helm upgrade --install в namespace test-calculator
Chart: ci-templates/helm-charts/frontend/
Ingress: test-calculator.vigdorov.ru → Service:80
[notify] Telegram уведомление
```
### service.yaml
```yaml
service:
name: test-calculator
type: web-frontend
deploy:
namespace: test-calculator
domain: test-calculator.vigdorov.ru
```
Минимальная конфигурация. ci-templates применит дефолты:
- `frontend.framework: react`
- `frontend.resources.cpu: 100m`
- `frontend.resources.memory: 128Mi`
- `frontend.replicas: 1`
- Dockerfile: `react.Dockerfile` (multi-stage: node → nginx)
- Nginx: `spa.conf` (gzip, кэш статики, SPA fallback, /health)
### Helm-чарт (frontend/)
```
Deployment
└── Pod (nginx:alpine)
├── /usr/share/nginx/html ← собранный SPA
└── /etc/nginx/nginx.conf ← spa.conf
Service (ClusterIP:80)
└── → Pod:80
Ingress (Traefik)
└── test-calculator.vigdorov.ru → Service:80
TLS: wildcard-cert (автоматически отражён в namespace)
```
### Схема сети
```
Браузер
│ HTTPS
DNS (cishost.ru) → VDS Nginx (reg.ru) → Keenetic Router
K3s Traefik
┌───────────────┘
Ingress: test-calculator.vigdorov.ru
Service: test-calculator (ClusterIP:80)
Pod: nginx (SPA)
```