Conversation
Deploying mateball-client with
|
| Latest commit: |
aae2049
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://204c7fd5.mateball-client.pages.dev |
| Branch Preview URL: | https://fix--398-initial-matching-co.mateball-client.pages.dev |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, 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 the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📋 워크스루온보딩 퍼널 플로우를 리팩토링하여 START와 GENDER 단계를 제거하고 FREQUENCY 및 DATE_SELECT 단계를 추가했습니다. 새로운 완료 카드 컴포넌트를 도입하고, 매칭 조건 API를 genderPreference에서 avgSeason으로 변경하여 관련 타입 정의를 업데이트했습니다. 📊 변경 사항
🎭 시퀀스 다이어그램sequenceDiagram
actor User
participant Onboarding as Onboarding Flow
participant Funnel as Funnel Component
participant DateSelect as DateSelect Component
participant GameMatch as GameMatchBottomSheet
participant API as Backend API
participant Complete as Complete Component
User->>Onboarding: 온보딩 시작
Onboarding->>Funnel: FREQUENCY 단계 렌더링 (currentStep prop 전달)
Funnel->>Onboarding: 해당 Step만 반환
User->>Onboarding: 직관 횟수 입력 후 다음으로
Onboarding->>Funnel: DATE_SELECT 단계 렌더링
Funnel->>DateSelect: activeType과 onComplete 콜백 전달
User->>GameMatch: 경기 날짜 선택 후 제출
GameMatch->>API: 매칭 생성 요청 (avgSeason 포함)
API-->>GameMatch: matchId 응답
GameMatch->>DateSelect: onComplete(matchId) 콜백 실행
DateSelect->>Onboarding: createdMatch 상태 저장 후 goNext()
Onboarding->>Funnel: COMPLETE 단계 렌더링
Funnel->>Complete: matchId, type, nickname 전달
Complete->>Complete: 타입에 따라 CompleteSingleCard 또는 CompleteGroupCard 렌더링
User->>Complete: 완료 버튼 클릭
Complete->>Onboarding: HOME으로 네비게이션
🎯 예상 코드 리뷰 노력🎯 4 (복잡) | ⏱️ ~45분 🔗 관련 PR
👥 제안된 검토자
🐰 시의 축하
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
|
MATEBALL-STORYBOOK |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/pages/edit-profile/edit-profile.tsx (1)
74-103:⚠️ Potential issue | 🟠 Major성별 초기값이 빈 문자열로 고정되어 저장 시 값 유실 위험이 있습니다.
initial.gender를''로 고정하면, 사용자가 성별을 건드리지 않고 다른 항목만 저장해도genderPreference가 빈 값으로 전송될 수 있습니다(Line 98-103). 기존 서버값을 초기값으로 유지해야 안전합니다.🛠️ 제안 수정안
const initial = { team: data?.team ?? '', - gender: '', + gender: data?.genderPreference ?? '', mateTeam: data?.teamAllowed ?? '', viewStyle: data?.style ?? '', };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/edit-profile/edit-profile.tsx` around lines 74 - 103, initial.gender is hard-coded to '' causing genderPreference to be overwritten on save; change initial to pull the server value (e.g., data?.gender or data?.genderPreference) instead of always '' so that genderValue (and consequently genderPreference sent in editMatchCondition inside handleSaveClick) retains the original server value when the user doesn't touch the gender field; update the initialization where initial is defined and ensure genderValue still reads from gender ?? initial.gender.src/shared/types/user-types.ts (1)
72-77:⚠️ Potential issue | 🟠 Major중복된 타입 정의로 인한 불일치 문제
postMatchConditionRequest가src/shared/types/match-types.ts에도 정의되어 있습니다. 두 파일의 정의가 다릅니다:
- match-types.ts:
team,teamAllowed,avgSeason,style(genderPreference 없음)- user-types.ts:
team,teamAllowed,style,genderPreference(avgSeason 없음)어떤 타입이 import되느냐에 따라 다른 동작이 발생할 수 있습니다. 타입 정의를 한 곳으로 통합하거나, 이 파일의 정의도
match-types.ts와 동일하게 업데이트해야 합니다.🔧 수정 제안
export interface postMatchConditionRequest { team: string; teamAllowed: string | null; + avgSeason: number; style: string; - genderPreference: string; }또는 중복을 제거하고
match-types.ts에서 re-export하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/types/user-types.ts` around lines 72 - 77, postMatchConditionRequest is duplicated with a conflicting shape between src/shared/types/user-types.ts and match-types.ts; update user-types.ts so the interface matches the canonical definition in match-types.ts (include avgSeason and remove or make genderPreference optional to match), or remove the duplicate and re-export the type from match-types.ts (e.g., export { postMatchConditionRequest } from './match-types') so there is a single source of truth; locate the interface named postMatchConditionRequest and either align its fields to match-types.ts or replace its declaration with a re-export.
🧹 Nitpick comments (4)
src/shared/components/bottom-sheet/game-match/game-match-bottom-sheet.tsx (2)
96-96: 중복된.toString()호출
createdMatchId는 이미 79번 라인에서response.matchId.toString()으로 문자열로 변환되어 있습니다. 다시.toString()을 호출할 필요가 없습니다.♻️ 수정 제안
- navigate(`${ROUTES.MATCH_CREATE(createdMatchId.toString())}?type=${queryType}`); + navigate(`${ROUTES.MATCH_CREATE(createdMatchId)}?type=${queryType}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/components/bottom-sheet/game-match/game-match-bottom-sheet.tsx` at line 96, createdMatchId is already a string (converted earlier), so remove the redundant .toString() when building the route; update the navigate call that uses ROUTES.MATCH_CREATE(createdMatchId.toString()) to pass createdMatchId directly (keeping interpolation with ?type=${queryType}), locating the change in the navigate invocation inside the GameMatchBottomSheet component where createdMatchId, ROUTES.MATCH_CREATE and queryType are used.
93-94: 타입 안전성을 통한onComplete콜백 처리 개선 권장현재 코드에서
fromOnboarding과onComplete의 관계가 타입 수준에서 강제되지 않아,fromOnboarding={true}이지만onComplete이 제공되지 않는 상태가 가능합니다. 실제 사용처(date-select.tsx)에서는 항상 함께 제공되고 있으나, 컴포넌트 인터페이스 설계상 이를 보장하지 않습니다.다음과 같이 개선할 수 있습니다:
fromOnboarding=true일 때onComplete을 필수 필드로 강제하는 Union 타입 활용- 또는
fromOnboarding=true일 때onComplete이 undefined인 경우 런타임 경고/에러 추가현재는 실무상 문제가 없으나, 컴포넌트 오사용 시 조용히 실패하는 구조를 개선하는 것이 권장됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/components/bottom-sheet/game-match/game-match-bottom-sheet.tsx` around lines 93 - 94, The branch that calls onComplete when fromOnboarding is true is not type-safe; update the GameMatchBottomSheet props to a discriminated union so that when fromOnboarding: true the onComplete callback is required (e.g. type Props = { fromOnboarding: true; onComplete: (id:string)=>void; ... } | { fromOnboarding?: false; onComplete?: ... }), and adjust the component signature to use that Props type so the compiler enforces presence of onComplete; alternatively (or additionally) add a runtime guard in the component before calling onComplete: if (fromOnboarding && !onComplete) { console.error("GameMatchBottomSheet: onComplete required when fromOnboarding is true"); return; } referencing the fromOnboarding prop and onComplete handler used in the component.src/shared/hooks/use-funnel.tsx (1)
15-20: 타입 단언이 런타임 안전성을 보장하지 않습니다.
Children.toArray의 결과에ReactElement<{ name: string }>[]로 타입 단언을 적용하고 있지만, 실제로 문자열이나 숫자 같은 비-엘리먼트 자식이 전달되면child.props.name이undefined가 되어 매칭에 실패할 수 있습니다.🛡️ 타입 가드를 사용한 안전한 필터링 제안
+import { Children, isValidElement, type ReactElement, type ReactNode, useCallback, useEffect } from 'react'; + const Funnel = ({ children, currentStep }: FunnelProps) => { - const childrenArray = Children.toArray(children) as ReactElement<{ name: string }>[]; - const matched = childrenArray.find((child) => child.props.name === currentStep); + const childrenArray = Children.toArray(children); + const matched = childrenArray.find( + (child): child is ReactElement<{ name: string }> => + isValidElement(child) && child.props.name === currentStep + ); return <>{matched}</>; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/hooks/use-funnel.tsx` around lines 15 - 20, The current Funnel component unsafely asserts Children.toArray(...) as ReactElement<{ name: string }>[], which can break at runtime if non-elements are passed; update Funnel to first convert children via Children.toArray and then filter them with React.isValidElement to narrow the union, e.g. produce a childrenArray of only React elements and use a type guard that ensures element.props has a name (reference: Funnel, childrenArray, matched), and then run find on that filtered array so matched access to child.props.name is safe.src/pages/onboarding/onboarding-group.tsx (1)
44-48: 불필요한 조건문 정리 필요
currentStep !== 'START'(Line 44)와currentStep !== 'DATE_SELECT'(Line 68) 조건은 해당 스텝들이GROUP_FUNNEL_STEPS에 없거나 주석 처리되어 항상 true입니다. TODO 주석에 따라 파일 전체 삭제 예정이라면 현재 상태로 유지해도 무방합니다.Also applies to: 68-68
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/onboarding/onboarding-group.tsx` around lines 44 - 48, The conditional checks comparing currentStep to hard-coded strings ('START' and 'DATE_SELECT') are redundant because those steps are not present in GROUP_FUNNEL_STEPS; replace the string comparisons with a membership check against GROUP_FUNNEL_STEPS (e.g., GROUP_FUNNEL_STEPS.includes(currentStep)) or simply remove the surrounding conditional if the ProgressBar/related UI should always render when steps exist; update the instances around ProgressBar (currentIndex, steps) and any other conditional blocks referencing currentStep to use GROUP_FUNNEL_STEPS.includes(currentStep) so the visibility is driven by the actual steps list rather than stale hard-coded values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/match/constants/matching.ts`:
- Around line 40-41: 파일의 호칭 표기 일관성을 맞추기 위해 MATCHING_SUGGESTION_MESSAGE_TITLE 함수의
템플릿에서 `${nickname}님`을 다른 메시지들(예: `${nickname} 님`)과 동일하게 공백 포함 표기인 `${nickname}
님`으로 변경하세요; 해당 상수명 MATCHING_SUGGESTION_MESSAGE_TITLE을 찾아 템플릿 리터럴 내부의 호칭 앞에 공백을
추가하여 UI 문구 표준에 맞게 통일합니다.
In `@src/pages/onboarding/components/complete-button-section.tsx`:
- Around line 22-28: The "만들기" button currently reuses handleGoToMate (which
routes to ROUTES.HOME); change its onClick to route to the match creation path
instead by invoking the MATCH_CREATE route with the current matchId (e.g., call
router.push(ROUTES.MATCH_CREATE(matchId)) or create a small helper like
handleGoToCreate that does router.push(ROUTES.MATCH_CREATE(matchId))). Update
the Button component's onClick prop in complete-button-section.tsx to use this
new target so "만들기" navigates to ROUTES.MATCH_CREATE(matchId) rather than
ROUTES.HOME.
In `@src/pages/onboarding/components/complete-single-card.tsx`:
- Around line 6-18: CompleteSingleCard and CompleteGroupCard call
useSuspenseQuery but are rendered without a local Suspense boundary; wrap the
place where these components are used (e.g., the Step/Funnel render branch that
chooses by type) with a React Suspense providing a fallback like CardLoading so
loading and errors are handled per-card instead of relying only on the global
boundary — ensure the Suspense wraps the expression that renders
CompleteSingleCard/CompleteGroupCard and import/use CardLoading as the fallback.
In `@src/pages/onboarding/components/complete.tsx`:
- Around line 11-17: The Complete component renders
MATCHING_SUGGESTION_MESSAGE_TITLE(nickname) but does not handle empty nickname;
update the rendering logic in Complete (and/or upstream before calling it) to
provide a fallback when nickname is falsy (e.g., use a default like "회원" or "님"
or a generic phrase) so MATCHING_SUGGESTION_MESSAGE_TITLE is never called with
an empty string; locate Complete and the MATCHING_SUGGESTION_MESSAGE_TITLE usage
and ensure nickname is normalized (e.g., nickname || DEFAULT_NICKNAME) or
conditionally render an alternative message when nickname is missing.
In `@src/pages/onboarding/onboarding.tsx`:
- Around line 148-152: When currentStep is FREQUENCY and the user previously
chose NO_TEAM_OPTION, handlePrev should mirror the forward skip logic by routing
back to SUPPORT_TEAM instead of goPrev() to SYNC_SUPPORT_TEAM; update the
handlePrev function to check if currentStep === 'FREQUENCY' &&
selections.SUPPORT_TEAM === NO_TEAM_OPTION and call goTo('SUPPORT_TEAM') (and
adjust selections.SYNC_SUPPORT_TEAM if needed) before falling back to the normal
goPrev() behavior so backward navigation matches the forward skip implemented in
the currentStep === 'SUPPORT_TEAM' block.
In `@src/pages/onboarding/utils/onboarding-button.ts`:
- Around line 40-51: In the MATCHING_TYPE branch validate selections.FREQUENCY
before calling matchMutate: ensure selections.FREQUENCY is non-null/non-empty
and converts to a finite number (e.g., Number(...) not NaN) and return early if
invalid; then pass the validated numeric value as avgSeason. Update the block
that checks SUPPORT_TEAM and VIEWING_STYLE (symbols: currentStep, MATCHING_TYPE,
selections, selections.FREQUENCY, SUPPORT_TEAM, VIEWING_STYLE,
parsedTeamAllowed, matchMutate) to include this frequency validation so
avgSeason is always a valid number.
In `@src/shared/constants/api.ts`:
- Line 34: DELETE_MATCH_CONDITION is missing the leading slash and will cause
incorrect URL composition with axios; update the constant DELETE_MATCH_CONDITION
to include the leading '/' (making it '/v3/users/match-condition') so it matches
POST_MATCH_CONDITION and other endpoints and avoids baseURL concatenation
errors.
In `@src/shared/constants/query-key.ts`:
- Around line 62-65: DELETE.MATCH()와 DELETE.CONDITION() currently both return
the same key (via MATCH_KEY.ALL + 'delete'), causing unintended cache
invalidation; change one to return a distinct segment (e.g., have
DELETE.CONDITION() return [...MATCH_KEY.ALL, 'condition', 'delete'] or
['match','condition','delete']) so the two keys differ; update the functions
DELETE.MATCH and DELETE.CONDITION in the query-key constants to use the new
unique segment while still based on MATCH_KEY.ALL.
---
Outside diff comments:
In `@src/pages/edit-profile/edit-profile.tsx`:
- Around line 74-103: initial.gender is hard-coded to '' causing
genderPreference to be overwritten on save; change initial to pull the server
value (e.g., data?.gender or data?.genderPreference) instead of always '' so
that genderValue (and consequently genderPreference sent in editMatchCondition
inside handleSaveClick) retains the original server value when the user doesn't
touch the gender field; update the initialization where initial is defined and
ensure genderValue still reads from gender ?? initial.gender.
In `@src/shared/types/user-types.ts`:
- Around line 72-77: postMatchConditionRequest is duplicated with a conflicting
shape between src/shared/types/user-types.ts and match-types.ts; update
user-types.ts so the interface matches the canonical definition in
match-types.ts (include avgSeason and remove or make genderPreference optional
to match), or remove the duplicate and re-export the type from match-types.ts
(e.g., export { postMatchConditionRequest } from './match-types') so there is a
single source of truth; locate the interface named postMatchConditionRequest and
either align its fields to match-types.ts or replace its declaration with a
re-export.
---
Nitpick comments:
In `@src/pages/onboarding/onboarding-group.tsx`:
- Around line 44-48: The conditional checks comparing currentStep to hard-coded
strings ('START' and 'DATE_SELECT') are redundant because those steps are not
present in GROUP_FUNNEL_STEPS; replace the string comparisons with a membership
check against GROUP_FUNNEL_STEPS (e.g.,
GROUP_FUNNEL_STEPS.includes(currentStep)) or simply remove the surrounding
conditional if the ProgressBar/related UI should always render when steps exist;
update the instances around ProgressBar (currentIndex, steps) and any other
conditional blocks referencing currentStep to use
GROUP_FUNNEL_STEPS.includes(currentStep) so the visibility is driven by the
actual steps list rather than stale hard-coded values.
In `@src/shared/components/bottom-sheet/game-match/game-match-bottom-sheet.tsx`:
- Line 96: createdMatchId is already a string (converted earlier), so remove the
redundant .toString() when building the route; update the navigate call that
uses ROUTES.MATCH_CREATE(createdMatchId.toString()) to pass createdMatchId
directly (keeping interpolation with ?type=${queryType}), locating the change in
the navigate invocation inside the GameMatchBottomSheet component where
createdMatchId, ROUTES.MATCH_CREATE and queryType are used.
- Around line 93-94: The branch that calls onComplete when fromOnboarding is
true is not type-safe; update the GameMatchBottomSheet props to a discriminated
union so that when fromOnboarding: true the onComplete callback is required
(e.g. type Props = { fromOnboarding: true; onComplete: (id:string)=>void; ... }
| { fromOnboarding?: false; onComplete?: ... }), and adjust the component
signature to use that Props type so the compiler enforces presence of
onComplete; alternatively (or additionally) add a runtime guard in the component
before calling onComplete: if (fromOnboarding && !onComplete) {
console.error("GameMatchBottomSheet: onComplete required when fromOnboarding is
true"); return; } referencing the fromOnboarding prop and onComplete handler
used in the component.
In `@src/shared/hooks/use-funnel.tsx`:
- Around line 15-20: The current Funnel component unsafely asserts
Children.toArray(...) as ReactElement<{ name: string }>[], which can break at
runtime if non-elements are passed; update Funnel to first convert children via
Children.toArray and then filter them with React.isValidElement to narrow the
union, e.g. produce a childrenArray of only React elements and use a type guard
that ensures element.props has a name (reference: Funnel, childrenArray,
matched), and then run find on that filtered array so matched access to
child.props.name is safe.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e3e7060c-2271-449d-bc49-3062427901d8
📒 Files selected for processing (25)
src/pages/edit-profile/edit-profile.tsxsrc/pages/match/constants/matching.tssrc/pages/onboarding/components/complete-button-section.tsxsrc/pages/onboarding/components/complete-group-card.tsxsrc/pages/onboarding/components/complete-single-card.tsxsrc/pages/onboarding/components/complete.tsxsrc/pages/onboarding/components/date-select.tsxsrc/pages/onboarding/components/frequency.tsxsrc/pages/onboarding/components/matching-type.tsxsrc/pages/onboarding/components/start.tsxsrc/pages/onboarding/components/sync-support-team.tsxsrc/pages/onboarding/constants/onboarding.tssrc/pages/onboarding/onboarding-group.tsxsrc/pages/onboarding/onboarding.tsxsrc/pages/onboarding/utils/onboarding-button.tssrc/pages/sign-up/sign-up.tsxsrc/shared/apis/match/match-mutations.tssrc/shared/components/bottom-sheet/game-match/game-match-bottom-sheet.tsxsrc/shared/components/bottom-sheet/game-match/game-match-footer.tsxsrc/shared/constants/api.tssrc/shared/constants/query-key.tssrc/shared/hooks/use-funnel.tsxsrc/shared/types/match-types.tssrc/shared/types/user-types.tssrc/shared/utils/show-error-toast.tsx
💤 Files with no reviewable changes (1)
- src/pages/onboarding/components/start.tsx
| const CompleteSingleCard = ({ matchId }: { matchId: number }) => { | ||
| const { data } = useSuspenseQuery(matchQueries.SINGLE_MATCH_RESULT(matchId)); | ||
|
|
||
| const cardProps: CardProps = { | ||
| ...data, | ||
| type: 'single', | ||
| imgUrl: [data.imgUrl], | ||
| chips: [data.team, data.style] as ChipColor[], | ||
| className: 'w-full', | ||
| }; | ||
|
|
||
| return <Card isCreated {...cardProps} />; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if Complete component or its parents are wrapped with Suspense boundary
rg -n -B5 -A10 'CompleteSingleCard|CompleteGroupCard|<Complete' --type=tsx --type=tsRepository: MATEBALL/MATEBALL-CLIENT
Length of output: 95
🏁 Script executed:
#!/bin/bash
# Fix: Use correct rg syntax for TypeScript files
# Search for CompleteSingleCard and CompleteGroupCard usage
rg -n -B5 -A10 'CompleteSingleCard|CompleteGroupCard|<Complete' -t tsRepository: MATEBALL/MATEBALL-CLIENT
Length of output: 8866
🏁 Script executed:
#!/bin/bash
# Also search for onboarding.tsx specifically to see how Complete components are rendered
fd -t f 'onboarding' | head -20Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 333
🏁 Script executed:
#!/bin/bash
# Look for any Suspense imports to understand the pattern in the codebase
rg -n 'import.*Suspense|from.*react' -t ts | grep -i suspense | head -20Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 755
🏁 Script executed:
#!/bin/bash
# Find Step and Funnel component definitions
fd -t f 'step\|funnel' -i | grep -E '\.(tsx|ts)$' | head -20Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Search for Step component usage and definition
rg -n 'Step.*from|const Step.*=|function Step' -t ts | head -20Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 1139
🏁 Script executed:
#!/bin/bash
# Check the onboarding.tsx file more completely to see overall structure
cat -n src/pages/onboarding/onboarding.tsx | head -160Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 6870
🏁 Script executed:
#!/bin/bash
# Check the Funnel component definition in use-funnel.tsx
cat -n src/shared/hooks/use-funnel.tsxRepository: MATEBALL/MATEBALL-CLIENT
Length of output: 2741
🏁 Script executed:
#!/bin/bash
# Check if there's a root layout or app component that provides Suspense
rg -n 'Suspense' -A5 -B5 src/shared/routes/global-error-boundary.tsxRepository: MATEBALL/MATEBALL-CLIENT
Length of output: 636
🏁 Script executed:
#!/bin/bash
# Check how Onboarding component is used/imported in the main app
rg -n 'Onboarding' --type ts src/ | grep -E 'import|from|export' | head -15Repository: MATEBALL/MATEBALL-CLIENT
Length of output: 1710
useSuspenseQuery 사용 시 전용 Suspense 바운더리 필요
CompleteSingleCard와 CompleteGroupCard는 useSuspenseQuery로 데이터를 로드하는데, 현재 Step/Funnel 컴포넌트는 Suspense를 제공하지 않습니다. 전역 GlobalErrorBoundary의 Suspense가 catch하지만, 카드별 전용 Suspense 바운더리를 추가하여 로딩 상태와 에러 처리를 더 세밀하게 제어할 수 있습니다.
<Suspense fallback={<CardLoading />}>
{type === 'single' ? (
<CompleteSingleCard matchId={matchId} />
) : (
<CompleteGroupCard matchId={matchId} />
)}
</Suspense>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/onboarding/components/complete-single-card.tsx` around lines 6 -
18, CompleteSingleCard and CompleteGroupCard call useSuspenseQuery but are
rendered without a local Suspense boundary; wrap the place where these
components are used (e.g., the Step/Funnel render branch that chooses by type)
with a React Suspense providing a fallback like CardLoading so loading and
errors are handled per-card instead of relying only on the global boundary —
ensure the Suspense wraps the expression that renders
CompleteSingleCard/CompleteGroupCard and import/use CardLoading as the fallback.
| const Complete = ({ nickname, matchId, type }: CompleteProps) => { | ||
| return ( | ||
| <div className="flex-1 flex-col-center gap-[4rem]"> | ||
| <p className="title_24_sb text-center text-gray-black"> | ||
| 매칭 조건 설정이 끝났어요! | ||
| <br /> | ||
| 메이트를 찾아볼까요? | ||
| </p> | ||
| <div className="w-full flex-1 flex-col-between gap-[4rem] whitespace-pre-line pt-[6.45rem]"> | ||
| <div className="w-full flex-col gap-[4rem] px-[1.6rem]"> | ||
| <p className="title_24_sb text-center text-gray-black"> | ||
| {MATCHING_SUGGESTION_MESSAGE_TITLE(nickname)} | ||
| </p> |
There was a problem hiding this comment.
nickname prop이 빈 문자열로 전달될 경우 UI 문제
MATCHING_SUGGESTION_MESSAGE_TITLE(nickname)은 "${nickname}님을 위한..." 형식의 메시지를 생성합니다. onboarding.tsx에서 nickname=""으로 전달하면 "님을 위한\n매칭카드를 만들어 드릴까요?"가 표시되어 자연스럽지 않습니다. 실제 사용자 닉네임을 전달하거나, 닉네임이 없을 경우의 fallback 메시지 처리가 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/onboarding/components/complete.tsx` around lines 11 - 17, The
Complete component renders MATCHING_SUGGESTION_MESSAGE_TITLE(nickname) but does
not handle empty nickname; update the rendering logic in Complete (and/or
upstream before calling it) to provide a fallback when nickname is falsy (e.g.,
use a default like "회원" or "님" or a generic phrase) so
MATCHING_SUGGESTION_MESSAGE_TITLE is never called with an empty string; locate
Complete and the MATCHING_SUGGESTION_MESSAGE_TITLE usage and ensure nickname is
normalized (e.g., nickname || DEFAULT_NICKNAME) or conditionally render an
alternative message when nickname is missing.
| if (currentStep === 'MATCHING_TYPE') { | ||
| const { SUPPORT_TEAM, SYNC_SUPPORT_TEAM, VIEWING_STYLE } = selections; | ||
| const parsedTeamAllowed = SYNC_SUPPORT_TEAM === '상관없어요' ? null : SYNC_SUPPORT_TEAM; | ||
|
|
||
| if (!SUPPORT_TEAM || !VIEWING_STYLE || !GENDER) return; | ||
| if (!SUPPORT_TEAM || !VIEWING_STYLE) return; | ||
|
|
||
| if (matchMutate) { | ||
| matchMutate( | ||
| { | ||
| team: SUPPORT_TEAM, | ||
| teamAllowed: parsedTeamAllowed, | ||
| style: VIEWING_STYLE, | ||
| genderPreference: GENDER, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: authQueries.USER_STATUS().queryKey }); | ||
| if (selections.MATCHING_TYPE === '1:1 매칭') { | ||
| gaEvent('condition_set_completed', { match_type: 'one_to_one' }); | ||
| goNext(); | ||
| } else { | ||
| gaEvent('condition_set_completed', { match_type: 'group' }); | ||
| if (setProgressOverride) { | ||
| setProgressOverride(0); | ||
| setTimeout(() => { | ||
| navigate(ROUTES.ONBOARDING_GROUP); | ||
| }, 300); | ||
| } | ||
| } | ||
| }, | ||
| matchMutate?.( | ||
| { | ||
| team: SUPPORT_TEAM, | ||
| teamAllowed: parsedTeamAllowed, | ||
| avgSeason: Number(selections.FREQUENCY), | ||
| style: VIEWING_STYLE, |
There was a problem hiding this comment.
FREQUENCY 값 검증 누락
MATCHING_TYPE 스텝에서 matchMutate를 호출할 때 SUPPORT_TEAM과 VIEWING_STYLE만 검증하고 FREQUENCY는 검증하지 않습니다. selections.FREQUENCY가 null이거나 빈 문자열이면 Number()가 0 또는 NaN을 반환할 수 있습니다.
🛡️ 검증 추가 제안
if (currentStep === 'MATCHING_TYPE') {
- const { SUPPORT_TEAM, SYNC_SUPPORT_TEAM, VIEWING_STYLE } = selections;
+ const { SUPPORT_TEAM, SYNC_SUPPORT_TEAM, VIEWING_STYLE, FREQUENCY } = selections;
const parsedTeamAllowed = SYNC_SUPPORT_TEAM === '상관없어요' ? null : SYNC_SUPPORT_TEAM;
- if (!SUPPORT_TEAM || !VIEWING_STYLE) return;
+ if (!SUPPORT_TEAM || !VIEWING_STYLE || !FREQUENCY) return;
matchMutate?.(
{
team: SUPPORT_TEAM,
teamAllowed: parsedTeamAllowed,
- avgSeason: Number(selections.FREQUENCY),
+ avgSeason: Number(FREQUENCY),
style: VIEWING_STYLE,
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/onboarding/utils/onboarding-button.ts` around lines 40 - 51, In the
MATCHING_TYPE branch validate selections.FREQUENCY before calling matchMutate:
ensure selections.FREQUENCY is non-null/non-empty and converts to a finite
number (e.g., Number(...) not NaN) and return early if invalid; then pass the
validated numeric value as avgSeason. Update the block that checks SUPPORT_TEAM
and VIEWING_STYLE (symbols: currentStep, MATCHING_TYPE, selections,
selections.FREQUENCY, SUPPORT_TEAM, VIEWING_STYLE, parsedTeamAllowed,
matchMutate) to include this frequency validation so avgSeason is always a valid
number.
| DELETE: { | ||
| MATCH: () => [...MATCH_KEY.ALL, 'delete'] as const, | ||
| CONDITION: () => [...MATCH_KEY.ALL, 'delete'] as const, | ||
| }, |
There was a problem hiding this comment.
중복된 쿼리 키로 인해 의도치 않은 캐시 무효화 발생 가능
DELETE.MATCH()와 DELETE.CONDITION() 모두 동일한 키 ['match', 'delete']를 반환합니다. 이로 인해 하나를 무효화하면 다른 쪽도 함께 무효화됩니다.
🔧 고유한 키 세그먼트 추가 제안
DELETE: {
MATCH: () => [...MATCH_KEY.ALL, 'delete'] as const,
- CONDITION: () => [...MATCH_KEY.ALL, 'delete'] as const,
+ CONDITION: () => [...MATCH_KEY.ALL, 'delete', 'condition'] as const,
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| DELETE: { | |
| MATCH: () => [...MATCH_KEY.ALL, 'delete'] as const, | |
| CONDITION: () => [...MATCH_KEY.ALL, 'delete'] as const, | |
| }, | |
| DELETE: { | |
| MATCH: () => [...MATCH_KEY.ALL, 'delete'] as const, | |
| CONDITION: () => [...MATCH_KEY.ALL, 'delete', 'condition'] as const, | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/constants/query-key.ts` around lines 62 - 65, DELETE.MATCH()와
DELETE.CONDITION() currently both return the same key (via MATCH_KEY.ALL +
'delete'), causing unintended cache invalidation; change one to return a
distinct segment (e.g., have DELETE.CONDITION() return [...MATCH_KEY.ALL,
'condition', 'delete'] or ['match','condition','delete']) so the two keys
differ; update the functions DELETE.MATCH and DELETE.CONDITION in the query-key
constants to use the new unique segment while still based on MATCH_KEY.ALL.
#️⃣ Related Issue
Closes #398
💎 PR Point
일부 api는 바로 연결하려 했는데 아직 배포 안 된 것들이 있어서 나중에 다른 브랜치에서 작업할게요~
📸 Screenshot
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
버그 수정