feat: add PWA install onboarding guide#9
Conversation
- add PWA install guide step to onboarding - add manifest shortcuts and app icons from the Spot logo - cover install helper behavior with unit tests
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Your plan includes 1 review of capacity. Refill in 36 minutes and 6 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthrough이 PR은 PWA 설치 온보딩 기능, 피드 좌표 시스템, 지도 통합, 채팅 필터링 UI, 폼 컨트롤 라이브러리, 인증 영속화를 포함하는 대규모 기능 추가 및 리팩토링을 수행합니다. ChangesPWA 설치 온보딩 기능
피드 데이터, 맵 통합, 채팅, 폼, 인증
Estimated code review effort🎯 4 (복잡함) | ⏱️ ~60분 Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/model/auth-store.ts (1)
116-117:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
resetPersona()가 영속 저장소를 지우지 않아 리셋이 유지되지 않습니다.Line 112에서 사용자별 persona를 localStorage에 저장했는데, 여기서는 메모리 상태만 비웁니다. 그래서
resetPersona()뒤 같은 사용자가 다시setSession()되면 Line 73-89 경로로 이전 persona가 바로 복원되고,src/features/auth/model/use-login-form.ts:52-57기준으로도 다시/onboarding을 밟지 못하게 됩니다.🔧 예시 수정
+function removeStoredOnboardingPersona(userId: string) { + if (!canUseLocalStorage()) return; + + try { + const { [userId]: _removed, ...rest } = readStoredOnboardingPersonas(); + window.localStorage.setItem( + ONBOARDING_PERSONA_STORAGE_KEY, + JSON.stringify(rest), + ); + } catch { + // localStorage 접근 실패는 reset 동작을 막지 않는다. + } +} + resetPersona: () => { - set({ userPersona: null, hasCompletedOnboarding: false }); + set((state) => { + const targetUserId = state.userPersona?.userId ?? state.userId; + if (targetUserId) { + removeStoredOnboardingPersona(targetUserId); + } + + return { + userPersona: null, + hasCompletedOnboarding: false, + }; + }); },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/model/auth-store.ts` around lines 116 - 117, resetPersona() currently only clears in-memory state (userPersona, hasCompletedOnboarding) but does not remove the persisted persona in localStorage, so the persona is immediately restored when setSession() runs; update resetPersona to also remove the same localStorage key used to persist userPersona (the key written where userPersona is saved), e.g. call localStorage.removeItem(...) for that key inside resetPersona, ensuring the persisted persona is cleared along with the in-memory state so onboarding can be re-triggered.
🧹 Nitpick comments (4)
src/features/chat/ui/ChatDrawer.tsx (1)
241-259: ⚡ Quick win필터별 목록도 동일한 최신순 정렬을 적용해 주세요.
Line 254-Line 258에서
allRooms만 정렬되고,personal/feed/spot탭은 원본 순서로 보여서 탭 전환 시 정렬 기준이 달라집니다.정렬 일관성 개선 예시
const { personalRooms, feedRooms, spotRooms, allRooms } = useMemo(() => { + const byUpdatedAtDesc = (left: ChatRoom, right: ChatRoom) => + new Date(right.updatedAt).getTime() - + new Date(left.updatedAt).getTime(); + const personal: PersonalChatRoom[] = []; const feed: SpotChatRoom[] = []; const spot: SpotChatRoom[] = []; for (const room of rooms) { if (room.category === 'personal') personal.push(room); else if (room.sourceFeedId) feed.push(room); else spot.push(room); } return { - personalRooms: personal, - feedRooms: feed, - spotRooms: spot, - allRooms: [...personal, ...feed, ...spot].sort( - (left, right) => - new Date(right.updatedAt).getTime() - - new Date(left.updatedAt).getTime(), - ), + personalRooms: [...personal].sort(byUpdatedAtDesc), + feedRooms: [...feed].sort(byUpdatedAtDesc), + spotRooms: [...spot].sort(byUpdatedAtDesc), + allRooms: [...personal, ...feed, ...spot].sort(byUpdatedAtDesc), }; }, [rooms]);Also applies to: 277-282
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/chat/ui/ChatDrawer.tsx` around lines 241 - 259, The per-tab arrays (personalRooms, feedRooms, spotRooms) are left in original order while only allRooms is sorted; update the useMemo block so each collection is sorted by updatedAt in the same descending order as allRooms (e.g., create or apply a sort-by-updatedAt comparator and call it on personal, feed, spot before returning them, keeping the existing allRooms construction intact) — look for the useMemo, variables personalRooms/feedRooms/spotRooms/allRooms, rooms and the updatedAt fields to apply the change.src/features/post/ui/FormField.tsx (1)
6-7: 🏗️ Heavy lift
FormField에htmlFor연결 경로를 추가해 접근성 연동을 보강해주세요.Line 23의
<label>이 현재htmlFor없이 렌더링되어, 라벨-입력 연결을 호출부에서 만들 수 없습니다.FormField에서htmlFor를 받아 전달할 수 있게 열어두는 편이 안전합니다.제안 diff
interface FormFieldProps { label: string; required?: boolean; labelSize?: 'display' | 'compact'; + htmlFor?: string; children: ReactNode; } export function FormField({ label, required, labelSize = 'display', + htmlFor, children, }: FormFieldProps) { @@ - <label className={labelClass}> + <label htmlFor={htmlFor} className={labelClass}> {label} {required && <span className="text-red-400 ml-0.5">*</span>} </label>Also applies to: 10-15, 23-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/post/ui/FormField.tsx` around lines 6 - 7, The FormField component is rendering a <label> without an htmlFor, preventing callers from linking labels to inputs; update the FormField props (e.g., add htmlFor?: string alongside labelSize and children) and pass that prop into the rendered <label> element (label htmlFor={htmlFor}) so callers can provide the input id and restore accessibility linkage; adjust any TypeScript types or prop forwarding in the FormField function to accept and forward htmlFor.src/features/feed/model/feed-map.test.ts (1)
22-91: ⚡ Quick win
resolveFeedCoordinate의 문자열 좌표/coordinate경로도 테스트에 포함해 주세요.현재 테스트는 핵심 폴백 순서는 잘 검증하지만, 실제 백엔드 응답에서 들어올 수 있는
coordinate: { lat: "37.5", lng: "127.0" }형태를 놓치고 있습니다. 이 케이스를 추가하면 좌표 정규화 회귀를 더 빨리 잡을 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/feed/model/feed-map.test.ts` around lines 22 - 91, 테스트에 문자열 좌표와 coordinate 경로 케이스를 추가해 resolveFeedCoordinate가 "coordinate: { lat: '37.5', lng: '127.0' }" 또는 coordinate 경로(예: coord vs coordinate)에서 문자열 값을 숫자로 정상 변환하는지 검증하세요; 구체적으로 feed-map.test.ts에 makeFeed를 사용해 item에 coordinate: { lat: '37.2636', lng: '127.0286' }(또는 coord/primaryPin 혼합 케이스) 를 넣고 resolveFeedCoordinate(item) 결과가 숫자 타입의 { lat: 37.2636, lng: 127.0286 }인지 기대값으로 assert하도록 추가하십시오.src/features/feed/ui/FeedBottomSheet.tsx (1)
60-84: ⚡ Quick win필터 로직을
filterVisibleFeedItems로 통합해 중복을 제거해 주세요.여기 필터 구현이
src/features/feed/model/feed-filter.ts와 사실상 동일해서, 조건 변경 시 바텀이랑 맵이 쉽게 불일치할 수 있습니다. 공용 함수 호출로 통일하는 편이 안전합니다.제안 diff
import { isSearchExcludedFeedItem, type FeedItem } from '../model/types'; +import { filterVisibleFeedItems } from '../model/feed-filter'; @@ - const filtered = feedItems.filter((item) => { - if (feedType === 'offer' && item.type !== 'OFFER') return false; - if (feedType === 'request' && item.type !== 'REQUEST') return false; - if ( - categories.length > 0 && - (!item.category || - !categories.includes(item.category as SpotCategory)) - ) - return false; - if (normalizedQuery.length > 0) { - if (isSearchExcludedFeedItem(item)) return false; - - const haystack = [ - item.title, - item.description ?? '', - item.category ?? '', - item.location, - item.authorNickname, - ] - .join(' ') - .toLowerCase(); - if (!haystack.includes(normalizedQuery)) return false; - } - return true; - }); + const filtered = filterVisibleFeedItems(feedItems, { + feedType, + categories, + searchQuery: normalizedQuery, + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/feed/ui/FeedBottomSheet.tsx` around lines 60 - 84, The inline filter logic that computes filtered duplicates the shared logic in filterVisibleFeedItems; replace the manual .filter callback on feedItems with a call to filterVisibleFeedItems(feedItems, { feedType, categories, query: normalizedQuery }) (or the correct parameter shape used in src/features/feed/model/feed-filter.ts), remove the duplicated checks (including references to isSearchExcludedFeedItem, category/feedType checks and haystack search), and add the necessary import for filterVisibleFeedItems at the top of FeedBottomSheet.tsx so the bottom sheet and map use the single shared filter implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/onboarding/model/pwa-install.ts`:
- Around line 35-42: The current device detection uses the normalized UA and
flags isIOS, isAndroid, isChromium, but iPadOS UA that contains "Macintosh" +
"Mobile" is misclassified as desktop; update the isIOS logic to also treat a UA
as iOS when normalized includes 'macintosh' AND 'mobile' (e.g., change isIOS to:
/iphone|ipad|ipod/.test(normalized) || (normalized.includes('macintosh') &&
normalized.includes('mobile'))), then keep the existing branching (use isIOS,
isAndroid, isChromium) so iPadOS returns 'ios-safari' instead of 'desktop'.
In `@src/features/onboarding/ui/use-pwa-install-prompt.ts`:
- Around line 91-96: The install flow can get stuck in 'prompting' because
installPrompt.prompt() / installPrompt.userChoice can throw; update the effect
that calls setInstallState('prompting') and awaits
installPrompt.prompt()/installPrompt.userChoice (functions referenced:
setInstallState, installPrompt.prompt, installPrompt.userChoice,
setInstallPrompt) to wrap the awaits in try/catch/finally: on error catch and
setInstallState back to a safe state (e.g., 'idle' or 'dismissed') and log or
handle the error, and in finally ensure setInstallPrompt(null) is always called
so state is cleaned up regardless of success or failure.
In `@src/shared/model/auth-store.ts`:
- Around line 9-10: canUseLocalStorage currently reads window.localStorage and
can throw in restricted environments; change it to only check typeof window !==
'undefined' (do not access window.localStorage) and move any actual localStorage
access into try/catch blocks inside readStoredOnboardingPersonas and
persistOnboardingPersona so exceptions are handled where they occur. Also update
resetPersona to remove the per-user persona from persisted storage (the
"spot-onboarding-personas" entry for the current userId) and update the
in-memory persona state (and call persistOnboardingPersona or the same
persistence routine) so getStoredOnboardingPersona(userId) cannot recover an old
completed state after reset; reference functions: canUseLocalStorage,
readStoredOnboardingPersonas, persistOnboardingPersona, resetPersona,
setSession, getStoredOnboardingPersona.
---
Outside diff comments:
In `@src/shared/model/auth-store.ts`:
- Around line 116-117: resetPersona() currently only clears in-memory state
(userPersona, hasCompletedOnboarding) but does not remove the persisted persona
in localStorage, so the persona is immediately restored when setSession() runs;
update resetPersona to also remove the same localStorage key used to persist
userPersona (the key written where userPersona is saved), e.g. call
localStorage.removeItem(...) for that key inside resetPersona, ensuring the
persisted persona is cleared along with the in-memory state so onboarding can be
re-triggered.
---
Nitpick comments:
In `@src/features/chat/ui/ChatDrawer.tsx`:
- Around line 241-259: The per-tab arrays (personalRooms, feedRooms, spotRooms)
are left in original order while only allRooms is sorted; update the useMemo
block so each collection is sorted by updatedAt in the same descending order as
allRooms (e.g., create or apply a sort-by-updatedAt comparator and call it on
personal, feed, spot before returning them, keeping the existing allRooms
construction intact) — look for the useMemo, variables
personalRooms/feedRooms/spotRooms/allRooms, rooms and the updatedAt fields to
apply the change.
In `@src/features/feed/model/feed-map.test.ts`:
- Around line 22-91: 테스트에 문자열 좌표와 coordinate 경로 케이스를 추가해 resolveFeedCoordinate가
"coordinate: { lat: '37.5', lng: '127.0' }" 또는 coordinate 경로(예: coord vs
coordinate)에서 문자열 값을 숫자로 정상 변환하는지 검증하세요; 구체적으로 feed-map.test.ts에 makeFeed를 사용해
item에 coordinate: { lat: '37.2636', lng: '127.0286' }(또는 coord/primaryPin 혼합
케이스) 를 넣고 resolveFeedCoordinate(item) 결과가 숫자 타입의 { lat: 37.2636, lng: 127.0286
}인지 기대값으로 assert하도록 추가하십시오.
In `@src/features/feed/ui/FeedBottomSheet.tsx`:
- Around line 60-84: The inline filter logic that computes filtered duplicates
the shared logic in filterVisibleFeedItems; replace the manual .filter callback
on feedItems with a call to filterVisibleFeedItems(feedItems, { feedType,
categories, query: normalizedQuery }) (or the correct parameter shape used in
src/features/feed/model/feed-filter.ts), remove the duplicated checks (including
references to isSearchExcludedFeedItem, category/feedType checks and haystack
search), and add the necessary import for filterVisibleFeedItems at the top of
FeedBottomSheet.tsx so the bottom sheet and map use the single shared filter
implementation.
In `@src/features/post/ui/FormField.tsx`:
- Around line 6-7: The FormField component is rendering a <label> without an
htmlFor, preventing callers from linking labels to inputs; update the FormField
props (e.g., add htmlFor?: string alongside labelSize and children) and pass
that prop into the rendered <label> element (label htmlFor={htmlFor}) so callers
can provide the input id and restore accessibility linkage; adjust any
TypeScript types or prop forwarding in the FormField function to accept and
forward htmlFor.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d7bd02a9-e306-4665-ab01-33070bdcb84b
⛔ Files ignored due to path filters (5)
public/apple-touch-icon.pngis excluded by!**/*.pngpublic/brand/spot-logo.pngis excluded by!**/*.pngpublic/icons/icon-192x192.pngis excluded by!**/*.pngpublic/icons/icon-512x512.pngis excluded by!**/*.pngpublic/icons/maskable-icon-512x512.pngis excluded by!**/*.png
📒 Files selected for processing (43)
public/manifest.jsonsrc/app/layout.tsxsrc/features/chat/api/chat-api.test.tssrc/features/chat/api/chat-api.tssrc/features/chat/model/types.tssrc/features/chat/ui/ChatDrawer.test.tsxsrc/features/chat/ui/ChatDrawer.tsxsrc/features/feed/api/feed-api.tssrc/features/feed/model/feed-filter.tssrc/features/feed/model/feed-layer-filter.tssrc/features/feed/model/feed-location.tssrc/features/feed/model/feed-map.test.tssrc/features/feed/model/types.tssrc/features/feed/model/use-feed.tssrc/features/feed/ui/FeedBottomSheet.tsxsrc/features/feed/ui/MapFeedCardPager.tsxsrc/features/map/client/MapClient.tsxsrc/features/map/model/types.tssrc/features/map/ui/ClusterBlob.tsxsrc/features/map/ui/MapFeedInfoCard.tsxsrc/features/map/ui/SpotInfoCard.tsxsrc/features/onboarding/client/OnboardingPageClient.tsxsrc/features/onboarding/model/pwa-install.test.tssrc/features/onboarding/model/pwa-install.tssrc/features/onboarding/model/types.tssrc/features/onboarding/ui/PwaInstallGuide.tsxsrc/features/onboarding/ui/use-pwa-install-prompt.tssrc/features/post/client/OfferFormClient.tsxsrc/features/post/client/RequestFormClient.tsxsrc/features/post/ui/FormCard.tsxsrc/features/post/ui/FormControls.tsxsrc/features/post/ui/FormField.tsxsrc/features/post/ui/ImageUploadGrid.tsxsrc/features/post/ui/ImageUploadSlot.tsxsrc/features/post/ui/ReceiptCard.tsxsrc/features/post/ui/post-form/OfferDetailsSection.tsxsrc/features/post/ui/post-form/PlanInputSection.tsxsrc/features/post/ui/post-form/PostBaseInfoSection.tsxsrc/features/post/ui/post-form/PreparationInputSection.tsxsrc/features/post/ui/post-form/PriceInputSection.tsxsrc/features/post/ui/post-form/RequestDetailsSection.tsxsrc/shared/model/auth-store.test.tssrc/shared/model/auth-store.ts
Summary
Checks
Notes
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항