MSG-489 feat: [웹] AI 경로추천 — 지역 자동 이동·1km 축척 고정·2차 자동 재요청·출발지 자동 판정 - #105
Conversation
- 제출하면 항상 현재 중심 기준 1km(MAP_SCALE_1KM_ZOOM)로 맞춘 뒤 요청한다("다시 짜기"도 동일). 이미 1km면 줌 명령 없이 즉시 발사 — 같은 값 zoomTo는 idle을 내지 않아 요청이 나가지 않는다
- 종전 "뷰포트 한 변 0.5도 초과일 때만 정규화"(needsSpanNormalize) 규칙은 위 규칙에 흡수·폐기. 다만 서버 상한 가드는 exceedsViewportSpan으로 부활시켜 **발사 직전**에 둔다 — 정규화 대기·상한 만료·2차를 포함한 발사 4경로가 모두 send() 한 지점을 지난다
- 줌 맞추는 동안 로딩 표시(normalizePending). rate-limit 기준 시각 requestedAt은 실제 mutate 시점에만 기록해 2차 조기 발사(14429)를 막는다
- 2차 자동 재요청은 "목표 뷰포트 도달" 판정 후 발사하고, 도달 실패 시에도 상한 뒤 종결한다 — 영구 로딩과 확정 실패 요청 둘 다 금지
- 탭이 숨은 동안에는 정착 상한 타이머를 걸지 않는다. 백그라운드 탭에서는 네이버 지도가 idle을 내지 않아 뷰포트 스토어가 옛 값에 머물고, 그 상태로 상한이 발화해 정규화되지 않은 범위(1.9°x4.7°)로 요청이 나가 서버 400(14401)이 재현됐다
- /ai-route에서 다른 집계 클러스터 마커(지역 점령·미션·핫구역)를 숨긴다 — gateRouteClusters를 MapShell 파생 결과에만 적용. 집계층은 줌 16(100m) 미만에서만 존재하므로 경로 게이트 하나로 충족된다. 홈·도감은 불변
- 검증: 결함 재현 시나리오에서 과대 뷰포트 요청 0건·복귀 후 정상 발사, 세션 recommend 7건 전건 span<=0.5°·200. 클러스터 /ai-route 0개 vs 홈 동일 뷰포트 3개. S6 간격 10,498ms·14429 없음, 결과 후 확대 시 추가 호출 0. 게이트 6종 green(web 1472 tests)
- 14429(요청 과다) 안내를 서버와 같은 문장 "요청이 너무 잦습니다. 잠시 후 다시 시도해주세요"로 교체 — 종전 문구는 사유를 알려주지 않아 재시도를 연속으로 누른 사용자가 같은 안내만 반복해서 봤다. FE 고정 문구 상수이며 서버 응답 문자열을 렌더하지는 않는다(MSG-488 §1-5 정책 유지)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX
브라우저 자동화 탭이 백그라운드면 rAF가 0.5~1fps로 얼어 계측이 35배까지 부풀려진다(같은 줌 전이가 숨은 탭 11,162ms · 전면 탭 316ms). 그 값이 "정규화 대기 14.2초 무피드백"이라는 리포트 문장과 후속 권장까지 만들어 냈고, 더 나쁜 것은 숨은 탭에서는 지도가 idle을 내지 않아 동작 자체가 달라진다는 점이다 — 오염된 계측이 실제 결함(14401)을 가리고 있었다. 계측 신뢰성을 검증자의 주의력이 아니라 절차의 전제 조건으로 내린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX
codex 리뷰(push 전 게이트) 지적 2건 반영. - [P1] 2차 요청 인스턴스에 onLoginRequired가 없어, 1차 성공과 지연된 2차 사이에 세션이 만료되면 401에도 로그인 모달이 뜨지 않았다. secondary 옵션은 onMutate만 분기하고 onError는 공통 경로라 콜백 주입 한 줄로 1·2차 동작이 같아진다. 문서에 "알려진 한계"로 적혀 있던 서술은 결함이었음을 명시하고 정정 - [P2] 목표 뷰포트 미도달 상태에서 bounds·zoom이 갱신될 때마다 이펙트가 재실행되며 상한 타이머를 처음부터 다시 걸어, 사용자가 계속 패닝하면 2차가 무한정 연기되고 패널이 로딩에 갇혔다 — §11 D13(영구 로딩 금지) 정면 위반. 지적은 2차만 짚었으나 정규화 대기도 같은 구조여서 함께 고쳤다 - 마감을 대기 사이클당 1회만 계산하는 순수 함수 advanceSettleDeadline 신설 — 이펙트 재실행은 남은 시간만 스케줄하고, remaining <= 0이면 타이머 없이 즉시 종결한다(setTimeout(fire, 0)은 다음 cleanup에 취소될 창이 남는다) - 가시성 공존: 마감이 hiddenSince를 기억해 복귀 시 숨어 있던 만큼 뒤로 밀린다(= 가시 시간만 소모). 숨은 동안 타이머를 걸지 않는 §12 동작과 도달 후 서버 10초 창은 그대로 - 실타이머 실측: 정규화 대기 반복 패닝 3,116ms · 2차 대기 3,168ms · 숨김 복귀 후 <2,200ms 종결. RED 단계에서 패닝 3시나리오 전부 미발사(무한)로 결함 재현 - 게이트 6종 green(web 1481 tests). 브라우저 실동작 4건은 확인불가 — 검증 시점에 탭이 hidden 고정(rAF 정지)이고 로그인 세션이 만료돼 있었다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX
검증 3회차는 탭이 hidden 고정(rAF 정지)이고 로그인 세션이 만료돼 실동작을 못 봤다. 전제(visible·121fps)를 충족시켜 재계측한 결과 4건 전건 통과. - 정규화 315ms(8km→1km, 200)·복구 851ms·2차 간격 10,492ms(드래그 3회 방해에도 연기 없음) - 계속 방해 시 3,088ms 종결 — recommend 0건, 로딩이 아니라 안내+"다시 시도" (P2 무한 연기 차단과 14401 가드가 같은 시나리오에서 동시 입증) - 요청 span 전건 0.1211°x0.2966° <= 0.5°, 비200은 의도적 14429 1건뿐 - 14429 화면 문구 "요청이 너무 잦습니다. 잠시 후 다시 시도해주세요" 노출 확인 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX
|
Claude finished @gyuhochoime's task in 5m 10s —— View job 🔍 코드 리뷰 완료작업 목록
결과: 🔴/🟡 결함 없음. codex 리뷰(P1 2차 401 로그인 누락, P2 정착 상한 무한 연기)가 코드·테스트로 확인된 대로 해결돼 있고, |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
🔍 코드 리뷰 요약 — MSG-489 (PR #105)범위: ✅ 잘한 점
🟢 사소한 참고 (블로킹 아님)
🟡 / 🔴 발견된 결함없습니다. 로직·엣지 케이스·타입 안정성·메모이제이션·재렌더링·보안 관점에서 확인했으나 반드시 수정해야 할 문제나 권장 수정 사항을 찾지 못했습니다. PR 본문에 기록된 codex 리뷰 P1(2차 401 로그인 처리 누락)·P2(정착 상한 무한 연기)는 코드·테스트로 모두 확인된 대로 해결되어 있습니다. View job run · 브랜치 feat/ai-route-mentioned-area-auto-move-origin |
PR #105 리뷰(react-doctor/query-mutation-missing-invalidation, warning) 판정. /api/routes/recommend는 body가 필요해 POST일 뿐 읽기형이라(생성 스펙: "…보내면 지점 목록을 돌려준다") 서버 리소스를 만들지도 고치지도 않고, 응답은 쿼리 캐시가 아니라 ai-route-store로 간다 — 낡아질 캐시가 없다. 제안대로 onSuccess에 invalidateQueries를 넣으면 1차 응답 분기(결과 미게시 + 2차 예약)에서도 실행돼 무관한 격자 쿼리를 재요청하고 오버레이를 흔든다. 규칙 자체는 레포의 다른 mutation에서 유효하므로(video 5·profile 3·badge 1이 실제로 invalidate) doctor.config.json 전역 off는 하지 않는다. pre-commit 훅이 --blocking warning이라 커밋이 막혀, 공식 인라인 억제(react-doctor-disable-next-line)로 이 한 줄만 좁혀 껐다 — 판정 근거는 호출부 주석과 docs/spec/MSG-489.md §13-2에 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX
|
Claude finished @gyuhochoime's task in 3m 36s —— View job 🔍 코드 리뷰 완료작업 목록
결과: 🔴/🟡 결함 없음. |
🔍 코드 리뷰 요약 — MSG-489 (PR #105)범위: ✅ 잘한 점
🟢 사소한 참고 (블로킹 아님, 이전 리뷰와 동일 관찰 재확인)
🟡 / 🔴 발견된 결함없습니다. 로직·엣지 케이스(대기 무한 연기, 탭 가시성, StrictMode 이중 실행, 2차 인증 처리)·타입 안정성·메모이제이션·보안(XSS: View job run · 브랜치 feat/ai-route-mentioned-area-auto-move-origin |
🎫 관련 티켓
MSG-488(PR #104) 위에서 갈라졌고, MSG-490(walk-paths 실경로)과 병렬로 진행됐습니다.
docs/spec/MSG-488.md§4-2 파일 소유권 표의 489열만 접촉해 490 소유 5파일 diff 0줄입니다(머지 순서 무관).📌 작업 내용
1.
mentionedArea자동 이동 + 2차 자동 재요청mentionedArea가 있으면 확인 없이centerLat/Lng로 이동 + 줌을 1km 단(MAP_SCALE_1KM_ZOOM)으로 고정 + 토스트("서면으로 이동했어요 / 지도 범위 약 1km 기준으로 동선을 짜요") 후 새 뷰포트로 2차 요청 1회. 2차 동안 패널은 로딩이고 1차 결과는 그리지 않습니다.kind(MOVE/ZOOM_OUT)는 분기하지 않습니다 — 생성 타입이 유니언이 아니라 rawstring이라 미지 값이 뚫립니다.429/14429, 요청 시작 +11s는 200,Retry-After없음). 그래서 1차requestedAt기준 잔여 시간을 기다린 뒤 발사하고, 대기 동안 로딩을 유지합니다.2. 출발지(origin) 자동 판정
origin을 싣고 입력 카드에 "현재 위치에서 출발" 행을 띄웁니다. 결과 화면에서는 그 행이 사라지고 버튼 문구 "현재 위치에서 다시 짜기"로만 알립니다(Figma 15666:13139).shared/geolocation.getCurrentPosition()이 권한 거부를 삼키고 서면 좌표를 돌려주는 문제로 "거부면 origin 생략"을 구조적으로 만족할 수 없어, 가산 exportgetCurrentPositionOrNull()을 추가했습니다(기존 소비 3곳 무수정).3. 추천 시 항상 1km 축척 고정 (사용자 지시)
zoomTo는idle을 내지 않아 요청이 영영 안 나갑니다.exceedsViewportSpan)로 위치를 옮겨 부활시켰습니다. 발사 4경로(즉시·정규화 대기·상한 만료·2차)가 모두send()한 지점을 지납니다.4.
/ai-route에서 집계 클러스터 마커 숨김 (사용자 지시)gateRouteClusters(clusters, pathname)를MapShell의 파생 결과에만 적용합니다(집계 훅 3종 무수정 → 홈·도감 영향 0). 집계층은 줌 16(100m) 미만에서만 존재하므로 경로 게이트 하나로 "100m보다 넓어지면 안 보이게"가 충족됩니다.5. 문구·하네스
✅ 체크리스트
pnpm lint/pnpm typecheck/pnpm --filter web test run통과docs/STATUS.md·docs/spec/MSG-489.md·docs/decisions/DECISIONS.md)🔍 검증 요약
자동 검증 (루트 풀 게이트, 전건 통과)
pnpm testpnpm typecheckpnpm lint(oxlint)pnpm format:checkpnpm check:duplicationpnpm --filter web build수용 기준
로직 L1~L25(vitest) · 화면 S1~S12(브라우저 실동작 + Figma 대조) 전건 통과, 확인불가 0.
실동작 실측 (실 API · 로그인 세션 · 전면 탭 121fps)
origin없음 → 자동 이동 후 2차origin있음, 버튼 "현재 위치에서 다시 짜기"/ai-route줌 10·12·13·16 전부 0개 / 같은 뷰포트 홈은 줌 12에서 3개(종전대로)codex 리뷰(push 전 게이트) 지적 2건 — 모두 실제 결함, 반영 완료
onLoginRequired가 없어, 1차 성공과 지연된 2차 사이 세션 만료 시 401에도 로그인 모달이 뜨지 않았습니다. (문서에 "알려진 한계"로 적혀 있었으나 결함이었습니다)bounds·zoom이 갱신될 때마다 상한 타이머가 처음부터 다시 걸려, 계속 패닝하면 2차가 무한정 연기되고 패널이 로딩에 갇혔습니다 — "영구 로딩 금지" 계약 정면 위반. 지적은 2차만 짚었지만 정규화 대기도 같은 구조여서 함께 고쳤습니다. 마감을 사이클당 1회만 계산하는 순수 함수(advanceSettleDeadline)로 교체했습니다.📸 스크린샷
_workspace/MSG-489/screenshots/(자동 이동 결과 + 클러스터 0개 / 홈 클러스터 대조쌍)💡 추가 논의할 사항
검증에서 발견한 함정 — 계측이 조용히 35배 틀렸습니다
브라우저 자동화 탭이 백그라운드였고 rAF가 0.5~1fps로 정지해 있었습니다. 같은 줌 전이가 숨은 탭 11,162ms · 전면 탭 316ms로 측정됐고, 그 값이 "정규화 대기 14.2초 무피드백"이라는 리포트 문장과 후속 티켓 권장까지 만들어 냈습니다. 더 나쁜 건 동작 자체가 달라진다는 점입니다 — 숨은 탭에서는 네이버 지도가
idle을 내지 않아 뷰포트 스토어가 옛 값에 머물고, 그 상태로 상한이 발화해 정규화되지 않은 범위(1.9°×4.7°)로 요청이 나가 서버 400(14401)이 재현됐습니다. 오염된 계측이 실제 결함을 가리고 있었습니다. 그래서 계측 신뢰성을 검증자의 주의력이 아니라 절차의 전제 조건으로 내렸습니다(8368b1f).선행 문서 정정
MSG-488 §11 / STATUS의 "
gateFillCells가zoom<16에서 채움 셀을 버려 격자 초록 틴트가 안 보인다 → MSG-489의 1km 고정이 해소한다"는 두 절 다 사실과 다릅니다. 실제 게이트는 색 있는 셀을 남기는 필터이고 AI 경로 셀은 전부 색을 가져 통과합니다(zoom 13에서 폴리곤 8개 DOM 실측). 1km(13)는 어차피 임계(16) 아래입니다. 저줌에서 육안으로 안 보이는 실제 원인은 셀이 6~7px이라 28px 마커에 가리는 것이고, 결과 후 확대하면 보입니다(줌 16 육안 확인).소유권 이탈 1건 (의도)
widgets/map-shell/MapShell.tsx+10/−2 — 488이 "완결·489 0줄"로 선언한 파일이지만, 클러스터가 이 파일에서 파생되므로 다른 경로가 없고 사용자가 명시 지시했습니다. MSG-490도 이 파일 0줄이라 병렬 머지 충돌은 없습니다.후속 후보
features/dex/use-region-stat-query.ts의 privateuseCurrentCoords와 이 티켓 동명 훅이 구조상 3복제(baseline 등재) — 공통화하려면 dex의 "조회 전 null 대기" 계약을 tri-state로 바꿔야 합니다.role="status", 기존 구현)가 "1km"를 토스트와 함께 낭독합니다(내용 일치).🤖 Generated with Claude Code
https://claude.ai/code/session_01QkrsSVoDCWaPAV45gzKpZX