From 9d75ca4874f8c866962f67ff6f677940da3b54c1 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Tue, 30 Jun 2026 18:57:48 +0500 Subject: [PATCH] =?UTF-8?q?fix(site-finder):=20=D0=BF=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D0=BF=20=D0=BA=D0=BE=D0=BD=D0=BA=D1=83=D1=80=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=20=E2=80=94=20NUMERIC-=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=85=D0=BE=D0=B4=D1=8F=D1=82=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BA=D0=BE=D0=B9,=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=BC=20=D1=86=D0=B5=D0=BD=D1=83=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=BE=D1=89=D0=B0=D0=B4=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прод-verify #2113 вскрыл: avg_price_per_m2_rub и avg_area_pd приходят с бэкенда СТРОКОЙ («160345», «47.5») — Pydantic v2 сериализует Decimal (ROUND(::numeric)) в строку. Гварды `typeof === "number"` / `Number.isFinite(str)` молча роняли поля: цена конкурента вообще не показывалась, а добавленная в #2113 «Ср. площадь кв.» не рендерилась. - toFiniteNumber(v) — приводит number|string→number|null. - formatPrice использует его (цена снова показывается). - avg_area_pd рендерится через toFiniteNumber. - Типы avg_price_per_m2_rub/avg_area_pd расширены до number|string|null (честно отражает рантайм). Frontend-only, контракт не меняется. --- .../components/site-finder/MarketLayers.tsx | 27 ++++++++++++------- frontend/src/types/site-finder.ts | 9 ++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/site-finder/MarketLayers.tsx b/frontend/src/components/site-finder/MarketLayers.tsx index 3986019e..8150ba01 100644 --- a/frontend/src/components/site-finder/MarketLayers.tsx +++ b/frontend/src/components/site-finder/MarketLayers.tsx @@ -75,9 +75,19 @@ function formatDistance(m: number | null | undefined): string | null { return `${Math.round(m).toLocaleString("ru-RU")} м`; } -function formatPrice(rub: number | null | undefined): string | null { - if (rub === null || rub === undefined || !Number.isFinite(rub)) return null; - return `${Math.round(rub).toLocaleString("ru-RU")} ₽/м²`; +// Бэкенд отдаёт NUMERIC-поля (avg_price_per_m2_rub, avg_area_pd) как СТРОКИ +// («160345», «47.5») — Pydantic v2 сериализует Decimal в строку. Приводим к числу, +// иначе typeof/Number.isFinite-гварды молча роняют поле (попап не показывал цену). +function toFiniteNumber(v: number | string | null | undefined): number | null { + if (v === null || v === undefined) return null; + const n = typeof v === "string" ? Number(v) : v; + return Number.isFinite(n) ? n : null; +} + +function formatPrice(rub: number | string | null | undefined): string | null { + const n = toFiniteNumber(rub); + if (n === null) return null; + return `${Math.round(n).toLocaleString("ru-RU")} ₽/м²`; } function formatReadyDt(iso: string | null | undefined): string | null { @@ -406,12 +416,11 @@ export function MarketLayers({ {priceStr && (
{priceStr}
)} - {typeof c.avg_area_pd === "number" && - Number.isFinite(c.avg_area_pd) && ( -
- Ср. площадь кв.: {c.avg_area_pd} м² -
- )} + {toFiniteNumber(c.avg_area_pd) !== null && ( +
+ Ср. площадь кв.: {toFiniteNumber(c.avg_area_pd)} м² +
+ )} {showSales && (
Продано: {c.units_sold ?? 0} diff --git a/frontend/src/types/site-finder.ts b/frontend/src/types/site-finder.ts index b9562e2d..1ba483c3 100644 --- a/frontend/src/types/site-finder.ts +++ b/frontend/src/types/site-finder.ts @@ -50,13 +50,14 @@ export interface ParcelAnalysisCompetitor { lon?: number | null; // Рыночные метрики из objective_lots (LEFT JOIN → null без маппинга). // Используются в popup-слое (цена/распроданность как velocity-прокси). - avg_price_per_m2_rub?: number | null; + // NUMERIC-поля приходят СТРОКОЙ (Pydantic v2 сериализует Decimal в строку), + // поэтому number | string — приводить к числу через toFiniteNumber перед форматом. + avg_price_per_m2_rub?: number | string | null; units_sold?: number | null; units_available?: number | null; // #2111 — средняя площадь квартиры (м²) из objective_lots. Backend кладёт через - // loose competitors[] dict-passthrough. Optional + nullable: тонкий стаб/старый - // кэш могут не содержать; popup рендерит только при наличии. - avg_area_pd?: number | null; + // loose competitors[] dict-passthrough (Decimal → строка). Optional + nullable. + avg_area_pd?: number | string | null; } export interface ParcelAnalysisDistrict { -- 2.45.3