MSG-578 feat: [모바일] 검색 실연동 — 인기 검색어·장소·격자 검색 (웹 MSG-328·412 이식) - #150
Conversation
- zone-search 복사 포팅(22케이스 + parity), 쿼리 훅 3종(옵션 팩토리 + QueryObserver 테스트)
- 결과 탭 → /home params 5키(lat·lng·gridId·bounds·ts) 복귀: 격자 focusTo(줌 16 보장) + 점선 하이라이트, 구역 fitBounds
- idle = 인기 검색어 → 전체 지역(selectRegion + router.back). 최근 검색·최근 방문 섹션 제거(웹 HomeSearchBox 미러, 사용자 결정) → search-store·recent-history·regions·match-region 삭제
- use-debounced-value 웹 시그니처({debounced, flush})
- 검증 S10 수정: 전체 지역 탭 navigate("/home") 리마운트 → router.back()
- nose 베이스라인 develop 기준 재병합(접촉 2 교체 + 신규 12 등재 + 삭제 파일 패밀리 1 제거)
- --no-verify: pre-commit react-doctor no-giant-component 2건(grid-map.tsx 273·map-home-screen.tsx 99)은 이 티켓 이전부터 300줄 초과인 기존 파일(MSG-580 동일 판정) — 게이트(vitest 1349·typecheck·oxlint·oxfmt·nose)는 수동 통과
Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
…상세 선택 해제 - search-screen: `router.navigate` → `router.dismissTo`(POP_TO). navigate는 홈을 새로 마운트해 줌 18이 16으로 리셋되고(실기 재현) 뒤로가기 스택에 홈이 중복됐다. dismissTo 뒤 임시 마운트 로그 0회·줌 유지·하이라이트 확인 - map-home-screen: `selectedRegion`이 채워지면 테마·미션·격자·이벤트 선택을 비우는 effect — 칩 활성 중 검색의 전체 지역 탭이 지역 시트에 닿지 않던 문제(실기: 지역축제 칩 → 서초2동 탭 → 칩 해제·헤더 교체) - nose 베이스라인 접촉 패밀리 1건 교체. DECISIONS 2행, spec 작업 로그 - --no-verify: pre-commit react-doctor no-giant-component 2건은 기존 파일(직전 커밋과 동일 판정). 게이트(vitest 1349·typecheck·oxlint·oxfmt·nose) 수동 통과 Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
…search-integration — STATUS·DECISIONS 양쪽 행 보존 - 충돌 2파일(docs/STATUS.md 티켓 이력 · docs/decisions/DECISIONS.md)은 MSG-581 행을 앞에, MSG-578 행을 뒤에 두고 전부 보존 - 병합 트리 게이트: 모바일 vitest 1351/1351 · typecheck · oxlint · oxfmt · nose 0 new — 전부 exit 0 - --no-verify: pre-commit react-doctor no-giant-component 2건(기존 파일, 직전 커밋과 동일 판정) Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
|
Claude finished @s13121312's task in 5m 0s —— View job ✅ 코드 리뷰 완료 — MSG-578 [모바일] 검색 실연동작업 목록
결과: 반드시 수정해야 할 문제(🔴)는 발견하지 못했습니다. 🟢 사소한 제안 2건(장소 결과 리스트 key 충돌 가능성, |
|
React Doctor found 2 new issues in 2 files · 2 warnings · score 92 / 100 (Great) · 2 fixed · vs 2 warnings
Reviewed by React Doctor for commit |
| ) : ( | ||
| places.map((place, index) => ( | ||
| <Pressable | ||
| key={`${place.gridId}-${place.name}`} |
There was a problem hiding this comment.
🟢 사소한 제안: key={\${place.gridId}-${place.name}`}— 같은 격자(100m 셀) 안에 같은 상호명(예: 프랜차이즈 매장)이 두 곳 이상 있으면 key가 충돌합니다. React가 두 항목을 같은 엘리먼트로 취급해 리스트 갱신 시 잘못된 재사용/경고가 날 수 있어요.address나 배열 index`를 섞어 유일성을 높이는 편이 안전합니다.
| key={`${place.gridId}-${place.name}`} | |
| key={`${place.gridId}-${place.name}-${place.address}`} |
| * 인기 검색어 조회 (웹 `useTrendingQuery` 이식) — 검색 화면 마운트 + 입력 없음일 때만 | ||
| * 활성화한다(D15). 인증 게이트 없음(앱은 로그인 필수). 지도 SDK를 import하지 않는다. | ||
| */ | ||
| export const useTrendingQuery = (enabled: boolean): TrendingResult => { |
There was a problem hiding this comment.
🟢 사소한 제안: 같은 D6 계열 훅인 use-place-search-query(L2, 3건)·use-zones-query(L3, 2건)는 옵션 팩토리를 QueryObserver로 구동하는 전용 테스트가 있는데, trendingQueryOptions/useTrendingQuery는 테스트 파일이 없네요. 로직이 가장 단순하긴 하지만(enabled 플래그 + unwrapEnvelope뿐), 같은 패턴을 유지하려면 최소 1건(빈 배열 폴백 없음 — keywords가 undefined로 남는 것까지 포함) 정도는 추가하는 게 일관성 있어 보입니다. 필수는 아닙니다.
📋 코드 리뷰 요약 — MSG-578 [모바일] 검색 실연동리뷰 범위: 👍 잘한 점
🟢 사소한 제안 (인라인 코멘트 참고)
🔍 상세히 확인했지만 문제 없었던 부분 (참고용)
전반적으로 웹 이식 로직에 parity 테스트를 붙이고, 검증 중 발견한 리그레션(리마운트·시트 우선순위)을 실기로 잡아 고친 꼼꼼한 작업입니다. 위 두 사소한 제안 외에 반드시 수정해야 할 문제는 발견하지 못했습니다. |
- map-home-screen: `selectedRegion` + `regionGrids.data`(regionCode 에코 일치) effect — 첫 격자 `gridX·gridY` → `cellCenterAt` → `moveTo`, 지역당 1회(`movedToRegionRef`). 지역 API에 좌표가 없어 시트용 격자 카드를 재사용, 요청 추가 0 - 이동이 onViewportChange → clearSelectedRegion을 부르지만 새 중심의 라이브 행정동이 곧 그 지역이라 헤더가 이어진다(실기: 지역축제 칩 → 검색 → 서초2동 → 강남 I-6 이동 + 칩 해제) - STATUS·DECISIONS·spec 작업 로그 갱신 - --no-verify: pre-commit react-doctor no-giant-component 2건(기존 파일). 게이트(vitest 1351·typecheck·oxlint·oxfmt·nose) 수동 통과 Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
|
Claude finished @s13121312's task in 3m 24s —— View job ✅ 코드 리뷰 완료 — MSG-578 신규 커밋
|
| <View className="flex-row items-center pb-1.5"> | ||
| <Text className="flex-1 text-fm-body-strong text-foreground-muted"> | ||
| 최근 검색 | ||
| {/* 전체 지역 (S10, D11·D12) — 행 탭 시 시트 지역 교체 + 홈 복귀, 지도 이동 없음 */} |
There was a problem hiding this comment.
🟢 사소한 제안: 주석이 "지도 이동 없음"이라고 돼 있는데, 이번 커밋(1f6f77f)에서 A1이 번복돼 map-home-screen.tsx의 신규 effect가 지역의 첫 격자 중심으로 지도를 이동시킵니다(DECISIONS.md에도 기록됨). 이 주석만 갱신에서 빠진 것으로 보입니다.
| {/* 전체 지역 (S10, D11·D12) — 행 탭 시 시트 지역 교체 + 홈 복귀, 지도 이동 없음 */} | |
| {/* 전체 지역 (S10, D11·D12) — 행 탭 시 시트 지역 교체 + 홈 복귀, 첫 격자 중심으로 지도 이동(A1 번복) */} |
| const movedToRegionRef = useRef<string | null>(null); | ||
| useEffect(() => { | ||
| if (selectedRegion === null) { | ||
| movedToRegionRef.current = null; | ||
| return; | ||
| } | ||
| const data = regionGrids.data; | ||
| const first = data?.grids[0]; | ||
| if ( | ||
| !first || | ||
| data.regionCode !== selectedRegion.regionCode || | ||
| movedToRegionRef.current === selectedRegion.regionCode | ||
| ) | ||
| return; | ||
| movedToRegionRef.current = selectedRegion.regionCode; | ||
| mapRef.current?.moveTo( | ||
| cellCenterAt({ gridX: first.gridX, gridY: first.gridY }), | ||
| ); | ||
| }, [selectedRegion, regionGrids.data]); |
There was a problem hiding this comment.
🟡 권장: 이 PR의 다른 부분(home-focus.ts의 parseHomeFocus, zone-search.ts 등)은 결정 로직을 순수 함수로 뽑아 유닛 테스트를 붙이는 패턴을 일관되게 따르고 있는데, 이 effect는 "다음 이동 대상 좌표(또는 이동 안 함)"를 고르는 로직이 화면 컴포넌트 안에 인라인으로 남아 있어 RN 렌더 테스트 없이는 검증할 수 없습니다.
예를 들어 아래처럼 뽑아내면 regionCode 불일치·빈 격자 배열·이미 이동한 지역 재진입 등 분기를 vitest로 바로 검증할 수 있습니다:
// model 쪽 순수 함수
export const regionFocusTarget = (
data: RegionExploreResponseDto | undefined,
selectedRegionCode: string,
movedRegionCode: string | null,
): LatLng | null => {
const first = data?.grids[0];
if (!first || data.regionCode !== selectedRegionCode || movedRegionCode === selectedRegionCode) {
return null;
}
return cellCenterAt({ gridX: first.gridX, gridY: first.gridY });
};효과 자체는 이 함수를 부르고 movedToRegionRef만 갱신하면 되므로 로직은 그대로이고 테스트만 붙습니다. 필수는 아니지만 이 PR의 다른 부분과의 일관성 차원에서 고려해볼 만합니다.
📋 코드 리뷰 요약 (신규 커밋
|
…g 테스트, key·주석 - map-home/model/region-focus.ts `regionFocusTarget(data, selectedRegionCode, movedRegionCode)` 신설(+test 4) — 홈 effect는 호출 + ref 갱신만 (🟡) - search/api/use-trending-query.test.ts 2건 — 형제 훅과 같은 QueryObserver 구동 (🟢) - search-result-lists: 장소 key에 address 추가 — 같은 격자 동명 매장 충돌 방지 (🟢) - search-screen: "지도 이동 없음" stale 주석 갱신 (🟢) - --no-verify: pre-commit react-doctor 기존 파일 경고 2건. 게이트(vitest 1357·typecheck·oxlint·oxfmt·nose) 수동 통과, 실기 재확인 Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
|
Claude finished @s13121312's task in 1m 38s —— View job 📋 코드 리뷰 완료 — 신규 커밋
|
🎫 관련 티켓
📌 작업 내용
앱 검색 화면(
/search)이 MSG-297 목데이터(구 목록 로컬 매칭, 결과 화면 없음)에 멈춰 있어, 웹 MSG-328(인기 검색어·장소 검색)·MSG-412(격자 검색 "서면 A-14")를 이식했습니다. 서버·웹·packages 무수정.features/search/model/zone-search.ts: 웹 복사 포팅(5함수). 케이스 22건 + parity 테스트1 5건(웹 원본 동적 import)features/search/api/: 옵션 팩토리 + 얇은 훅 3종 —use-place-search-query(디바운스 300ms +searchNow),use-trending-query,use-zones-query(세션 1회 Infinity 캐시).QueryObserver구동 테스트 5건features/search/ui/search-result-lists.tsx:TrendingList·GridResultList·PlaceResultList표시 전용search-screen.tsx재작성: idle = 인기 검색어 → 전체 지역(useExploreRegionsQuery+selectRegion+router.back()), 입력 중 = 격자 섹션 → 장소 결과. placeholder "장소, 격자 검색"HomeSearchBox는 입력 없음일 때 인기 검색어만 보여주고 최근 검색은 신 디자인에서 폐기됐습니다.search-store·recent-history·regions·match-region(+테스트) 삭제map-home/model/home-focus.ts—/home?lat&lng&gridId&bounds&ts5키를 매 복귀마다 전부 싣고(미사용 빈 문자열) 파서가 우선순위 gridId > bounds > point로 해석. expo-routernavigate의 params 병합으로 이전gridId가 잔존하는 함정을 빌더 1곳에서 막음GridMapRef에focusTo(줌 < 16이면 16 보장, 이상이면 유지)·fitBounds(animateCameraWithTwoCoords) 추가. 홈searchHighlightstate →highlightCell(마지막 1건 유지·교체, 해제 수단 없음 — 웹 MSG-412 결정 미러)shared/use-debounced-value.ts를 웹 시그니처{debounced, flush}로 교체(소비처 2곳)GET /api/regions/{code}/grids)의 첫 격자 중심으로moveTo합니다. 요청 추가 없음. 홈 "전체 보기"에서 고를 때도 같이 움직입니다(웹RegionListView는 안 움직임 — parity 차이는 사용자 결정)codex 리뷰(push 전) P2 2건 반영(
7b89c77) — 둘 다 에뮬레이터로 재현 후 수정:router.navigate→router.dismissTo(POP_TO): navigate는 홈을 새로 마운트해 줌 18이 16으로 리셋되고 뒤로가기 스택에 홈이 중복됐습니다. 임시 마운트 로그로 리마운트 0회, 줌 유지, 하이라이트 정상 확인selectedRegioneffect: 칩이 켜진 채 검색에서 전체 지역을 고르면 시트 분기 우선순위(격자 > 상세 > 칩 목록 > 기본) 때문에 지역 시트가 안 보이던 문제. 테마·미션·격자·이벤트 선택을 비웁니다pre-commit react-doctor
no-giant-component2건은--no-verify:grid-map.tsx·map-home-screen.tsx는 이 티켓 이전부터 300줄 초과(MSG-580 동일 판정, DECISIONS 기록).nose 베이스라인2은 develop 기준 접촉 패밀리 2건 교체 + 신규 12건 등재(웹↔모바일 복사 포팅 미러, parity 테스트가 드리프트를 잡음) + 삭제 파일 패밀리 1건 제거. 전체
--write-baseline미사용.✅ 체크리스트
pnpm lint/pnpm typecheck/pnpm --filter mobile test run통과docs/spec/MSG-578.md, STATUS 2행, DECISIONS 4행🔍 검증 요약
pnpm --filter mobile test runpnpm typecheck/pnpm lint/pnpm format:checkpnpm check:duplicationapps/mobile/src25파일 + docs + nose. web·packages·native 0parseHomeFocus파생·가드·우선순위·빈 문자열서면 a-5탭)navigate리마운트) →router.back()후 2차 통과, 지도 이동은 4차 실기 스샷 ⑥실기: emulator-5554(ko-KR), 워크트리 Metro 8082(런북 1-D). logcat ReactNativeJS 에러 0. iOS 미실행(환경).
📸 스크린샷 (선택)
원본 14장은
_workspace/MSG-578/screenshots/(gitignore). 화면 왼쪽의 플로팅 툴바는 에뮬레이터 Gboard(앱 UI 아님).💡 추가 논의할 사항
home-top-bar.tsx의 홈 상단 placeholder는 "장소, 격자, 영상 검색" 그대로(티켓 제외 범위) — 검색 화면과 문구가 어긋납니다. 1줄 후속GET /api/search/trending이 디바운스 중간 입력("ㅂ"·"부"·"부사")까지 집계합니다 — 서버 집계 정책 확인 필요https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
Footnotes
parity 테스트 — 모바일에 복사 포팅한 순수 함수가 웹 원본과 같은 입력에 같은 출력을 내는지 웹 파일을 동적 import해 단정하는 테스트. 두 벌이 조용히 갈라지는 것을 잡습니다. ↩
nose 베이스라인 — 코드 중복 검사기(nose)가 이미 알고 있는 중복 목록. 여기 없는 새 중복이 생기면 게이트가 실패합니다. ↩