IM3-263: feat(market): 4 spot 기준 경쟁점 표시 통일 + 시뮬 결정성 + AI 요약 레이아웃#204
Merged
Conversation
증상: winner 동(예: 망원2동) 안 vacancy_spot 0건 시 1~4위 자리 비어 1순위 펄싱 핀만 (그것도 동 centroid 가짜 위치) 표시되던 회귀. 원인: buildBestVacancies 가 winner 동만 필터 → spot 부족 시 fallback 없음. 수정: winner 동 spot 우선 (backend score) + 부족분은 top3 동 spot 으로 채움 (listing_count 정렬). 항상 최대 4개 반환, 50m 근접 dedup 유지. - winnerSorted = winner 동 spot, score 정렬 - top3Sorted = top3 동 spot, listing_count 정렬 (score 없음) - merged → dedup → top 4 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/src/services/corp_brand_resolver.py: users.company_name → ftc_brand_franchise.corpNm 매칭, get_corp_industries / resolve_brand_for_industry - backend/src/main.py: GET /corp/operated-industries (JWT 기반 자동 추출), _validate_and_resolve_brand 헬퍼 + 7 endpoint Depends(get_optional_user) 통합 - backend/src/schemas/simulation_input.py: biz_number optional 필드 - frontend/src/api/client.ts: getOperatedIndustries() + OperatedIndustriesResponse 타입 - frontend/src/App.tsx: mount 시 fetch + 운영 외 frontend 라벨 disable + line-through + click toast 다업종 corp ((주)더본코리아 8업종 27 brand 등) 시 운영 외 업종 dropdown 차단. 비회원/CORP_NOT_IN_FTC: industries=null 반환 → 모든 업종 허용 (graceful degrade). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: winner=망원2동(매물 0건), spot 1위=망원1동(인접 동 매물) 케이스에서 사용자가 두 정보의 misalign 을 인지 못 함. 수정: TARGET 박스 아래에 amber warning 라벨 표시. - 조건: bestVacancy.dongName !== winner_district 일 때만 - 메시지: "추천 1순위 ○○동에 임대 매물이 없어, 인접 후보 동 △△동의 매물에서 공실 spot 을 자동 추천합니다" 분석 기준 동 변경 (옵션 2) 은 별도 작업으로 진행. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: winner=망원2동(매물 0건) 케이스에서 PHASE 2 LLM 에이전트들 (market/ population/legal/demographic/trend/competitor) 이 망원2동 기준으로 분석 → 화면 표시 분석 결과가 spot 1위 동(망원1동) 와 다른 동 가리켜 misalign. 수정: PHASE 2 진입 시 target_district 결정 로직 변경. - 1순위: winner 동 spot 중 score(또는 listing_count) 1위 → 그 동 - 2순위: top3 동 spot 중 listing_count 1위 (winner 동 매물 0건일 때) - fallback: winner_district frontend buildBestVacancies 와 동일 로직 — 분석 기준과 화면 표시 일관. winner_district 변수는 그대로 보존 → 노란 강조·라벨·랭킹 정보 미영향. 캐시 키는 target_district 따라 자동 분리 (spot 동 캐시 신규 산출). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: - 주요 경쟁점 카드: '컴포즈커피 망원시장점 50m' (분석 동 centroid 기준) - 지도 마커: 그 매장이 안 보임 (all_competitor_locations 누락) - 주요 경쟁점 카드의 거리가 spot 1위 좌표 기준 아님 (예: 무흐 82m 누락) 원인: 두 데이터 소스가 분리됨. - competitor_intel.samples: 분석 동 centroid 500m, top 20 cap - all_competitor_locations: 4동 centroid 1.5km, 357개 (samples 외 매장) 수정: - MapSection.buildCompetitors: all_competitor_locations + samples union → place_name + 좌표 dedup → 200 cap. 지도 마커에 두 소스 매장 모두 포함 (컴포즈 망원시장점 누락 해소). - MarketTab.topCompetitors: spot 1위 좌표(vacancy_spots top1) 결정 후 모든 매장의 haversine 재계산 → 가까운 5개. spot 1위 좌표 기준이라 무흐 82m 같은 진짜 가까운 매장이 카드 상단에 표시. → 카드와 지도 모두 spot 1위 좌표 기준 일관된 정보. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: 1. '동 한눈에' 의 공실률이 winner 동(망원2동) 데이터로 표시 → district_rankings[winner].vacancy_rate 직접 참조 2. 마켓맵 위 작은 안내 라벨이 한눈에 안 들어옴 수정: 1. MarketTab 에 analysisDong 변수 추가 (spot 1위 동 우선, fallback winner) _spot1Info 에 dongName 포함 → analysisDong 추출 WinnerDistrictSummary props 에 analysisDong 전달, district_rankings 검색 시 사용 → 공실률 = analysisDong (망원1동) 데이터로 일관 2. 안내 라벨 → 큰 amber 배너로 강조 · AlertTriangle 아이콘 + "추천 입지 안내 — 매물 자동 보정" 헤더 · "○○동에 매물 없어 △△동의 매물에서 추천" 본문 · 추가 라인: "분석 결과/주요 경쟁점/동 한눈에 모두 △△동 기준으로 도출" · border-2 + box-shadow glow 로 시각 강조 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이슈 ① — spot 1위 기준 반경 500m 경쟁업체 매 시뮬마다 바뀜
Root cause: _load_vacancy_spots SQL 에 ORDER BY 없음 → PG 자연순서 비결정
→ vacancy_spots ordering 매번 다름 → competitor_intel _matched[0] 비결정
→ spot_lat/lon 캐시 키 매번 다름 → 카니발 분석/sample 새로 생성
Fix A: ORDER BY NaverVacancy.id 추가 (district_ranking.py)
Fix B: competitor_intel _matched 를 score 1위 (없으면 listing_count) 정렬
→ frontend bestVacancy 좌표와 backend 분석 기준점 완전 통일
이슈 ② — 주요 경쟁점 5개 지도에 강조 표시
· MarketMap topCompetitors prop 추가 (rank, lat, lng)
· 좌표 매칭 시 노란 큰 동그라미(28px) + 번호 라벨 + zIndex=6
· MapSection → MarketMap 흐름 연결, MarketTab 에서 topCompetitors 전달
· 범례에 "주요 경쟁점 (1~5위)" 항목 추가
이슈 ③ — 다른 경쟁점 마커 팝업창 가려져 안 보임
Root cause: Kakao InfoWindow 가 CustomOverlay (펄싱핀 zIndex=5/번호핀=5) 에 가려짐
Fix: InfoWindow → CustomOverlay (zIndex=100) 로 통일
· X 버튼 직접 부착 (CustomOverlay 엔 close UI 없음)
· 마커 위쪽 화살표 + transform translateY(-10px) 로 깔끔한 배치
· 경쟁점/자사매장 두 핸들러 모두 openPopup 헬퍼로 통합
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회귀: 이전 commit 에서 top5 매장을 노란 동그라미로 '치환' 해서 반경 안 빨간 삼각형이 5개 사라진 것처럼 보이는 문제. 사용자 보고: "반경 내 경쟁점 어디 갔냐". 수정: · 빨간 삼각형은 모든 경쟁점에 항상 그대로 그림 (반경 내/외 색·크기 분기 보존) · top5 매장은 그 위에 별도 노란 번호 badge overlay 추가 (yAnchor=1.6 으로 위쪽 띄움) · 매장 누락 X — 시각적으로 빨간 삼각형 + 노란 번호 둘 다 보임 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회귀:
MarketTab 의 topCompetitors 가 IIFE 라 매 렌더마다 새 배열 참조 생성.
→ MapSection.topCompetitorsForMap useMemo 매 렌더 새 배열
→ MarketMap useEffect deps (topCompetitors) 매 렌더 변경
→ useEffect 재실행 → cleanup 으로 모든 마커 setMap(null) → 재생성 직전 또 deps 바뀜
→ 마커 그려지자마자 지워지는 무한 루프 → 화면에 빨간 삼각형/노란 핀 모두 안 뜸
Fix: useMemo([simResult, _spot1.lat, _spot1.lng]) 로 안정 참조.
simResult 동일하면 동일 배열 참조 유지 → useEffect 재실행 안 일어남.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
요구:
사용자 보고 — 풀이 4동 centroid 기준 1.5km 라 spot 1위 좌표 가장자리 매장이
cap 또는 race condition 으로 누락. spot 좌표 기준으로 모든 분석 일치 필요.
변경:
· _resolve_spot1_coord(result) helper 추가 — vacancy_spots 의 score 1위 좌표 추출
(graph._resolve_spot_dong / frontend buildBestVacancies 와 동일 규칙)
· _query_kakao_store_by_coord(lat, lon, keyword, radius_m, limit) helper 추가
bounding box → haversine 정확 필터 → ORDER BY kakao_id 결정성 + (distance, id) tie-break
· _collect_all_competitor_locations 시그니처 확장:
spot_lat/spot_lon 인자 있으면 spot 좌표 단일 검색 (1.5km, top 400)
없으면 fallback: 4동 centroid 분기 (구버전 호환, set→sorted 결정성)
· 호출처 4곳 (predict / analyze/llm / analyze/llm/async / simulate) 에서
_resolve_spot1_coord 로 spot 좌표 추출 후 인자 전달
효과:
· 풀 자체가 spot 좌표 기준 → 가장자리 매장 누락 없음
· 4동 centroid race condition (asyncio.gather + seen_ids) 자동 제거
· spot 좌표 결정적 (Fix A 효과) + SQL ORDER BY 결정적 → 시뮬마다 동일 결과
· cap 100→400 으로 spot 좌표 1.5km 안 동종 매장 모두 포함 (마포 카페 ~340개 커버)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
요구:
사용자 — "공실 spot 1,2,3,4 모두 기준으로 경쟁점 내부/외부 표시"
backend (main.py):
· _resolve_top_spot_coords(result, max_n=4) helper — 4개 spot 좌표 list 반환
(frontend buildBestVacancies 와 동일: winner score 정렬 → top3 listing_count → 50m dedup)
· _collect_all_competitor_locations 에 spot_coords list 인자 추가
각 spot 별 1.5km × top 400 검색 후 합집합 + dedup (kakao_id 기준)
결과 정렬: spot1 좌표 기준 거리순 (frontend topCompetitors 와 정합)
· 호출처 4곳 (predict / analyze/llm / analyze/llm/async / simulate) 모두 spot_coords 전달
frontend:
· MarketMap within 판정 = 4 spot 중 최단 거리 ≤ radius 면 내부
targetSpots 그대로 사용 (Layer 4 에서 2~4위 반경원 그릴 때 쓰는 prop 재활용)
· MapSection legend "내부 X / 총 Y" 카운트도 동일 4 spot union 기준
효과:
· 4 spot 어느 하나라도 500m 안 매장 = 진한 빨간 삼각형 (내부)
· 풀 합집합으로 spot2,3,4 가장자리 매장도 포함 (이전 spot1 단일 1.5km 누락 해소)
· 결정성 유지 — _resolve_top_spot_coords + spot_coords list 모두 결정적, ORDER BY id
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…된 문제 증상: 4 spot 합집합 풀이 backend 에서 spot1 좌표 거리순 정렬로 반환되는데, frontend buildCompetitors cap 200 이 spot1 가까운 매장 200개만 유지 → spot 2,3,4 주변 매장은 spot1 기준 멀어서 cap 에 잘려 지도에서 사라짐. Fix: · frontend buildCompetitors cap 200 → 1000 (4 spot × 1.5km 합집합 모두 보존) · backend spot_limit 400 → 800 (한 spot 1.5km 안 카페가 400개 넘는 동 대비) 마포 카페 전체가 수백개 수준이라 1000 cap 에 도달하는 일은 사실상 없음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t fallback 원인 증상: spot 모드 진입했는데 _query_kakao_store_by_coord 가 ModuleNotFoundError: No module named 'src.database.connection' 발생. 호출처 try/except 가 exception 잡아 all_competitor_locations=[] 반환. frontend 는 samples 20개만 사용하는 else 분기로 빠져 풀이 20개로 보임. Fix: from src.database.connection import get_sync_engine → from src.database.sync_engine import get_sync_engine 실제 모듈 위치 확인 (commercial_intelligence.py 와 동일 import 패턴). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontend:
· MarketMap.tsx — top5 노란 뱃지 클릭 가능 (이전 pointer-events:none 회귀 fix,
같은 경쟁점 popup 열림)
· DemographicTab.tsx, FlowVsRevenueScatter.tsx — 이전 세션 demographic 작업분
· types/index.ts — demographic 스키마 타입 확장
backend:
· demographic_depth.py — 이전 세션 demographic 분석 로직 변경
· synthesis.py — 이전 세션 변경
· schemas/demographic.py — 이전 세션 스키마 확장
TS check + AST parse 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
공실 spot 1~4 모두 기준으로 경쟁점 풀 합집합 + within/외부 분류 (winner centroid →
spot 좌표로 통일)
주요 변경
Backend
_resolve_top_spot_coords— 4 spot 좌표 list (winner score → top3 listing_count →50m dedup)
_query_kakao_store_by_coord— spot 좌표 SQL (ORDER BY kakao_id결정성)_collect_all_competitor_locationsspot_coords인자 추가 (각 spot 1.5km × 800합집합)
_load_vacancy_spotsSQLORDER BY id(PG 자연순서 비결정 root cause fix)competitor_intel._matchedscore 1위 정렬 (frontend bestVacancy 와 분석 기준 통일)Frontend
Test plan
[all_competitors:spots] N spot ... 최종 합집합 M개로그 (M≥200)