feat(tradein): «По вашей улице» — StreetDealsCard на real ДКП-сделки Росреестра #556
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#556
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-street-deals-frontend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Новая секция
<StreetDealsCard>после<DealsCard>на trade-in результатах. Питается отGET /api/v1/trade-in/street-deals(PR #555). Показывает:Empty graceful: не рендерим если
street === nullилиcount === 0.Why per-street
Open dataset Росреестра агрегирует адреса до улицы (без номера дома) — см. Fix_Rosreestr_Dkp_Filter_May24. Endpoint фильтрует по street + rooms + area BETWEEN target ± 15% → 5-60 матчей по большинству ЕКБ-улиц с реалистичной median.
Verified на проде:
Космонавтов, 50 / 55м² / 2-комн→ 62 ДКП-сделок median 150 К ₽/м²{street:null, count:0, deals:[]}(для несуществующих улиц)Changes (5 файлов)
types/trade-in.ts—StreetDealsResponseinterface послеHouseAnalyticsResponselib/trade-in-api.ts—useStreetDeals(address, area_m2, rooms)hook сqueryKey=[trade-in, street-deals, ...], staleTime 5 минcomponents/trade-in/StreetDealsCard.tsx— NEW, reuse existing.card/.count-strip/.dtстили, reuseopenRosreestrWithAddressизlib/rosreestr.tsapp/page.tsx— import + mount<StreetDealsCard estimate={resultData.estimate} />после<DealsCard />Решение по диапазону
Reнgе показан как
X – Y млн ₽черезrange_low_rub/1_000_000— безопаснее предложенного hack'а сdeals[0].area_m2(защита от empty array).Verification
После deploy секция должна появиться в UI ниже секции 3 «Сделки».
Refs: завершает 4-PR roadmap (PR-A #549 → PR-D #550 → PR-C-back #555 → PR-C-front).
Deep Code Review #556 —
StreetDealsCard(real ДКП-сделки Росреестра)Verdict: APPROVE. Tight 4-file UI integration consuming
/street-deals(PR #555). Contract verified end-to-end; follows established peer-card conventions perfectly.Cross-contract verification with PR #555 (1:1 match)
BASE = /api/v1/trade-in+ path/street-deals✓address/area_m2/rooms✓ (matches FastAPI signature)street | period_from | period_to | count | median_price_rub | median_price_per_m2 | range_low_rub | range_high_rub | deals[AnalogLot]— 1:1 with PydanticStreetDealsResponsedate→ ISO string serialization; TSstringcorrect_empty()/street=None/count=0→ frontendnullrender — symmetricCorrectness
useQueryqueryKey=["trade-in","street-deals",address,area_m2,rooms]— stable, scoped, cache-friendlyenabled: !!address && area_m2 !== null && rooms !== null— gates correctly (note:0is falsy butrooms=0for studio is allowed — the explicit!== nullkeeps that path open ✓)isLoading || isError → null,!data || !street || count===0 → null— defensive, matches peerDealsCard.tsxrange_low_rub / 1_000_000.toFixed(1)— safer than the earlierdeals[0].area_m2hack (PR body decision noted, agree)URLSearchParams.toString()auto-encodes cyrillic addresses → no URL-injectionSecurity / XSS
{d.address},{data.street}) — React escapesdangerouslySetInnerHTML, no URL props (Rosreestr button uses fixed constantROSREESTR_REQUEST_URLfromlib/rosreestr.tsPR #550)source_urlfromAnalogLotnot rendered here → nosafeUrlneededDesign-tokens / CSS
All referenced classes verified to exist in
components/trade-in/trade-in.css:.card,.card-head,.card-meta,.section-kicker,.count-strip,.count-cell,.label,.value,.unit,.mono,data-tnum,.dt,.addr,.a-main,.rosreestr-btn,.table-foot. Zero new CSS, full reuse — consistent with peer cards (#529/#546).Accessibility
<table className="dt" aria-label="Сделки по улице">— matches peerDealsCardexactly (aria-label="Фактические сделки")<th>withoutscope/ no<caption>— same as peer; broader a11y refactor would be cross-component scope, out of bounds for this PR (no-silent-regression rule)<button type="button" title="...">— explicit type, descriptive title ✓Performance / re-renders
staleTime: 5*60_000(5 min) — matches peer hooks (useEstimateHouseAnalyticsetc.)data.deals.mapover ≤10 rows — negligibleMinor (non-blocking, all match peer conventions)
key={i}(index) — acceptable for static top-10 list with no reordering (peerDealsCarddoes same)(d: AnalogLot, i: number)annotation — redundant but harmlessstyle={{ color: "var(--fg)" }}— matches peer patternиз {data.count} сделокis grammatically correct only for genitive plural (5-20, 25-30...). Edgecount=1→ "из 1 сделок";count=2/3/4→ "из 2/3/4 сделок". Same convention as peerDealsCard("Показано N фактических сделок") — out-of-scope to fix repo-wide here. The 12-month street-level filter typically yields 5-60 hits, so the common case is correct. Worth a follow-upformatPlural()helper PR later — not blocking.Positive
range_low_rubdirect field vsdeals[0]hack) — correct guard against empty-array bugRisk / reversibility
DealsCard; renders null in all error/empty paths.Proceeding to squash-merge per any-scope auto-merge policy.