UX: Настраиваемые веса POI (садик ≠ Мегамарт) — custom user profiles (расширяет #43) #114

Closed
opened 2026-05-12 15:38:15 +00:00 by lekss361 · 0 comments
lekss361 commented 2026-05-12 15:38:15 +00:00 (Migrated from github.com)

Эшелон: 🟢 UX / Configurability
Source feedback: Макс, 12.05.2026 14:01

Контекст

Существующий #43 I2 "Per-class weight profile" предлагал 3 фиксированных профиля (эконом / комфорт / бизнес). Макс хочет более гибкое:

«Надо дать возможность доставлять свои веса. Потому что садики и Мегамарт должны иметь разные веса.»

То есть — per-POI category веса (а не только per-housing-class). Пользователь должен иметь возможность настраивать веса:

  • Детсад × 2.0
  • Школа × 1.5
  • Поликлиника × 1.2
  • Мегамарт × 0.3
  • Парк × 1.8
  • Метро × 2.5
  • Промзона × -1.5 (negative)
  • ...

И сохранять свои профили для разных типов проектов.

Это расширение #43 I2 — не замена. 3 предустановки эконом/комфорт/бизнес остаются (как presets), но пользователь может создать свой профиль.

Что сделать

1. Backend

1.1. Schema data/sql/89_user_weight_profiles.sql

CREATE TABLE user_weight_profiles (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users(id),
  name TEXT NOT NULL,                    -- "Мой эконом", "Премиум", "Семейный"
  description TEXT,
  base_preset TEXT,                       -- "economy" | "comfort" | "business" | "custom"
  weights JSONB NOT NULL,                 -- { "kindergarten": 2.0, "shop_mall": 0.3, ... }
  is_default BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_id, name)
);

CREATE INDEX uwp_user_idx ON user_weight_profiles(user_id);
CREATE INDEX uwp_default_idx ON user_weight_profiles(user_id, is_default) WHERE is_default;

1.2. API endpoints

  • GET /api/v1/users/me/weight-profiles — список профилей пользователя
  • POST /api/v1/users/me/weight-profiles — создать
  • PATCH /api/v1/users/me/weight-profiles/{id} — изменить
  • DELETE /api/v1/users/me/weight-profiles/{id} — удалить
  • POST /api/v1/users/me/weight-profiles/{id}/set-default — make default
  • GET /api/v1/site-finder/poi-categories — список доступных POI categories с дефолтными весами

1.3. analyze_parcel принимает ?profile_id={id} query param

Если передан — применяет custom weights пользователя.
Если нет — fallback на default profile пользователя.
Если нет default — fallback на comfort preset.

1.4. POI Categories — полный список

DEFAULT_POI_CATEGORIES = {
  # Образование
  "kindergarten":      {"weight": 1.5, "label": "Детсад", "icon": "🏡"},
  "school":            {"weight": 1.5, "label": "Школа", "icon": "🏫"},
  "extracurricular":   {"weight": 0.8, "label": "Доп.образование (кружки)", "icon": "📚"},

  # Здравоохранение
  "polyclinic":        {"weight": 1.2, "label": "Поликлиника", "icon": "🏥"},
  "hospital":          {"weight": 0.8, "label": "Больница", "icon": "⚕️"},
  "dental":            {"weight": 0.5, "label": "Стоматология", "icon": "🦷"},

  # Транспорт
  "metro_stop":        {"weight": 2.0, "label": "Метро", "icon": "🚇"},
  "tram_stop":         {"weight": 1.0, "label": "Трамвай", "icon": "🚊"},
  "bus_stop":          {"weight": 0.5, "label": "Автобус", "icon": "🚌"},
  "highway_access":    {"weight": 0.8, "label": "Выезд на магистраль", "icon": "🛣"},

  # Торговля
  "shop_mall":         {"weight": 1.0, "label": "ТРЦ / Мегамарт", "icon": "🏬"},
  "shop_supermarket":  {"weight": 1.3, "label": "Супермаркет (продукты)", "icon": "🛒"},
  "shop_small":        {"weight": 0.4, "label": "Магазин у дома", "icon": "🏪"},

  # Спорт / Рекреация
  "park":              {"weight": 1.8, "label": "Парк", "icon": "🌳"},
  "fitness":           {"weight": 1.0, "label": "Фитнес / спорт", "icon": "💪"},
  "river":             {"weight": 1.2, "label": "Водоём", "icon": "🌊"},

  # Соц.инфраструктура
  "library":           {"weight": 0.5, "label": "Библиотека", "icon": "📖"},
  "theater":           {"weight": 0.7, "label": "Театр / музей", "icon": "🎭"},
  "religious":         {"weight": 0.3, "label": "Религиозный объект", "icon": "⛪"},

  # Negative (штрафы)
  "industrial_zone":   {"weight": -1.5, "label": "Промзона", "icon": "🏭"},
  "highway_noise":     {"weight": -1.0, "label": "Шумная магистраль", "icon": "🚦"},
  "cemetery":          {"weight": -1.2, "label": "Кладбище", "icon": "⚰️"},
  "prison":            {"weight": -1.0, "label": "Учреждение ФСИН", "icon": "🔒"},
}

