-
Notifications
You must be signed in to change notification settings - Fork 0
MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이 #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
107aa59
MSG-488 chore: OpenAPI 스냅샷 갱신 — walk-paths 추가·UserProfile role 필수 필드 반영
gyuhochoime 8071445
MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이 (로그인 전용)
gyuhochoime 703827d
MSG-488 fix: 지도 준비 전 제출 버튼 비활성 — canSubmit↔buildRecommendBody 성립 조건 일치
gyuhochoime 98d156d
MSG-488 fix: 점령 격자 grids 참조 안정화 + AI 경로 각주 중복 제거 (PR #104 리뷰)
gyuhochoime 2445fc7
MSG-488 chore: react-doctor toSorted 권고 off 선언 + 규칙 설정 무효 실측 기록
gyuhochoime File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| # FillMap 웹 (MSG-478) — 색인 대상은 비로그인으로 열리는 홈뿐이다. | ||
| # /dex·/profile은 로그인 뒤 화면, /oauth/는 카카오 콜백, /upload는 스텁(실제 업로드는 모달). | ||
| # /dex·/profile·/ai-route는 로그인 뒤 화면, /oauth/는 카카오 콜백, /upload는 스텁(실제 업로드는 모달). | ||
| User-agent: * | ||
| Allow: / | ||
| Disallow: /dex | ||
| Disallow: /profile | ||
| Disallow: /oauth/ | ||
| Disallow: /upload | ||
| Disallow: /ai-route | ||
|
|
||
| Sitemap: https://fillmap.kr/sitemap.xml |
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
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
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
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
99 changes: 99 additions & 0 deletions
99
apps/web/src/features/ai-route/api/use-route-recommend.test.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
| import { renderHook, waitFor } from "@testing-library/react"; | ||
| import type { ReactNode } from "react"; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { envelopeResponse, errorEnvelope } from "@/test/envelope-response"; | ||
| import { ROUTE_POINTS } from "@/test/route-points"; | ||
| import { stubFetch } from "@/test/stub-fetch"; | ||
| import { useAiRouteStore } from "../model/ai-route-store"; | ||
| import { useRouteRecommend } from "./use-route-recommend"; | ||
|
|
||
| const wrapper = ({ children }: { children: ReactNode }) => ( | ||
| <QueryClientProvider | ||
| client={new QueryClient({ defaultOptions: { queries: { retry: false } } })} | ||
| > | ||
| {children} | ||
| </QueryClientProvider> | ||
| ); | ||
|
|
||
| const BODY = { | ||
| text: "서면에서 밥 먹고 저녁 경기까지 동선 짜 줘", | ||
| viewport: { | ||
| minLat: 35.1521, | ||
| minLng: 129.0537, | ||
| maxLat: 35.1662, | ||
| maxLng: 129.0712, | ||
| }, | ||
| }; | ||
|
|
||
| /** 훅 마운트 → 1회 요청 — 세 시나리오가 공유하는 진입 동작 */ | ||
| const mutateRecommend = ( | ||
| callbacks?: Parameters<typeof useRouteRecommend>[0], | ||
| ) => { | ||
| const { result } = renderHook(() => useRouteRecommend(callbacks), { | ||
| wrapper, | ||
| }); | ||
| result.current.mutate(BODY); | ||
| }; | ||
|
|
||
| describe("useRouteRecommend — 추천 1회 요청 배선 (L5·L7 배선)", () => { | ||
| beforeEach(() => { | ||
| useAiRouteStore.setState(useAiRouteStore.getInitialState(), true); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("응답 봉투를 벗겨 지점과 부족 안내를 스토어에 싣는다 (§4-3)", async () => { | ||
| const received = stubFetch(() => | ||
| envelopeResponse({ | ||
| points: ROUTE_POINTS, | ||
| notice: null, | ||
| mentionedArea: null, | ||
| }), | ||
| ); | ||
| mutateRecommend(); | ||
|
|
||
| await waitFor(() => | ||
| expect(useAiRouteStore.getState().status).toBe("result"), | ||
| ); | ||
| expect(useAiRouteStore.getState().points.map((p) => p.order)).toEqual([ | ||
| 1, 2, 3, | ||
| ]); | ||
| expect(received[0].body).toEqual(BODY); | ||
| }); | ||
|
|
||
| it("실패는 developCode 매핑 안내로 스토어에 실린다 — 입력 문장은 유지된다 (§1-4)", async () => { | ||
| stubFetch(() => errorEnvelope(14400, "뷰포트가 너무 넓습니다", 400)); | ||
| useAiRouteStore.getState().setText(BODY.text); | ||
| const { result } = renderHook(() => useRouteRecommend(), { wrapper }); | ||
|
|
||
| result.current.mutate(BODY); | ||
|
|
||
| await waitFor(() => | ||
| expect(useAiRouteStore.getState().status).toBe("error"), | ||
| ); | ||
| expect(useAiRouteStore.getState().errorNotice?.message).toBe( | ||
| "지도를 조금 더 확대하거나 다른 곳으로 옮긴 뒤 다시 시도해 주세요", | ||
| ); | ||
| expect(useAiRouteStore.getState().text).toBe(BODY.text); | ||
| }); | ||
|
|
||
| it("401(2403)이면 입력 대기로 되돌아가고 로그인 콜백이 호출된다 (§1-3)", async () => { | ||
| stubFetch(() => errorEnvelope(2403, "인증이 필요합니다", 401)); | ||
| const onLoginRequired = vi.fn(); | ||
| const { result } = renderHook( | ||
| () => useRouteRecommend({ onLoginRequired }), | ||
| { | ||
| wrapper, | ||
| }, | ||
| ); | ||
|
|
||
| result.current.mutate(BODY); | ||
|
|
||
| await waitFor(() => expect(onLoginRequired).toHaveBeenCalledTimes(1)); | ||
| expect(useAiRouteStore.getState().status).toBe("idle"); | ||
| expect(useAiRouteStore.getState().errorNotice).toBeNull(); | ||
| }); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { useMutation } from "@tanstack/react-query"; | ||
| // 생성 mutation 옵션은 barrel 미재수출 — 직접 경로 import (MSG-323 관례) | ||
| import { recommendMutation } from "@/shared/api/generated/@tanstack/react-query.gen"; | ||
| import type { RouteRecommendRequestDto } from "@/shared/api/generated"; | ||
| import { unwrapEnvelope } from "@/shared/api/envelope"; | ||
| import { useAiRouteStore } from "../model/ai-route-store"; | ||
| import { routeErrorNotice } from "../model/route-error"; | ||
|
|
||
| /** | ||
| * AI 경로 추천 요청 (MSG-488 §4-3) — `POST /api/routes/recommend`를 1회 쏜다. | ||
| * 생성 SDK mutation 옵션 기반(직접 fetch·URL 하드코딩 없음, use-video-mutations 패턴 미러). | ||
| * 봉투 언랩 → 스토어 게시까지가 이 훅의 계약이고, 화면 분기는 스토어를 구독한다. | ||
| * | ||
| * mutation은 TanStack 기본 `retry: 0`이라 14429(10초 제한)가 자동 재시도로 악화되지 않는다. | ||
| * 콜백은 훅 레벨 옵션으로 받는다 — mutate per-call 콜백은 관찰자 언마운트 시 유실된다(MSG-325 선례). | ||
| * | ||
| * [MSG-489 확장점] 2차 자동 재요청 트리거가 이 훅에 얹힌다. | ||
| */ | ||
| // 생성 팩토리는 mutationFn을 항상 채운다 — UseMutationOptions 타입만 optional이라 !로 좁힌다 | ||
| const recommendFn = recommendMutation().mutationFn!; | ||
|
|
||
| export const useRouteRecommend = (callbacks?: { | ||
| /** 401(2403) — 패널은 입력을 유지한 채 입력 대기로 돌아가고 로그인 모달만 연다 (§1-3) */ | ||
| onLoginRequired?: () => void; | ||
| }) => { | ||
| const startRequest = useAiRouteStore((s) => s.startRequest); | ||
| const succeed = useAiRouteStore((s) => s.succeed); | ||
| const fail = useAiRouteStore((s) => s.fail); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: (body: RouteRecommendRequestDto, context) => | ||
| recommendFn({ body }, context), | ||
| // 이전 결과·선택은 요청 시작 시점에 비운다 — 로딩 화면에 잔상이 남지 않는다 (L7) | ||
| onMutate: () => startRequest(), | ||
| onSuccess: (response) => { | ||
| const data = unwrapEnvelope(response); | ||
| succeed(data.points, data.notice); | ||
| }, | ||
| onError: (error) => { | ||
| const notice = routeErrorNotice(error); | ||
| fail(notice); | ||
| if (notice.requiresLogin) callbacks?.onLoginRequired?.(); | ||
| }, | ||
| }); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React Doctor ·
react-doctor/query-mutation-missing-invalidation(warning)useMutation with no cache update here can leave your users looking at stale data after it runs.
Fix → Add
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })so cached data stays in sync after the mutationDocs