Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
96d4ba8
fix(map): 공실 spot 1~4위 항상 4개 보장 — winner+top3 통합 후보
yezin013 May 6, 2026
66b874e
feat(corp): 사업자번호 기반 운영 업종 dropdown 자동 차단
bat1120 May 6, 2026
ba01036
feat(map): winner != spot 1위 동 안내 라벨 추가
yezin013 May 6, 2026
e9b65f7
feat(graph): PHASE 2 분석 target 을 spot 1위 동으로 변경 — winner 매물 0건 케이스 대응
yezin013 May 6, 2026
20f6fe3
fix(market): 주요 경쟁점·지도 마커 데이터 통합 + spot 1위 좌표 기준 거리
yezin013 May 6, 2026
0d32c8e
fix(market): 동 한눈에 공실률 + 안내 라벨 — analysisDong(spot 1위 동) 기준 + 큰 배너
yezin013 May 6, 2026
2c5a310
fix(market): 시뮬마다 경쟁업체 변동 + 팝업 가려짐 + top5 지도 강조
yezin013 May 6, 2026
edb4e54
fix(market): top5 매장이 빨간 삼각형으로 안 그려지던 버그 — 번호 라벨 별도 overlay 로 분리
yezin013 May 6, 2026
ccf2775
fix(market): topCompetitors useMemo 누락으로 마커 무한 cleanup → 화면 미표시 회귀
yezin013 May 6, 2026
6a5c30c
feat(market): all_competitor_locations 수집 기준을 공실 spot 1위 좌표로 통일
yezin013 May 6, 2026
4e27f01
feat(market): 4 spot 모두 기준으로 경쟁점 내부/외부 분류 + 풀 합집합 수집
yezin013 May 6, 2026
7121753
fix(market): 풀 cap 200 → 1000 — spot 2,3,4 주변 매장이 spot1 거리순 정렬에 잘려 누락…
yezin013 May 6, 2026
6a7fc60
fix(market): _query_kakao_store_by_coord import 경로 오류 — spot 모드 silen…
yezin013 May 6, 2026
d04b31e
chore: PR 포함 자투리 변경 통합 commit
yezin013 May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions backend/src/agents/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,68 @@ async def llm_analysis_phase_node(state: AgentState) -> dict:
"""
Phase 2: 6개 LLM 에이전트 병렬 실행

target_district를 Phase 1에서 확정된 winner_district로 덮어쓰고 실행.
이로써 시장/인구/법률 분석 데이터가 추천 1위 동을 기준으로 생성됨.
target_district 결정 우선순위 (2026-05-06 변경):
1. spot 1위 동 (실제 입주 가능 매물 위치) — winner 동 spot 우선,
부족 시 top3 동 spot 으로 채움 (frontend buildBestVacancies 와 동일 로직).
2. winner_district fallback (vacancy_spots 데이터 자체 없을 때).

의도: winner=망원2동(매물 0건) + spot 1위=망원1동(인접 동 매물) 케이스에서
분석 결과 (시장/인구/법률 등) 가 winner 가 아닌 실제 매물 동 기준으로 산출되어
화면 정보 일치 (사용자 요구).

winner_district 자체는 그대로 보존 (노란 강조·라벨 표시용).
"""
t_start = time.perf_counter()

# winner_district를 분석 기준동으로 사용 (Phase 1에서 확정)
winner = state.get("winner_district") or state.get("target_district", "")
original_target = state.get("target_district", "")
if winner and winner != original_target:
top3 = state.get("top_3_candidates") or []
vacancy_spots = state.get("vacancy_spots") or []

def _resolve_spot_dong() -> str:
"""spot 1위 동 결정 — frontend buildBestVacancies 와 동일 로직."""

def _is_valid(s: dict) -> bool:
lat, lon = s.get("lat"), s.get("lon")
return isinstance(lat, (int, float)) and isinstance(lon, (int, float))

# 1순위: winner 동 spot 중 score(또는 listing_count) 1위
winner_spots = [s for s in vacancy_spots if s.get("dong_name") == winner and _is_valid(s)]
if winner_spots:
winner_spots.sort(
key=lambda s: (
-(s.get("score") if isinstance(s.get("score"), (int, float)) else float("-inf")),
-(s.get("listing_count") or 0),
)
)
return str(winner_spots[0].get("dong_name") or winner)
# 2순위: top3 동 spot 중 listing_count 1위 (winner 동 매물 0건 케이스)
top3_set = set(top3)
top3_spots = [
s for s in vacancy_spots if s.get("dong_name") in top3_set and s.get("dong_name") != winner and _is_valid(s)
]
if top3_spots:
top3_spots.sort(key=lambda s: -(s.get("listing_count") or 0))
return str(top3_spots[0].get("dong_name") or winner)
return winner # fallback

spot_dong = _resolve_spot_dong()
analysis_target = spot_dong or winner

if winner and analysis_target != winner:
print(
f"--- [PHASE 2] target_district 교체: {original_target} → {analysis_target} "
f"(winner={winner}, spot 1위 동 기준 분석 — winner 동 매물 부족) ---"
)
elif winner and winner != original_target:
print(f"--- [PHASE 2] target_district 교체: {original_target} → {winner} (winner 기준 분석) ---")
else:
print(f"--- [PHASE 2] target_district={winner} (변경 없음) ---")
print(f"--- [PHASE 2] target_district={analysis_target} (변경 없음) ---")

# winner를 target_district로 주입한 상태로 LLM 에이전트 실행
# spot 1위 동을 target_district로 주입한 상태로 LLM 에이전트 실행.
# winner_district 는 그대로 보존 (별도 표시용).
analysis_state = dict(state)
analysis_state["target_district"] = winner
analysis_state["target_district"] = analysis_target

print("--- [PHASE 2] 6개 LLM 에이전트 병렬 실행 시작 ---")
(
Expand Down
11 changes: 10 additions & 1 deletion backend/src/agents/nodes/competitor_intel.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,23 @@ def _make_competitor_attr(
}

# winner 매물 좌표 (legal agent 와 동일 기준점) — 일관성 확보.
# state.vacancy_spots 에서 winner_district 매칭 spot 추출, 없으면 첫 spot.
# graph.py 가 target_district 를 spot 1위 동으로 이미 교체한 상태로 진입.
# → 그 동의 spot 중 score 1위 (없으면 listing_count 1위) 좌표 사용.
# frontend buildBestVacancies 의 1위 spot 좌표와 동일 — 분석 기준점 완전 통일.
# 이전 _matched[0] 은 정렬 없는 자연순서 첫 spot 이라 비결정적이었음.
spot_lat: float | None = None
spot_lon: float | None = None
_vac_spots = state.get("vacancy_spots") or []
if isinstance(_vac_spots, list) and _vac_spots and target_district:
_matched = [
s for s in _vac_spots if isinstance(s, dict) and s.get("dong_name") == target_district
]
_matched.sort(
key=lambda s: (
-(s.get("score") if isinstance(s.get("score"), (int, float)) else float("-inf")),
-(s.get("listing_count") or 0),
)
)
_spot = _matched[0] if _matched else _vac_spots[0]
try:
_slat = _spot.get("lat") if isinstance(_spot, dict) else None
Expand Down
Loading
Loading