Всего ~25 категорий.

2. Frontend

2.1. UI «Мои профили весов» в Site Finder

Сайдбар-панель:

  • Dropdown «Выбрать профиль»
  • Список профилей: Default presets (3) + Custom profiles (N)
  • Кнопка «Создать новый профиль»

Модалка создания/редактирования профиля:

  • Поле «Название»
  • Поле «Описание» (опц.)
  • Слайдеры по каждой POI category (default → custom override)
  • Группировка категорий: Образование / Здравоохранение / Транспорт / Торговля / etc
  • Toggle «Сделать дефолтным»
  • Live preview score для текущего открытого cad

Реактивный recompute:

  • При изменении слайдера — debounced (300мс) recompute analyze_parcel с custom profile
  • Сравнение «было vs стало» (delta-score)

2.2. Visualization

  • Stacked bar "Вклад каждой категории в total score" — после изменения весов сразу видно
  • Sankey diagram (опционально) — flow от POI category → score → final verdict

3. Migration от #43

  • #43 I2 становится частью этого issue (переименовать или закрыть после merge)
  • 3 preset'а (эконом / комфорт / бизнес) становятся read-only system profiles
  • Пользователь может склонировать preset и кастомизировать

Acceptance

  • Schema migrated, RLS на user_id
  • CRUD API работает
  • UI создания профиля с 25 слайдерами + live preview
  • При создании/изменении профиля — score test cad меняется в реальном времени
  • Default profile применяется автоматом в analyze
  • Backward-compat: query param ?class_profile=economy от #43 продолжает работать
  • Документация для пользователей: как настроить веса для разных проектов

Effort

~3-4 дня

  • Schema + CRUD API (1 день)
  • Backend integration в analyze_parcel (0.5 дня)
  • Frontend modal + слайдеры (1.5 дня)
  • Sankey / stacked bar viz (1 день)
  • Testing + docs (0.5 дня)

Owner

backend-engineer + database-expert (schema) + frontend-engineer (UI)

Dependencies

  • Supersedes #43 I2 (3-preset → flexible custom)
  • POI category data в БД должна иметь правильную классификацию (текущий osm_poi_ekb)

Replaces / Supersedes

  • #43 I2 — close после merge этого issue (или extend in scope)
  • Feedback source: Макс (12.05.2026 14:01)
  • Extends: #43 (closed/extended)
  • Related: #41 A3 per-category routing decay (может комбинироваться)
