release: 4차 배포 — AI 경로추천 추가 (MSG-488~490) - #107
Conversation
MSG-121 feat: 개인 도감(지도 탭) UI 구현
MSG-122 feat: 개인 도감(갤러리 탭) UI 구현
MSG-123 feat: 개인 도감(뱃지 탭) UI 구현
MSG-124 feat: 프로필 페이지 UI 구현
MSG-125 feat: 프로필 편집 모달 UI 구현
MSG-46 feat: 로그인 모달 UI 구현
- test-first 절차에 RED 실행 확인 단계 신설 (보존 단정 예외 명문화, 빌드 리포트 RED 열) - 검증 규칙 감사에 테스트 무결성 항목 (기존 테스트 약화·skip·삭제·특수분기 diff 감사) - 버그 수정·리뷰 반영 특칙 — 로직 결함은 재현 실패 테스트 먼저 - page-builder에 RED 확인 착수 조건·이전 티켓 테스트 불변 원칙 근거: 6각도 하네스 리서치 + 적대 검증 (ImpossibleBench·Anthropic 공식 TDD·superpowers) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
allowOnly 기본값이 !CI라 로컬 pnpm test(빌더 완료 조건·검증 1단계)만 .only 잔류를 허용하던 비대칭 해소 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MSG-255 feat: 프론트엔드 하네스 TDD 적용
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…81) * MSG-431 feat: [모바일] 내 영상 관리 — 도감·격자 상세 공용 영상 액션 메뉴 + 공개 범위·삭제·신고 실연동 * MSG-431 refactor: buildGridVideoRows myVideos 필수화 — 부분 데이터 소유 오판정을 타입으로 차단 리뷰 지적: 모델은 myVideos undefined를 지원하는데 useGridVideosQuery는 두 응답이 모두 도착해야만 호출해, 모델이 연 유연성이 도달 불가였다. 제안된 mine.data ?? [] 는 채택하지 않는다 — 내 영상 목록 없이 행을 만들면 모든 행이 mine: false가 되어 내 영상에 "신고하기"가 뜬다(자기 영상 신고). 화면은 isPending 동안 스피너라 "전역만 먼저 렌더"는 도달할 수도 없어, 얻는 것 없이 안전장치만 잃는 교환이다. 대신 파라미터를 필수로 바꿔 그 위험을 타입으로 표현 불가능하게 만들고, 일어나지 않는 동작을 서술하던 JSDoc과 그것을 단정하던 테스트를 정리했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* MSG-451 feat: 지도 홈·도감 잔손질 5건 — 축제/팝업 칩 줌아웃 집계·칩 최근접 진입·격자 상세 중 불러오기·클러스터 이름·대표 뱃지 FIFO * MSG-451 fix: 칩 최근접 진입에서 대상 선택 제거 — 지도 이동만 하고 목록 화면 유지 * MSG-451 fix: 칩 갈아탈 때 최근접 이동 실종 — 확정 대기와 진입 정착 분리 * MSG-451 chore: 웹 CD 도입 — main 배포·미사용 카카오 env 제거·assets sync 재업로드 방지 * MSG-451 fix: 최근접 이동 활성화를 실제 이동 시에만 소진 (codex P1) + codex 리뷰 게이트 경로 확정 * MSG-451 docs: codex P1 잔여 케이스 WAIVED 기록 — 확정 전 목록 기준 이동 중심이 행정동 밖이라 확정이 미뤄지는 동안 직전 확정 영역 목록의 최근접으로 이동하고 활성화가 소진되는 케이스. 그 목록이 곧 좌측 패널이 보여주는 목록이라 화면과 일치하고, 두 신호로 보정하는 대안은 지도가 두 번 튀어 AC 11과 충돌한다. 근본 해결(목록의 bbox를 신호로 노출)은 별도 티켓. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MSG-451 refactor: 리뷰 반영 — settledChip 파생화·pendingZoomRef 제거·불필요 useMemo 제거 react-doctor 4건(no-adjust-state-on-prop-change ×2, no-derived-state, no-usememo-simple-expression) 반영. web 점수 100/100. - settledChip을 state에서 **렌더 중 파생**으로: 정착 = 목표 줌 도달이라 입력만으로 계산된다. state로 두면 effect가 한 박자 늦게 맞추는 동안 소비처가 옛 값을 본다 - pendingZoomRef 제거 — 목표 줌도 현재 칩에서 곧바로 파생한다. effect가 ref에 실어 두고 다른 effect가 읽던 구조라, MSG-403이 겪은 "이전 칩의 잔여 목표가 남는" 동기화 사고가 구조적으로 가능했다. 파생으로 그 상태 자체를 없앴다 - targets의 useMemo 제거 — 삼항이 두 배열 중 하나를 그대로 돌려주므로 참조가 이미 안정적이다(메모해도 같은 값). 감싸면 비교 비용만 늘어난다 동작 동등성은 기존 테스트 22건이 무수정 통과로 확인. 전 게이트 그린 (웹 1258 · 모바일 876 · typecheck 6 · lint · format · duplication). 미반영 1건(사소한 제안): 활성화 처리 전 구간에서 부모 리렌더마다 nearestTarget이 O(n) 재계산되는 점. 목록이 수십 건 규모라 실측 영향이 없고, 캐시를 넣으면 정착· 목록 신호와 별개의 무효화 축이 하나 더 생긴다 — 리뷰어도 참고로 남긴 항목이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* MSG-444 feat: [모바일] 카카오 로그인 실연동 — 네이티브 SDK + ID 토큰 서버 교환 * MSG-444 docs: 모바일 테스트 수치 정정 (885→893 → 876→894) 리뷰 지적 반영. STATUS.md의 테스트 수치가 실제와 어긋났다. 원인은 리베이스가 아니라 **계산 착오**였다 — 초회 리포트가 측정된 893에서 수용 기준 개수(8)를 빼 885를 도출했는데, 기준 수와 테스트 케이스 수는 다르다. 실제 신규 테스트는 4개 파일에 19건이고 삭제가 1건이라 델타는 +18이다. 실측으로 확정: 신규 테스트 파일 4개를 --exclude하면 875건 → 삭제한 스텁 테스트 1건을 더해 develop 기준은 876. 현재 894. 추가로 STATUS.md가 '테스트 8종 신설'로 기준 수를 테스트 수처럼 표기하던 것도 '로직 기준 8종에 대응하는 테스트 19건'으로 정정했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…EV__ 게이팅
리뷰 13건 중 채택 2건. 나머지는 오탐 6건 기각·2건 이월(DECISIONS.md 기록).
## 트리머 clamp 역전 (웹·모바일 동시)
adjustStartHandle의 상·하한이 5초 미만 원본에서 역전됐다:
min = max(0, 끝-28) = 0, max = 끝-5 < 0 이라 min > max가 되고,
clamp(v, min, max) = min(max(v,min),max)는 이때 항상 max(음수)를 돌려준다.
5초 미만 영상은 예외가 아니라 정상 경로다 — select-validation이 "서버는
durationSec 1초부터 받고 highlight-preview는 5초 미만에 빈 배열을 준다"며
길이 하한을 일부러 두지 않았고, 그 경로의 초기 구간은 clampSegment가
{0, duration}으로 만든다. 슬라이더 ratio()가 표시만 0~1로 가려서 화면에는
안 보이고 상태·서버 트리밍 요청에만 음수 시각이 남는 형태였다.
max = Math.max(min, 끝-5)로 상한이 하한 아래로 내려가지 않게 막았다.
최소 길이를 만족시킬 여지가 없는 구간이므로 시작 0 고정이 유일한 정의다.
기존 픽스처가 못 잡은 이유: 최단 구간 {40,42}는 길이가 2일 뿐 끝값이 42라
max = 37 > 0이었다. 경계 테스트는 구간 길이가 아니라 끝값을 줄여야 밟는다.
웹→모바일 포팅본이라 parity 테스트가 양쪽을 값으로 묶어 동시 수정이 강제된다.
## dev 스모크 화면 __DEV__ 게이팅
expo-router는 파일 존재만으로 라우트를 등록하므로 "화면에 진입 버튼이 없다"가
프로덕션 미도달을 뜻하지 않는다. 딥링크(fillmap://dev/api-smoke)를 아는 사람은
스토어 빌드에서도 닿고, 이 화면의 dev 로그인은 고정 oid로 실제 세션 토큰을
발급받는다. 서버가 운영에서 해당 엔드포인트를 막았는지 FE는 보증할 수 없어
심층 방어로 둔다.
리뷰 제안(훅보다 앞에 조건부 return)은 훅 규칙 위반이라 웹 DevLoginPanel과
같은 래퍼 형태로 바꿔 채택했다 — 본체를 ApiSmokeScreen으로 분리하고
default export는 __DEV__ ? <ApiSmokeScreen /> : null.
## 기각 6건 (근거는 DECISIONS.md)
- rn-scrollview-dynamic-padding ×3: paddingBottom이 insets.bottom(불변)이라
규칙 전제와 다르고, 제안된 contentInset은 RN에서 iOS 전용이라 채택하면
Android 하단 패딩이 사라진다
- query-mutation-missing-invalidation ×3: 로그인 3종은 queryClient.clear()를
이미 부르고, useLogout은 안 비우는 이유가 JSDoc에 명시, use-push-toggle은
상태 정본이 zustand+브라우저 권한이라 무효화할 쿼리가 없다
- no-create-object-url-without-revoke: use-video-duration.ts:52에 revoke가 이미 있다
## 부수
nose.baseline.json 재등재(416→421) — 포팅 쌍의 지문이 이 수정으로 바뀌었다.
전량 재기록이라 무관한 노이즈 family 2건(~0 removable)도 함께 수용됐다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* MSG-446 feat: [모바일] 영상 재생 화면 신설 — 카드 탭 목적지 부재 해소 * MSG-446 style: [모바일] 런처 아이콘을 FillMap 마크로 교체 * MSG-446 chore: develop 리베이스 후 중복 베이스라인 재등재 (423 families) * MSG-446 docs: 스펙 스테일 정정 — 함수명 playbackAccessNotice 통일 + MSG-444 머지 반영 * MSG-446 refactor: PR #85 리뷰 반영 — 피드 카드의 소유 문구·길이를 항목당 1회 파생 * MSG-446 refactor: PR #85 리뷰 반영 — 갤러리 카드의 길이·순번 라벨도 1회 파생으로 통일
* MSG-449 setting: [모바일] 하네스 모바일 검증 경로 추가 + dev client 빌드·실기 절차 정비 * MSG-449 fix: PR #86 리뷰 반영 — 프로파일 범위를 3-B까지 확장 표기 + 런북 함정 개수 정정
* MSG-463 chore: 서버 명세 재생성 — 행사·미션 API 반영 + 전체 지역 목록 커서 페이지 대응
MSG-462·463·464 세 티켓의 공통 베이스라인이다. 세 워크트리가 각자
curl + openapi-ts 를 돌리면 1,948줄 생성물 diff가 세 벌 생겨 머지에서
충돌하고, 실행 시점이 갈리면 생성물끼리도 어긋난다. 한 벌만 develop에 둔다.
api-docs.json 재수집 + openapi-ts 재생성으로 들어온 것:
- 행사 도메인(MSG-439~443): 영상 업로드·위치별 피드·댓글·도움돼요·알림 구독
- 격자 역조회 2종: GET /api/grids/{gridId}/missions (MSG-462가 쓴다),
GET /api/grids/{gridId}/event-locations (행사방 — 웹은 아직 미사용)
- 미션 경유 영상 업로드(MSG-459), 알림 카테고리 EVENT 추가(7종 → 8종)
- 비로그인 조회 허용 반영(MSG-454) — 미션 상세의 progress 는 익명이면 null
깨지는 타입은 1건뿐이라 최소 봉합만 했다. GET /api/regions/explore 가
배열에서 RegionExplorePageResponseDto { items, hasNext, nextCursor } 로
바뀌어(MSG-460 서버 레인) select 가 배열을 꺼내지 못한다. 첫 페이지의
items 만 언랩해 화면 동작을 그대로 두고, 다음 페이지 이어받기(무한 스크롤)는
MSG-463 본편으로 넘겼다. RegionListView 는 무변경이다.
검증: typecheck 통과 · vitest 181파일 1259건 전부 통과 · oxlint 통과 · oxfmt 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* MSG-463 docs: PR #87 리뷰 반영 — 20개 절단이 의도임을 훅 주석에 명시
리뷰 지적: select가 items만 꺼내 21번째 지역부터 조용히 사라지는데,
결과 타입에도 hasNext가 없어 후속 티켓에서 놓치기 쉽다.
절단 사실과 사용자 영향(안내 없이 잘린다)을 주석에 올리고, hasNext·nextCursor를
버리는 것이 누락이 아니라 의도임을 명시했다 — 표시가 없으면 다음 사람이 이걸
결함으로 보고 되살리려 든다. 교체 지점도 getExploreRegionsInfiniteOptions로 못박았다.
결과 타입에 hasNext를 미리 싣자는 제안은 채택하지 않았다. 지금 아무도 읽지 않는
필드이고, MSG-463이 이 훅을 useInfiniteQuery로 통째 교체하므로 태어나서 한 번도
쓰이지 않고 사라진다. 같은 파일이 과거 PR 리뷰에서 "항상 true라 죽은 파라미터"인
enabled를 제거한 것과 같은 판단이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* MSG-447 feat: [모바일] OS 권한 요청 흐름 — 위치·알림 런타임 권한
* MSG-447 chore: develop 리베이스 후 중복 베이스라인 재등재 (426 families)
* MSG-447 fix: push 전 codex 리뷰 반영 — 완료 화면 설정 왕복 복귀 시 푸시 등록 완결
지적(P1): 업로드 완료 화면에서 권한을 거부한 뒤 [설정 열기]로 나가 권한을 켜고
돌아오면 ① 안내가 그대로 남고 ② 토큰 등록도 일어나지 않았다. 완료 화면이 약속한
알림이 끝내 오지 않는 경로였다.
원인은 decidePushPrompt("granted") = skip의 전제였다. "granted인데 토큰이 없으면
사용자가 토글로 끈 것"이라는 판단이 이 경로에서만 거짓이다 — 사용자는 방금 알림을
켜려고 설정까지 다녀왔다. 두 상태를 가르는 것이 "이 화면이 안내를 띄운 적 있는가"다.
shouldRegisterAfterSettings(noticeShown, permission)를 순수 함수로 두고,
usePushPermissionPrompt에 포그라운드 복귀 재판독을 붙였다. 안내를 띄운 적 없는
사용자에게는 아무 일도 일어나지 않으므로 D3(업로드가 사용자의 OFF 결정을 뒤집지
않는다)는 그대로다.
프로필 토글이 기준 13에서 같은 이유로 재판독을 붙였는데, 업로드 축에 같은 구멍이
남아 있었다. 실기 검증이 그 경로를 밟지 않아 놓쳤다.
실기 재확인: 완료 화면 거부 → [설정 열기] → 허용 → 복귀에서 안내가 사라지고
프로필 토글이 ON(= 권한 + 보관 토큰 둘 다 성립)으로 바뀌는 것을 확인했다.
모바일 테스트 970 → 973
* MSG-447 fix: PR #89 리뷰 반영 — 설정 CTA 터치 영역 44dp 확보 + 등록 실패 시 안내 정합
## ① [설정 열기] 터치 영역 (permission-settings-notice.tsx)
11px 캡션 텍스트만 감싼 Pressable이라 터치 영역이 44dp에 크게 미달했다.
권한 거부에서 빠져나오는 유일한 진입점이라 여기서 놓치면 복구 동선 자체가 막힌다.
지적이 제안한 hitSlop 12(저장소의 작은 요소 보강 표준값 — checkbox.tsx·
recent-region-row.tsx)만으로는 14+24=38dp라 여전히 모자라, 세로 패딩 4dp를
더해 22+24=46dp를 확보했다. checkbox.tsx가 "20px + hitSlop 12로 44px 이상"을
계산으로 맞춘 선례를 그대로 따른다.
실측 확인: Pressable 렌더 높이 58px ÷ density 2.625 = 22dp (텍스트 14 + 패딩 8).
## ② 등록 실패 시 낡은 권한 안내 (use-push-permission-prompt.ts)
설정에서 권한을 켜고 돌아왔는데 enablePush가 failed(토큰 취득·서버 등록 실패)면
안내가 "기기 설정에서 알림 권한을 허용해야 한다"로 남았다. 사용자는 방금 그렇게
하고 돌아왔으므로, 실제 원인(등록 실패)과 다른 원인을 안내하게 된다.
권한이 켜진 것이 확인된 시점에 등록 결과를 기다리지 않고 안내를 내린다 —
reconcilePushError가 "권한과 등록 실패는 다른 축"으로 가른 것과 같은 판단이다.
다만 재시도 대상에서 빼는 것(noticeShownRef)은 등록까지 성공했을 때만이다.
안내와 함께 지우면 다음 포그라운드 복귀가 재시도할 근거를 잃어, 권한은 있는데
토큰은 영영 등록되지 않는 상태로 굳는다.
실기 재확인: 안내 표시 → [설정 열기] → 시스템 설정 진입 → 허용 → 복귀에서
안내 소멸 + 토글 ON(= 권한 + 보관 토큰 둘 다 성립).
* MSG-447 chore: hitSlop 추가로 생긴 Pressable 관용 중복 베이스라인 등재
[설정 열기]에 hitSlop을 붙이면서 onboarding-screen 등 기존 hitSlop Pressable 6곳과
구조가 같아졌다. 0/10 shared · ~0 removable — 추출할 공통 로직이 없는 RN 관용 형태라
의도 중복으로 등재한다.
* MSG-462 feat: 지도 줌 속도 완화 + 축제·팝업 격자 클릭 시 행사 상세 열기 * MSG-462 fix: 리뷰 반영 — 격자 미션 스테일 응답 폐기 + 휠 deltaMode 정규화
* MSG-464 fix: 로그인 로고 알파 자산 교체 + 썸네일 onError 폴백 ui-web Thumbnail 승격 * MSG-464 chore: 리뷰 반영 — Thumbnail 렌더 중 state 조정 사유 주석 (useEffect 전환 함정)
* MSG-463 feat: 전체 지역 목록 커서 무한 스크롤 + 비로그인 칩 정보 노출 * MSG-463 test: 리뷰 반영 — 코스 카드 비로그인 방문 수 숨김 스모크 추가
* MSG-465 chore: claude-review 완주 검증을 execution output 3단 판정으로 교체 - 검증 스텝의 gh pr view 마커 grep 판정 제거 → 액션 outputs.execution_file 대상 3단 판정(파일 존재 → result is_error=false&&subtype=success → 게시 도구 호출 증거: gh pr comment Bash 또는 update_claude_comment) - 실패 사유별 구분 ::error:: + is_error/subtype/num_turns 실측값 출력, 형태 이상 시 상위 2000바이트 진단 덤프 (로그만으로 실패 원인 특정) - SHA 마커는 hard-fail에서 warning 강등, 프롬프트 지시는 존치 + 1줄 보정 - PR #89 af67beb 오탐(진행 코멘트에 요약 이어붙임 → 마커 부재로 실패 표시) 해소, MSG-403 불변 조건(요약 미게시 종료 = 실패)은 ③ 게시 증거가 유지 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * MSG-465 chore: 게시 증거 판정을 tool_result 성공까지 상관 검사로 조임 - codex 리뷰 P2 반영 — ③이 tool_use(요청)만 검사해 게시 호출이 권한·API 오류로 실패해도 통과하던 구멍 해소 - 게시 tool_use id 집합 → tool_use_id 매칭 tool_result 중 is_error != true 최소 1건 요구 (SDK 계약: is_error 부재 = 성공) - 실패 메시지 세분화: "게시 도구 호출 없음" vs "게시 시도 전부 실패·결과 누락" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* MSG-445 fix: [모바일] 실기 점검 결함 3건 + 진입 스플래시를 지도 타일 표시까지 유지 - 시트 헤더 버튼 탭 불가: 48dp 핸들 오버레이가 콘텐츠 헤더(실측 26dp부터)를 덮어 ‹·✕·전체 보기가 전부 먹혔다. 오버레이 제거 + 핸들 바 자체를 Pressable로, hitSlop으로 핸들 밴드(0~26dp)만 수신 - 업로드 제목 입력 영구 제거: Figma 정본(14176:6661)·서버 계약 둘 다에 없는 표시 전용 UI — UI·스토어·영속 스키마·액션까지 삭제 - 진입 지도 지연: onInitialized는 getMapAsync(지도 객체 생성)라 타일보다 1.5~2초 이르다는 것이 실측으로 확인됨. 스플래시를 타일 첫 표시 + 점령 조회 완료까지 유지 (shared/splash-gate 순수 게이트 + 상한 2.5초). 래퍼 2.9.0이 OnLoadListener를 노출하지 않아 patches/로 onLoaded 추가 — 네이티브 변경이라 prebuild+재빌드 필요. 패치는 Android 전용이라 iOS는 onInitialized로 폴백(codex 리뷰 P2 반영) - hey-api 명세 최신화(additive 드리프트 2건) 스냅샷 + 생성물 동반 커밋 --no-verify 사유: react-doctor no-giant-component 경고 2건(GridMap·MapHomeScreen)이 pre-commit을 막는다. 두 파일 모두 이 브랜치 이전부터 300줄 기준을 넘고 있었고 (MSG-427 시점 496줄 / MSG-447 시점 403줄) 이번 변경은 각각 +13·+21줄이다. 분할은 MSG-445 범위 밖 리팩터라 후속 티켓으로 남긴다. 함께 건너뛰는 나머지 두 게이트는 수동으로 충족했다 — format:check 통과, 검증 리포트 03_verify_report.md 작성 완료. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MSG-445 fix: 스플래시 상한 2.5초 → 8초 — 타일 도착과 겹쳐 상시 발동하던 경합 해소 리뷰 지적(PR #94) 반영. 두 계측치의 시계 원점이 달라 "타일 ~10.8초"가 곧 지연은 아니었지만, 환산하면 지적이 더 정확했다: 녹화 10800ms = JS 약 3192ms이고 hold()는 JS 약 700ms라 상한 발동이 약 3200ms — 타일 도착과 10ms 차이의 경합이었다. 상한이 이기면 이 티켓이 고치려던 "지도 없는 첫 화면"이 그대로 재발한다. 실기 재확인: 상한을 60초로 풀고 재면 자연 해제가 앱 실행 후 9.8초(이벤트 경로, 해제 시점 타일 있음) = hold() 기준 약 2.5초. 8000ms는 그 3배 이상 여유이고, 이벤트가 영영 오지 않는 경로에서도 8초를 넘겨 붙잡지 않는다. 8초 상한 재측정에서 스플래시는 10.75초에 해제됐고(상한이 아니라 이벤트가 이김) 첫 프레임에 타일·격자·시트가 모두 있다. 상수에 계측 근거와 초안이 왜 틀렸는지를 주석으로 고정했다. --no-verify 사유: 직전 커밋과 동일(react-doctor no-giant-component 기존 초과). format:check 통과·검증 리포트 갱신 완료. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MSG-445 docs: 스플래시 상한 스테일 주석 정리 — 코드는 숫자 대신 상수를 가리키게 리뷰 지적(PR #94) 반영. 9cb8e61에서 상한을 8초로 올린 뒤 세 곳이 "2.5초"로 남아 있었다. - grid-map.tsx: 숫자를 빼고 `SPLASH_MAP_TIMEOUT_MS`를 가리킨다 — 값이 한 곳에만 있어야 다음 조정 때 또 어긋나지 않는다(리뷰 제안은 숫자 갱신이었으나 재발 방지 쪽으로 변경) - docs/spec/MSG-445.md: 현재 값 8초 + 상수명 병기 - splash-gate.ts의 2500ms 언급은 "초안이 왜 틀렸는지"를 설명하는 이력이라 그대로 둔다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* MSG-476 feat: 업로드 공개 범위 선택 + 블러 안내·통지 경로 제거 * MSG-476 fix: READY 폴링 간격 백오프 + 세션 판정 정정 * MSG-476 fix: 교체 시 READY 폴링 기산점 갱신 (PR #99 리뷰)
* MSG-475 feat: 줌아웃 집계 마커 Figma 말풍선 교체 + 핫구역 행정 단위 집계 (#100) * MSG-475 docs: nose baseline 카운트를 리베이스 후 실측(421→429)으로 정정
* MSG-488 chore: OpenAPI 스냅샷 갱신 — walk-paths 추가·UserProfile role 필수 필드 반영 * MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이 (로그인 전용) * MSG-488 fix: 지도 준비 전 제출 버튼 비활성 — canSubmit↔buildRecommendBody 성립 조건 일치 codex 리뷰 P2. viewport bounds가 null인 동안(지도 초기화 지연·실패) 문장만 유효하면 버튼이 활성이었고, 클릭하면 buildRecommendBody가 null을 내며 submit이 조용히 early-return해 "눌러도 안 되는 버튼"이 됐다. canSubmit에 mapReady(= bounds !== null)를 추가해 L8·L9 판정 집합을 일치시키고, 두 함수의 정합을 양방향으로 고정하는 테스트를 더했다 — 단위 테스트가 각 함수는 검사했지만 둘 사이의 계약은 아무도 검사하지 않던 자리다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX * MSG-488 fix: 점령 격자 grids 참조 안정화 + AI 경로 각주 중복 제거 (PR #104 리뷰) useOccupiedGridsQuery가 flattenGridPages 결과를 memo 없이 반환해 데이터가 그대로여도 매 렌더 새 배열이 나왔다. AiRoutePage는 그 파생을 전역 map-overlay-store에 effect로 게시하므로, 결과 화면에서 타이핑할 때마다 clear → 재게시가 돌아 naver Marker·Polyline이 재생성됐다. renderHook 연속 rerender로 RED 재현 후 근원에서 useMemo로 고정했다. 모바일 이식본(apps/mobile/.../use-occupied-grids-query.ts:69)은 이미 같은 가드를 갖고 있었다 — 웹 원본만 빠져 있었고, 이 수정은 패리티를 되돌린다. MapShell:149의 같은 패턴도 함께 해소된다. RESULT_FOOTNOTE 문단은 로딩·결과 블록에 중복돼 있던 것을 한 곳으로 합쳤다. nose 승인 패밀리 3건 시프트 재키잉(437→437, 2건은 digest 바이트 동일). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX * MSG-488 chore: react-doctor toSorted 권고 off 선언 + 규칙 설정 무효 실측 기록 PR #104 리뷰에서 react-doctor가 [...arr].sort()를 toSorted()로 바꾸라고 지적했는데, .oxlintrc.json:84는 같은 API를 error로 금지하고 있다(Hermes 미구현 — MSG-427에서 지도 홈을 마운트 즉시 크래시). oxlint 금지가 mobile/ui-native 한정이라 웹 파일에서는 react-doctor만 말하고 게이트가 아무것도 안 잡는다. 웹 toSorted 6곳 중 모바일 이식본이 있는 5곳 전부가 손으로 되돌려져 Hermes 주석을 달고 있는 것이 그 비용의 실측이다. 단 이 설정은 현재 무효다: v0.9.3에서 rules list/explain은 off로 보고하고 공식 `react-doctor rules disable`이 쓴 것도 같은 내용인데 스캐너는 규칙을 그대로 보고한다 (루트·apps/web cwd 양쪽, ignore.tags도 동일). 기존 js-combine-iterations: off도 같은 이유로 무효였다. 실효 방어선은 CLAUDE.md 이력 기록이고, 설정은 도구가 고쳐지면 듣도록 선언만 남긴다. CLAUDE.md 5행 유지 규칙에 따라 2026-08-19 e2e apt 행을 HARNESS_CHANGELOG로 이관. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) * MSG-489 feat: [웹] AI 경로추천 — mentionedArea 자동 이동·1km 고정·2차 자동 재요청·출발지 자동 판정 * MSG-489 feat: [웹] AI 경로추천 — 추천 시 1km 축척 고정 + 경로 화면 집계 마커 숨김 - 제출하면 항상 현재 중심 기준 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 * MSG-489 chore: 웹 실동작 검증에 탭 가시성·rAF 프레임률 전제 조건 추가 브라우저 자동화 탭이 백그라운드면 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 * MSG-489 fix: [웹] AI 경로추천 — 2차 요청 로그인 처리 부착 + 정착 상한 무한 연기 차단 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 * MSG-489 docs: 브라우저 실측 4건 보강 — 검증 3회차 확인불가 해소 검증 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 * MSG-489 docs: react-doctor 캐시 무효화 지적 기각 — 사유 명시 + 해당 줄만 인라인 억제 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 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) * MSG-490 3-A 브라우저 실동작 검증 완료 — S1~S8 전건 통과, 실패 0건, 확인불가 0건. * MSG-490 docs: R2 origin 배선을 489 실구현 실측으로 "현재 불필요" 정정 * MSG-490 docs: codex 리뷰 P2 기각 근거 기록 — S1 실측·React 배칭·488 선례 * MSG-490 fix: resolved:true+빈 path 세그먼트 직선 폴백 — 끝점 마커 미도달 방어 (PR #106 리뷰)
|
Claude finished @gyuhochoime's task in 5m 41s —— View job 코드 리뷰 완료작업 목록
전체 요약은 별도 코멘트로 게시했습니다: #107 (comment) 한 줄 요약: RN 경계·비동기 경합 방어·에러 폴백 처리가 전반적으로 매우 꼼꼼합니다. 🟡 1건( |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
코드 리뷰 요약 — PR #107 (AI 경로추천, MSG-488~490)95개 변경 파일 중 신규/수정 로직 전체를 diff만이 아니라 주변 코드까지 잘한 점
지적 사항🟡 그 외에는 버그·타입 안정성·성능·보안 관점에서 문제가 될 만한 지점을 찾지 못했습니다. 메모이제이션( |
|
|
||
| return ( | ||
| <ul className="flex flex-col gap-1.5"> | ||
| {points.map((point, index) => { |
There was a problem hiding this comment.
🟡 권장 — points.map()이 order 정렬 없이 서버가 준 배열 순서 그대로 카드를 렌더합니다.
같은 points를 소비하는 route-legs.ts:111와 route-overlay.ts:90는 둘 다 [...points].sort((a, b) => a.order - b.order)로 방어적으로 정렬한 뒤 파생하는데, 카드 목록만 이 정렬이 빠져 있습니다. RoutePointDto.order의 타입 주석은 "방문 순서(1부터 연속)"일 뿐 배열 자체가 그 순서로 온다는 계약은 아니라서, 서버 응답 배열이 order 오름차순이 아니면:
- 카드가 방문 순서와 다르게 표시되고
index > 0 && leg커넥터가leg.find(item => item.toOrder === point.order)로 리그를 찾긴 하지만, 화면상 "바로 위 카드"가 실제leg.fromOrder의 지점이 아닐 수 있어 커넥터 위치와 의미가 어긋납니다.
현재 테스트 픽스처(test/route-points.ts)가 항상 정렬된 배열만 만들어서 이 경로는 테스트로 잡히지 않습니다. 서버가 실제로 항상 정렬해 준다는 보장이 있다면 무시해도 되지만(추측), 방어적으로는 다음처럼 맞추는 게 다른 두 파생 함수와 일관됩니다.
const orderedPoints = useMemo(
() => [...points].sort((a, b) => a.order - b.order),
[points],
);
// ...
{orderedPoints.map((point, index) => { ... })}
3차 배포(#103,
d60ff3e) 이후 develop에 쌓인 3개 커밋(AI 경로추천 웨이브 MSG-488~490)을 배포한다. main 트리는 develop8d3ff99(MSG-474)와 동일했으므로 그 뒤 전부가 이번 범위다. 이 브랜치의 트리는origin/develop0b2c355와 바이트 동일(git diff0줄).웹 (fillmap.kr — 이번 배포로 실제 반영되는 부분)
/ai-route— 사이드 레일 5번째 항목(홈 · AI 경로추천 · 업로드 · 도감 · 프로필), 자연어 문장 →POST /api/routes/recommend→ 좌측 패널 카드(순번·종류 태그·표시명·추천 이유) + 지도 오버레이(번호 마커·격자 강조·연결선), 결과 부족 배너, 에러 5종 안내. 로그인 전용(익명 401). ui-webSkeleton신설·RetryNotice승격 (MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이 #104)mentionedArea) 확인 없이 그 중심으로 이동 + 1km 축척 고정 + 토스트 + 2차 자동 재요청(서버 10초 rate limit이 예외가 아니라 잔여 대기 후 발사), 출발지 자동 판정(현위치가 뷰포트 안일 때만origin전송·"현재 위치에서 출발" 표시) (MSG-489 feat: [웹] AI 경로추천 — 지역 자동 이동·1km 축척 고정·2차 자동 재요청·출발지 자동 판정 #105)POST /api/routes/walk-paths로 세그먼트별 실경로 좌표열·실거리 점진 교체, 실패 세그먼트는 직선·직선 거리 유지의 조용한 폴백 (MSG-490 feat: [웹] AI 경로추천 실보행 경로 — walk-paths 점진 렌더·세그먼트 폴백·실거리 표기 #106)apps/web+packages: 83 files, +5274/−73모바일 (apps/mobile — 스토어 배포와 무관, 코드만 동반)
UserProfileResponseDto.role필수화에 따른 테스트 픽스처 2파일 +2줄. 화면·모델 변경 없음BE 선행 조건 (전부 배포 확인됨 — 각 PR 실측)
POST /api/routes/recommend(BE MSG-457·458·468) — 488·489 브라우저 검증이 실 API 응답으로 수행됨. 익명401/2403실측 → 로그인 전용 게이트POST /api/routes/walk-paths(BE MSG-483) — 490 실동작 S1~S8 실 API 통과, 부분 실패(resolved:false)는 200으로 옴429/14429) → FE가 대기 후 발사. BE 정책 변경 없이 동작openapi/api-docs.json변경은 1줄(walk-paths 추가·role 등 필드 추가분 스냅샷 갱신)이며 생성물 드리프트 0히스토리 — 이 PR은 "Create a merge commit" 으로 머지
#103도 결국 squash로 들어가(
d60ff3e의 부모가42df186하나) merge-base가 여전히f55d7b8(MSG-105)이라, develop→main 직접 PR은 가짜 충돌이 잡힌다(양쪽 트리는 동일). 이 브랜치의 커밋ad3f681은 main(d60ff3e)과 develop(0b2c355)을 둘 다 부모로 기록한 머지 커밋이고 트리는 develop과 동일하다. merge commit으로 머지해야 main 이력이 develop과 연결돼 다음 릴리스부터 develop→main 직접 PR이 가능해진다. squash면 #96·#103과 같은 상태가 반복된다.배포 확인
머지 → Actions
CD (web)실행 확인 → https://fillmap.kr 로그인 → 사이드 레일 "AI 경로추천" → 문장 입력("서면에서 밥 먹고 카페 들르기") → 카드·지도 경로 표시 확인.🤖 Generated with Claude Code
https://claude.ai/code/session_01PStNrbdpwCLXUSGvpK8JZs