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

11 KiB
Raw Permalink Blame History

Архитектура 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 — вычисления

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 — подсветка при нажатии с клавиатуры

Адаптивность

/* Калькулятор */
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

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)