**Эшелон:** 🟢 UX / Configurability **Source feedback:** Макс, 12.05.2026 14:01 ## Контекст Существующий **#43 I2** "Per-class weight profile" предлагал **3 фиксированных профиля** (эконом / комфорт / бизнес). **Макс хочет более гибкое**: > «Надо дать возможность доставлять свои веса. Потому что садики и Мегамарт должны иметь разные веса.» То есть — **per-POI category веса** (а не только per-housing-class). Пользователь должен иметь возможность настраивать веса: - Детсад × 2.0 - Школа × 1.5 - Поликлиника × 1.2 - Мегамарт × 0.3 - Парк × 1.8 - Метро × 2.5 - Промзона × -1.5 (negative) - ... И **сохранять свои профили** для разных типов проектов. Это **расширение #43 I2** — не замена. 3 предустановки эконом/комфорт/бизнес остаются (как presets), но пользователь может **создать свой профиль**. ## Что сделать ### 1. Backend #### 1.1. Schema `data/sql/89_user_weight_profiles.sql` ```sql CREATE TABLE user_weight_profiles ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), name TEXT NOT NULL, -- "Мой эконом", "Премиум", "Семейный" description TEXT, base_preset TEXT, -- "economy" | "comfort" | "business" | "custom" weights JSONB NOT NULL, -- { "kindergarten": 2.0, "shop_mall": 0.3, ... } is_default BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, name) ); CREATE INDEX uwp_user_idx ON user_weight_profiles(user_id); CREATE INDEX uwp_default_idx ON user_weight_profiles(user_id, is_default) WHERE is_default; ``` #### 1.2. API endpoints - `GET /api/v1/users/me/weight-profiles` — список профилей пользователя - `POST /api/v1/users/me/weight-profiles` — создать - `PATCH /api/v1/users/me/weight-profiles/{id}` — изменить - `DELETE /api/v1/users/me/weight-profiles/{id}` — удалить - `POST /api/v1/users/me/weight-profiles/{id}/set-default` — make default - `GET /api/v1/site-finder/poi-categories` — список доступных POI categories с дефолтными весами #### 1.3. `analyze_parcel` принимает `?profile_id={id}` query param Если передан — применяет custom weights пользователя. Если нет — fallback на default profile пользователя. Если нет default — fallback на `comfort` preset. #### 1.4. POI Categories — полный список ```python DEFAULT_POI_CATEGORIES = { # Образование "kindergarten": {"weight": 1.5, "label": "Детсад", "icon": "🏡"}, "school": {"weight": 1.5, "label": "Школа", "icon": "🏫"}, "extracurricular": {"weight": 0.8, "label": "Доп.образование (кружки)", "icon": "📚"}, # Здравоохранение "polyclinic": {"weight": 1.2, "label": "Поликлиника", "icon": "🏥"}, "hospital": {"weight": 0.8, "label": "Больница", "icon": "⚕️"}, "dental": {"weight": 0.5, "label": "Стоматология", "icon": "🦷"}, # Транспорт "metro_stop": {"weight": 2.0, "label": "Метро", "icon": "🚇"}, "tram_stop": {"weight": 1.0, "label": "Трамвай", "icon": "🚊"}, "bus_stop": {"weight": 0.5, "label": "Автобус", "icon": "🚌"}, "highway_access": {"weight": 0.8, "label": "Выезд на магистраль", "icon": "🛣"}, # Торговля "shop_mall": {"weight": 1.0, "label": "ТРЦ / Мегамарт", "icon": "🏬"}, "shop_supermarket": {"weight": 1.3, "label": "Супермаркет (продукты)", "icon": "🛒"}, "shop_small": {"weight": 0.4, "label": "Магазин у дома", "icon": "🏪"}, # Спорт / Рекреация "park": {"weight": 1.8, "label": "Парк", "icon": "🌳"}, "fitness": {"weight": 1.0, "label": "Фитнес / спорт", "icon": "💪"}, "river": {"weight": 1.2, "label": "Водоём", "icon": "🌊"}, # Соц.инфраструктура "library": {"weight": 0.5, "label": "Библиотека", "icon": "📖"}, "theater": {"weight": 0.7, "label": "Театр / музей", "icon": "🎭"}, "religious": {"weight": 0.3, "label": "Религиозный объект", "icon": "⛪"}, # Negative (штрафы) "industrial_zone": {"weight": -1.5, "label": "Промзона", "icon": "🏭"}, "highway_noise": {"weight": -1.0, "label": "Шумная магистраль", "icon": "🚦"}, "cemetery": {"weight": -1.2, "label": "Кладбище", "icon": "⚰️"}, "prison": {"weight": -1.0, "label": "Учреждение ФСИН", "icon": "🔒"}, } ``` Всего ~25 категорий. ### 2. Frontend #### 2.1. UI «Мои профили весов» в Site Finder **Сайдбар-панель:** - Dropdown «Выбрать профиль» - Список профилей: Default presets (3) + Custom profiles (N) - Кнопка «Создать новый профиль» **Модалка создания/редактирования профиля:** - Поле «Название» - Поле «Описание» (опц.) - **Слайдеры по каждой POI category** (default → custom override) - Группировка категорий: Образование / Здравоохранение / Транспорт / Торговля / etc - Toggle «Сделать дефолтным» - Live preview score для текущего открытого cad **Реактивный recompute:** - При изменении слайдера — debounced (300мс) recompute `analyze_parcel` с custom profile - Сравнение «было vs стало» (delta-score) #### 2.2. Visualization - **Stacked bar** "Вклад каждой категории в total score" — после изменения весов сразу видно - **Sankey diagram** (опционально) — flow от POI category → score → final verdict ### 3. Migration от #43 - **#43 I2** становится частью этого issue (переименовать или закрыть после merge) - 3 preset'а (эконом / комфорт / бизнес) становятся **read-only system profiles** - Пользователь может склонировать preset и кастомизировать ## Acceptance - [ ] Schema migrated, RLS на user_id - [ ] CRUD API работает - [ ] UI создания профиля с 25 слайдерами + live preview - [ ] При создании/изменении профиля — score test cad меняется в реальном времени - [ ] Default profile применяется автоматом в analyze - [ ] Backward-compat: query param `?class_profile=economy` от #43 продолжает работать - [ ] Документация для пользователей: как настроить веса для разных проектов ## Effort ~3-4 дня - Schema + CRUD API (1 день) - Backend integration в `analyze_parcel` (0.5 дня) - Frontend modal + слайдеры (1.5 дня) - Sankey / stacked bar viz (1 день) - Testing + docs (0.5 дня) ## Owner `backend-engineer` + `database-expert` (schema) + `frontend-engineer` (UI) ## Dependencies - **Supersedes #43 I2** (3-preset → flexible custom) - POI category data в БД должна иметь правильную классификацию (текущий `osm_poi_ekb`) ## Replaces / Supersedes - **#43 I2** — close после merge этого issue (или extend in scope) ## Links - **Feedback source:** Макс (12.05.2026 14:01) - **Extends:** #43 (closed/extended) - **Related:** #41 A3 per-category routing decay (может комбинироваться)
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#114
No description provided.