From c60d394ff0cfcfc4e4ef0da7276fa3aa3df81195 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Mon, 25 May 2026 00:20:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein):=20=D1=8D=D1=82=D0=B0=D0=B6/?= =?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20option?= =?UTF-8?q?al=20+=20best=20test=20presets=20=D0=BF=D0=BE=20deal=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX: floor и total_floors теперь optional — все 6 мест в estimator.py уже имеют None-guards (Tier H/IMV/Cian валидируют наличие до вызова). При None — Tier H/IMV/Cian skip, оценка через Tier W (wide) с понижением confidence до medium/low. Critical guard: tradein.deals также не имеют жёстких NOT NULL на этих полях — schema-only change достаточен. Test presets: заменены на top-6 ЕКБ улиц по deal_count за 12 мес (rosreestr_deals ДКП квартиры): - Академика Ландау (686 сделок/год, Академический новостройки) - Малышева 84 (центр классика — оставлен) - Краснолесья (487 сделок/год, Академический эконом) - Космонавтов 50 (415 сделок/год, Уралмаш бюджет) - 8 Марта 80 (305 сделок/год, центр Ленинский) - Московская (426 сделок/год, центр премиум) Разброс: 4 района, размеры 35-75м², 1-3 комнаты, цены 120-185 К ₽/м². Все адреса дают medium+ confidence из-за объёма ДКП-сделок на улице. --- tradein-mvp/backend/app/schemas/trade_in.py | 4 +- .../src/components/trade-in/EstimateForm.tsx | 30 +++++--- .../src/components/trade-in/TestPresets.tsx | 76 +++++++++---------- tradein-mvp/frontend/src/types/trade-in.ts | 4 +- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/tradein-mvp/backend/app/schemas/trade_in.py b/tradein-mvp/backend/app/schemas/trade_in.py index 475f80e3..614383cd 100644 --- a/tradein-mvp/backend/app/schemas/trade_in.py +++ b/tradein-mvp/backend/app/schemas/trade_in.py @@ -16,8 +16,8 @@ class TradeInEstimateInput(BaseModel): address: str = Field(min_length=3, max_length=500) area_m2: float = Field(gt=10, lt=500) rooms: int = Field(ge=0, le=10) # 0 = студия - floor: int = Field(ge=1, le=100) - total_floors: int = Field(ge=1, le=100) + floor: int | None = Field(default=None, ge=1, le=100) + total_floors: int | None = Field(default=None, ge=1, le=100) year_built: int | None = Field(default=None, ge=1800, le=2100) house_type: Literal["panel", "brick", "monolith", "monolith_brick", "other"] | None = None repair_state: Literal["needs_repair", "standard", "good", "excellent"] | None = None diff --git a/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx b/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx index 2c37c266..b266a138 100644 --- a/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx @@ -54,11 +54,18 @@ function validate(f: FormState): string | null { if (!f.area_m2 || isNaN(a) || a <= 10 || a >= 500) return "Площадь: от 10 до 500 м²"; const r = Number(f.rooms); if (f.rooms === "" || isNaN(r) || r < 0 || r > 10) return "Комнат: 0 (студия) — 10"; - const fl = Number(f.floor); - if (!f.floor || isNaN(fl) || fl < 1 || fl > 100) return "Этаж: 1—100"; - const tf = Number(f.total_floors); - if (!f.total_floors || isNaN(tf) || tf < 1 || tf > 100) return "Этажей в доме: 1—100"; - if (fl > tf) return "Этаж не может быть больше этажей в доме"; + if (f.floor !== "") { + const fl = Number(f.floor); + if (isNaN(fl) || fl < 1 || fl > 100) return "Этаж: 1—100"; + if (f.total_floors !== "") { + const tf = Number(f.total_floors); + if (!isNaN(tf) && fl > tf) return "Этаж не может быть больше этажей в доме"; + } + } + if (f.total_floors !== "") { + const tf = Number(f.total_floors); + if (isNaN(tf) || tf < 1 || tf > 100) return "Этажей в доме: 1—100"; + } if (f.year_built !== "") { const y = Number(f.year_built); if (isNaN(y) || y < 1800 || y > 2100) return "Год постройки: 1800—2100"; @@ -71,8 +78,8 @@ function toPayload(f: FormState): TradeInEstimateInput { address: f.address.trim(), area_m2: Number(f.area_m2), rooms: Number(f.rooms), - floor: Number(f.floor), - total_floors: Number(f.total_floors), + floor: f.floor !== "" ? Number(f.floor) : null, + total_floors: f.total_floors !== "" ? Number(f.total_floors) : null, has_balcony: f.has_balcony, }; if (f.year_built !== "") out.year_built = Number(f.year_built); @@ -212,7 +219,7 @@ export function EstimateForm({ onSubmit, isPending, error }: Props) {
+ + 1-й и последний этаж снижают цену на 5–10% — заполни если знаешь + {/* Year + House type */}
diff --git a/tradein-mvp/frontend/src/components/trade-in/TestPresets.tsx b/tradein-mvp/frontend/src/components/trade-in/TestPresets.tsx index e629d77d..385047e8 100644 --- a/tradein-mvp/frontend/src/components/trade-in/TestPresets.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/TestPresets.tsx @@ -13,6 +13,19 @@ interface Preset { } const PRESETS: Preset[] = [ + { + label: "Академика Ландау · 1к/38м²", + hint: "Академический новостройки · 686 сделок/год", + data: { + address: "Екатеринбург, ул. Академика Ландау, 27", + area_m2: 38, + rooms: 1, + floor: 12, + total_floors: 25, + year_built: 2015, + house_type: "monolith", + }, + }, { label: "Малышева 84 · 2к/55м²", hint: "Центр · 36 аналогов", @@ -27,37 +40,37 @@ const PRESETS: Preset[] = [ }, }, { - label: "Куйбышева 48 · 3к/75м²", - hint: "Парковый · 22 аналога", + label: "Краснолесья · 3к/75м²", + hint: "Академический эконом · 487 сделок/год", data: { - address: "Екатеринбург, ул. Куйбышева, 48", + address: "Екатеринбург, ул. Краснолесья, 30", area_m2: 75, rooms: 3, floor: 7, - total_floors: 12, - year_built: 2005, - house_type: "monolith", + total_floors: 16, + year_built: 2010, + house_type: "panel", }, }, { - label: "Цвиллинга 58 · 1к/46м²", - hint: "Гольфстрим · 17 аналогов", + label: "Космонавтов 50 · 2к/55м²", + hint: "Уралмаш бюджет · 415 сделок/год", data: { - address: "Екатеринбург, улица Цвиллинга, 58", - area_m2: 46, - rooms: 1, - floor: 4, - total_floors: 25, - year_built: 2020, - house_type: "monolith", + address: "Екатеринбург, ул. Космонавтов, 50", + area_m2: 55, + rooms: 2, + floor: 5, + total_floors: 9, + year_built: 1975, + house_type: "panel", }, }, { - label: "8 Марта 50 · 1к/45м²", - hint: "Ленинский · 19 аналогов", + label: "8 Марта 80 · 1к/35м²", + hint: "Центр Ленинский · 305 сделок/год", data: { - address: "Екатеринбург, ул. 8 Марта, 50", - area_m2: 45, + address: "Екатеринбург, ул. 8 Марта, 80", + area_m2: 35, rooms: 1, floor: 3, total_floors: 9, @@ -66,31 +79,18 @@ const PRESETS: Preset[] = [ }, }, { - label: "Малышева 84 · 3к/68м²", - hint: "Центр · 33 аналога", + label: "Московская · 2к/55м²", + hint: "Центр премиум · 426 сделок/год", data: { - address: "Екатеринбург, ул. Малышева, 84", - area_m2: 68, - rooms: 3, - floor: 5, + address: "Екатеринбург, ул. Московская, 195", + area_m2: 55, + rooms: 2, + floor: 6, total_floors: 9, year_built: 1980, house_type: "brick", }, }, - { - label: "Белинского 200 · 2к/50м²", - hint: "ЮЗ · 7 аналогов · премиум", - data: { - address: "Екатеринбург, ул. Белинского, 200", - area_m2: 50, - rooms: 2, - floor: 6, - total_floors: 10, - year_built: 1990, - house_type: "panel", - }, - }, ]; interface Props { diff --git a/tradein-mvp/frontend/src/types/trade-in.ts b/tradein-mvp/frontend/src/types/trade-in.ts index 2766bcc9..97345d55 100644 --- a/tradein-mvp/frontend/src/types/trade-in.ts +++ b/tradein-mvp/frontend/src/types/trade-in.ts @@ -16,8 +16,8 @@ export interface TradeInEstimateInput { address: string; // min 3, max 500 area_m2: number; // 10 < x < 500 rooms: number; // 0-10 (0 = студия) - floor: number; // 1-100 - total_floors: number; // 1-100 + floor: number | null; // 1-100, optional + total_floors: number | null; // 1-100, optional year_built?: number; // 1800-2100 house_type?: HouseType; repair_state?: RepairState;