온보딩 step-4 complete API 연동 및 에러 처리 통일#712
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 패턴 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…일 삭제 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eMutation 인라인 적용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough클래스 온보딩 API 저장 엔드포인트 4개가 추가되고, 온보딩 모달의 상태 구조(동의 플래그, 배열 필드) 및 Step1~Step4 컴포넌트가 일관된 뮤테이션 기반 제출 패턴으로 재구성되었습니다. Step1은 바이브 경험수준 선택, Step2는 직무 다중선택, Step3은 관심사 선택, Step4는 완료 뮤테이션으로 동작합니다. ChangesClass Onboarding API and Modal Refactoring
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 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 docstrings
🧪 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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx (2)
138-170:⚠️ Potential issue | 🟠 Major | ⚡ Quick win현재 프로필 이미지 선택 UI는 저장되지 않으니 노출하면 안 됩니다.
Line 115-122의 Step1 payload에는 이미지가 포함되지 않고, 이 PR의 Step4도 완료 API만 호출합니다. 지금 상태로는 사용자가 이미지를 골라도 최종적으로는 조용히 버려집니다. 업로드 엔드포인트가 준비되기 전까지는 이 컨트롤을 숨기거나 비활성화해 주세요.
🤖 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/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx` around lines 138 - 170, The profile-image picker is currently interactive but images are not saved (Step1 payload and Step4 completion ignore images), so hide or disable the upload controls until an upload endpoint exists: remove or conditionally skip rendering the file input (fileInputRef) and the change button (the button using RefreshCw and onClick that calls fileInputRef.current?.click()), and instead render a non-interactive avatar display (keep data.profileImageUrl rendering via Image or the emoji fallback) with aria-hidden or aria-disabled attributes; ensure handleImageChange is no longer reachable while the upload feature is disabled.
232-258:⚠️ Potential issue | 🟠 Major | ⚡ Quick win필수 동의 항목은 실제 체크박스나 버튼으로 바꿔 주세요.
현재 row는 클릭 가능한
div라서 포커스되지 않고, Space/Enter로도 조작할 수 없습니다. 필수 동의가canProceed를 막고 있으므로 키보드 사용자에게는 Step1 완료가 사실상 불가능합니다.🤖 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/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx` around lines 232 - 258, The consent rows are currently non-focusable divs so keyboard users cannot toggle required consents (CONSENTS, data, toggleConsent), blocking canProceed; replace the clickable outer <div> with an actual semantic control (e.g., a <button> or a label + <input type="checkbox">) for each consent so it is focusable and operable via Enter/Space, keep the existing onClick behavior by calling toggleConsent(consent.key), expose state via aria-checked or the checkbox checked prop (bound to data[consent.key]), and preserve the link button behavior (stopPropagation and window.open) — ensure toggleConsent and any canProceed checks continue to read the same data state.
🤖 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/api/endpoints/class-onboarding/class-onboarding.ts`:
- Around line 10-88: The
saveClassOnboardingStep1/saveClassOnboardingStep2/saveClassOnboardingStep3/saveClassOnboardingComplete
functions log and rethrow errors directly, which bypasses our global error
handling and loses backend error details; replace the catch blocks to call
analyzeError(err) and sendErrorToSentry(err) and then show a user toast via
useToastStore.getState().showToast(...) (using the userMessage from
analyzeError), and when resData.statusCode !== 200 include the backend error
payload/message (e.g., resData.message or resData.error) in the thrown error so
analyzeError can generate a meaningful userMessage; ensure each function uses
those same symbols (analyzeError, sendErrorToSentry,
useToastStore.getState().showToast) before rethrowing the error.
In `@src/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx`:
- Around line 3-13: The component-level mutation logic in step-1-nickname.tsx
should be moved into a dedicated React Query hook under hooks/queries/ (e.g.
create and export useSaveClassOnboardingStep1Mutation or
useClassOnboardingStep1Mutation) that encapsulates the useMutation call, calls
saveClassOnboardingStep1, and centralizes error/response handling and options;
then import and call that hook from the component instead of using useMutation
directly; ensure the new hook exports a consistent API (mutate/mutateAsync,
status, error) so Step1's component only consumes the hook and not the raw
mutation implementation.
In `@src/components/auth/modals/onboarding-modal/steps/step-2-job.tsx`:
- Around line 57-68: When opening the "etc" input restore the previously saved
text so the UI and state match: in handleEtcClick (which currently calls
setEtcMode(true) and may call updateData for jobs) also set the input state from
data.jobEtcText (e.g. setEtcInput(data.jobEtcText || '')) when the Other option
is selected (isOtherSelected) so the textarea shows the saved value; ensure
handleEtcAdd still trims and saves via updateData('jobEtcText', etcInput.trim())
and closes the mode with setEtcMode(false). Also apply the same restoration
logic to the other occurrence referenced around lines 142-167.
In `@src/components/auth/modals/onboarding-modal/steps/step-4-completion.tsx`:
- Around line 13-29: Step4Completion currently starts the completion mutation
without toggling the parent "submitting" lock, so the modal can be closed or
navigated away mid-request; before calling complete() set the onboarding
submitting flag (use the same API used in onboarding-modal.tsx, e.g. the
onSubmittingChange/setSubmitting method from useOnboardingStore or the
onSubmittingChange prop) to true, and ensure you reset it to false in the
mutation callbacks (onSuccess/onError or onSettled) so the modal/back navigation
remain locked for the duration of saveClassOnboardingComplete; update
handleFinish and the useMutation callbacks in Step4Completion to mirror the
submission-lock pattern used in onboarding-modal.tsx.
---
Outside diff comments:
In `@src/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx`:
- Around line 138-170: The profile-image picker is currently interactive but
images are not saved (Step1 payload and Step4 completion ignore images), so hide
or disable the upload controls until an upload endpoint exists: remove or
conditionally skip rendering the file input (fileInputRef) and the change button
(the button using RefreshCw and onClick that calls
fileInputRef.current?.click()), and instead render a non-interactive avatar
display (keep data.profileImageUrl rendering via Image or the emoji fallback)
with aria-hidden or aria-disabled attributes; ensure handleImageChange is no
longer reachable while the upload feature is disabled.
- Around line 232-258: The consent rows are currently non-focusable divs so
keyboard users cannot toggle required consents (CONSENTS, data, toggleConsent),
blocking canProceed; replace the clickable outer <div> with an actual semantic
control (e.g., a <button> or a label + <input type="checkbox">) for each consent
so it is focusable and operable via Enter/Space, keep the existing onClick
behavior by calling toggleConsent(consent.key), expose state via aria-checked or
the checkbox checked prop (bound to data[consent.key]), and preserve the link
button behavior (stopPropagation and window.open) — ensure toggleConsent and any
canProceed checks continue to read the same data state.
🪄 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: bfcd88c2-8ecb-4f7b-935e-8ea404d8d4a0
📒 Files selected for processing (7)
src/api/endpoints/class-onboarding/class-onboarding.tssrc/components/auth/modals/onboarding-modal/onboarding-modal.tsxsrc/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsxsrc/components/auth/modals/onboarding-modal/steps/step-2-job.tsxsrc/components/auth/modals/onboarding-modal/steps/step-3-goals.tsxsrc/components/auth/modals/onboarding-modal/steps/step-4-completion.tsxsrc/types/api/class-onboarding.types.ts
| export const saveClassOnboardingStep1 = async ( | ||
| data: ClassOnboardingStep1Request, | ||
| ): Promise<ClassOnboardingStepResponse> => { | ||
| try { | ||
| const { data: resData } = await axiosInstance.post( | ||
| '/v6/class-onboarding/me/step-1', | ||
| data, | ||
| ); | ||
|
|
||
| if (resData.statusCode !== 200) { | ||
| throw new Error('Failed to save class onboarding step 1'); | ||
| } | ||
|
|
||
| return resData.content; | ||
| } catch (err) { | ||
| console.error('Error saving class onboarding step 1:', err); | ||
| throw err; | ||
| } | ||
| }; | ||
|
|
||
| export const saveClassOnboardingStep2 = async ( | ||
| data: ClassOnboardingStep2Request, | ||
| ): Promise<ClassOnboardingStepResponse> => { | ||
| try { | ||
| const { data: resData } = await axiosInstance.post( | ||
| '/v6/class-onboarding/me/step-2', | ||
| data, | ||
| ); | ||
|
|
||
| if (resData.statusCode !== 200) { | ||
| throw new Error('Failed to save class onboarding step 2'); | ||
| } | ||
|
|
||
| return resData.content; | ||
| } catch (err) { | ||
| console.error('Error saving class onboarding step 2:', err); | ||
| throw err; | ||
| } | ||
| }; | ||
|
|
||
| export const saveClassOnboardingStep3 = async ( | ||
| data: ClassOnboardingStep3Request, | ||
| ): Promise<ClassOnboardingStepResponse> => { | ||
| try { | ||
| const { data: resData } = await axiosInstance.post( | ||
| '/v6/class-onboarding/me/step-3', | ||
| data, | ||
| ); | ||
|
|
||
| if (resData.statusCode !== 200) { | ||
| throw new Error('Failed to save class onboarding step 3'); | ||
| } | ||
|
|
||
| return resData.content; | ||
| } catch (err) { | ||
| console.error('Error saving class onboarding step 3:', err); | ||
| throw err; | ||
| } | ||
| }; | ||
|
|
||
| export const saveClassOnboardingComplete = async ( | ||
| data: ClassOnboardingCompleteRequest, | ||
| ): Promise<ClassOnboardingStepResponse> => { | ||
| try { | ||
| const { data: resData } = await axiosInstance.post( | ||
| '/v6/class-onboarding/me/complete', | ||
| data, | ||
| ); | ||
|
|
||
| if (resData.statusCode !== 200) { | ||
| throw new Error('Failed to save class onboarding complete'); | ||
| } | ||
|
|
||
| return resData.content; | ||
| } catch (err) { | ||
| console.error('Error saving class onboarding complete:', err); | ||
| throw err; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: error-handler.ts 패턴과 다른 엔드포인트들의 에러 처리 방식 확인
# error-handler.ts 존재 및 사용 패턴 확인
fd -t f "error-handler" --exec cat {}
# 다른 엔드포인트 파일들의 에러 처리 패턴 확인
rg -n -A5 "axiosInstance\.(post|get)" src/api/endpoints/ --type=ts | rg -C3 "catch|error"Repository: code-zero-to-one/study-platform-client
Length of output: 17920
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) axiosInstance 정의/인터셉터 확인
fd -t f "axiosInstance" src/api --exec "rg -n \"axiosInstance\" {} -S" > /tmp/axiosinstance_refs.txt || true
echo "== axiosInstance references =="
head -n 200 /tmp/axiosinstance_refs.txt || true
# 2) axiosInstance/get/post 시그니처와 error 처리(인터셉터 reject 등) 확인
fd -t f "axios" src/api --exec "rg -n \"axiosInstance|interceptor|response\\.error|onFulfilled|onRejected\" {} -S" > /tmp/axios_interceptor_refs.txt || true
echo "== axios related refs (interceptors) =="
head -n 200 /tmp/axios_interceptor_refs.txt || true
# 3) class-onboarding 파일의 실제 내용 재확인(리뷰 스니펫과 일치하는지)
fd -t f "class-onboarding.ts" src/api/endpoints --exec "cat -n {} | sed -n '1,140p'" > /tmp/class_onboarding_cat.txt || true
echo "== class-onboarding.ts (excerpt) =="
cat /tmp/class_onboarding_cat.txt || true
# 4) 다른 endpoint들이 error-handler를 쓰는 패턴 확인
echo "== usage of analyzeError/sendErrorToSentry in endpoints =="
rg -n "analyzeError\\(|sendErrorToSentry\\(|useToastStore\\(" src/api/endpoints --type=ts -S | head -n 80 || true
# 5) endpoints에서 catch 내부에서 console.error를 쓰는 패턴 확인
echo "== console.error in endpoints catch blocks =="
rg -n "catch\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,300}?console\\.error" src/api/endpoints --type=ts -S | head -n 80 || trueRepository: code-zero-to-one/study-platform-client
Length of output: 965
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) class-onboarding.ts 실제 내용 확인 (리뷰 스니펫이 맞는지)
CLASS_ONBOARDING_FILE="$(find src -type f -path '*src/api/endpoints/class-onboarding/class-onboarding.ts' -print -quit || true)"
if [ -n "$CLASS_ONBOARDING_FILE" ]; then
echo "== class-onboarding.ts (first 120 lines) =="
sed -n '1,120p' "$CLASS_ONBOARDING_FILE" | nl -ba | head -n 140
else
echo "class-onboarding.ts not found"
fi
# 2) axiosInstance 정의/인터셉터 관련 파일 찾기
echo "== axiosInstance 문자열이 등장하는 파일(상위 50개) =="
grep -RIn --include='*.ts' --include='*.tsx' "axiosInstance" src/api | head -n 50 || true
# 3) 인터셉터( onFulfilled/onRejected / interceptor / response ) 확인
echo "== axios 인터셉터/리젝트 관련 문자열(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "interceptor|onFulfilled|onRejected|response\\.error|axios\\.interceptors" src/api | head -n 80 || true
# 4) error-handler 사용 패턴 확인 (analyzeError / sendErrorToSentry / useToastStore)
echo "== endpoints에서 analyzeError/sendErrorToSentry/useToastStore 사용(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "analyzeError\\(|sendErrorToSentry\\(|useToastStore\\(" src/api/endpoints | head -n 80 || true
# 5) endpoints catch 내부에서 console.error 사용 확인(상위 80개)
echo "== endpoints에서 console.error 사용(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "catch[^{]*\\{[^}]*console\\.error|console\\.error" src/api/endpoints | head -n 80 || true
# 6) utils/error-handler.ts 존재 확인 + 파일명/대략적 위치
echo "== utils/error-handler.ts 존재 여부 =="
find src -type f -path '*error-handler.ts' -print || trueRepository: code-zero-to-one/study-platform-client
Length of output: 187
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) class-onboarding.ts 정확한 위치/내용 확인
echo "== class-onboarding.ts 후보 찾기 =="
find src -type f -name 'class-onboarding.ts' -print | head -n 20 || true
CLASS_ONBOARDING_FILE="$(find src -type f -name 'class-onboarding.ts' -print -quit || true)"
if [ -n "$CLASS_ONBOARDING_FILE" ]; then
echo "== class-onboarding.ts 경로 =="
echo "$CLASS_ONBOARDING_FILE"
echo "== class-onboarding.ts line count =="
wc -l "$CLASS_ONBOARDING_FILE" | head -n 1 || true
echo "== class-onboarding.ts 첫 120줄 =="
head -n 120 "$CLASS_ONBOARDING_FILE" | awk '{printf "%5d\t%s\n", NR, $0}'
else
echo "class-onboarding.ts not found"
fi
# 2) error-handler.ts 존재 확인 및 간단 경로 출력
echo "== error-handler.ts 후보 찾기 =="
find src -type f -name 'error-handler.ts' -print || true
# 3) endpoints에서 console.error 사용(해당 파일 포함) 확인
echo "== src/api/endpoints 내 console.error 사용(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' "console\\.error" src/api/endpoints | head -n 80 || true
# 4) endpoints에서 analyzeError/sendErrorToSentry/useToastStore 호출 존재 여부
echo "== src/api/endpoints 내 analyzeError/sendErrorToSentry/useToastStore 호출(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "analyzeError\\(|sendErrorToSentry\\(|useToastStore\\(" src/api/endpoints | head -n 80 || true
# 5) axios 인스턴스/인터셉터가 어디에 정의되어 있는지(문자열 기반)
echo "== src/api 내 axiosInstance 정의/인터셉터 관련 문자열(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "axiosInstance|interceptor|onFulfilled|onRejected|axios\\.interceptors" src/api | head -n 80 || trueRepository: code-zero-to-one/study-platform-client
Length of output: 12692
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# axiosInstance 인터셉터가 error-handler를 호출하는지 확인
echo "== src/api/client/axios.ts 관련 구간 (interceptors/에러 처리) =="
sed -n '1,140p' src/api/client/axios.ts | awk '{printf "%4d\t%s\n", NR, $0}'
echo "== src/api/client/api-logger.ts 관련 구간 (interceptors/에러 처리) =="
sed -n '1,220p' src/api/client/api-logger.ts | awk '{printf "%4d\t%s\n", NR, $0}'
echo "== src/api/client/axiosV2.ts 관련 구간 (에러 처리) =="
sed -n '1,120p' src/api/client/axiosV2.ts | awk '{printf "%4d\t%s\n", NR, $0}'
# 에러 핸들러 호출이 실제로 어디서 이뤄지는지(전역적으로)
echo "== src/api 내 analyzeError/sendErrorToSentry 호출 위치 =="
grep -RIn --include='*.ts' --include='*.tsx' -E "analyzeError\\(|sendErrorToSentry\\(" src/api | head -n 80 || true
echo "== src 내 useToastStore().showToast 호출 위치 (에러 표시) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "useToastStore\\(\\)\\.[a-zA-Z0-9_]*showToast" src | head -n 80 || true
# class-onboarding에 toast 호출이 있는지
echo "== class-onboarding.ts 내 toast 호출 여부 =="
grep -n "useToastStore" -n src/api/endpoints/class-onboarding/class-onboarding.ts || trueRepository: code-zero-to-one/study-platform-client
Length of output: 9847
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src 전역 analyzeError/sendErrorToSentry 호출 위치(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "analyzeError\\(|sendErrorToSentry\\(" src | head -n 80 || true
echo "== src 전역 useToastStore().showToast 호출 위치(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "useToastStore\\(\\).*showToast\\(" src | head -n 80 || true
echo "== src 전역 showToast(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "showToast\\(" src | head -n 80 || true
echo "== endpoints에서 statusCode !== 200 사용 패턴(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "statusCode\\s*!==\\s*200" src/api/endpoints | head -n 80 || true
echo "== endpoints에서 statusCode === 200 사용 패턴(상위 80개) =="
grep -RIn --include='*.ts' --include='*.tsx' -E "statusCode\\s*===\\s*200" src/api/endpoints | head -n 80 || true
echo "== auth-response-interceptor 구현(관련 파일 헤더/일부) =="
for f in src/api/client/auth-response-interceptor.ts src/api/client/auth-response-interceptor.test.ts; do
if [ -f "$f" ]; then
echo "--- $f (first 220 lines) ---"
head -n 220 "$f" | awk '{printf "%4d\t%s\n", NR, $0}'
fi
done
echo "== auth-response-interceptor에서 error-handler 호출 여부(상위 40개) =="
grep -n -E "error-handler|analyzeError|sendErrorToSentry|showToast|useToastStore" -S src/api/client/auth-response-interceptor.ts src/api/client/auth-response-interceptor.test.ts 2>/dev/null || trueRepository: code-zero-to-one/study-platform-client
Length of output: 25555
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "saveClassOnboardingStep[1-3]|saveClassOnboardingComplete" src --type ts --type tsx || true
echo "== endpoints/class-onboarding 함수 호출부(앞/뒤 3줄) 후보 =="
for sym in \
"saveClassOnboardingStep1" \
"saveClassOnboardingStep2" \
"saveClassOnboardingStep3" \
"saveClassOnboardingComplete"
do
echo "--- $sym ---"
grep -RIn --include='*.ts' --include='*.tsx' "$sym" src | head -n 20 || true
doneRepository: code-zero-to-one/study-platform-client
Length of output: 1850
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# class-onboarding mutation 호출부(온에러/토스트 처리) 확인
FILES=(
"src/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx"
"src/components/auth/modals/onboarding-modal/steps/step-2-job.tsx"
"src/components/auth/modals/onboarding-modal/steps/step-3-goals.tsx"
"src/components/auth/modals/onboarding-modal/steps/step-4-completion.tsx"
)
for f in "${FILES[@]}"; do
if [ -f "$f" ]; then
echo "===== $f (relevant excerpt) ====="
# 에러 핸들러/토스트가 있는 부분만 대략적으로 추출
grep -n -E "onError|showToast|useToastStore|toast|mutationFn|mutation" "$f" | head -n 80 || true
# try/catch가 아니라 onError 쪽 로직 확인 위해 앞뒤 40줄씩 출력
# (line numbers 기반으로 대충 주변을 보여주기 위해 해당 라인 근처 출력)
awk 'NR>=1 && NR<=220 {print NR"\t"$0}' "$f" | head -n 220 | tail -n 220 || true
else
echo "Missing: $f"
fi
doneRepository: code-zero-to-one/study-platform-client
Length of output: 22113
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# query-client.ts에서 전역 onError가 toast를 띄우는지 확인
if [ -f src/config/query-client.ts ]; then
echo "== src/config/query-client.ts (first 220 lines) =="
head -n 220 src/config/query-client.ts | awk '{printf "%4d\t%s\n", NR, $0}'
else
echo "query-client.ts not found"
fi
# endpoints 호출부(온보딩 step)에서 onError가 toast를 띄우는지 더 넓게 검색
grep -RIn --include='*.ts' --include='*.tsx' -E "saveClassOnboardingStep[1-3]|saveClassOnboardingComplete" src/components/auth/modals/onboarding-modal | head -n 80 || trueRepository: code-zero-to-one/study-platform-client
Length of output: 3082
class-onboarding 엔드포인트 에러 처리가 utils/error-handler 기준에 맞지 않습니다.
src/api/endpoints/class-onboarding/class-onboarding.ts(10-88)의saveClassOnboardingStep1~saveClassOnboardingComplete는catch에서console.error후throw만 하고,utils/error-handler.ts의analyzeError/sendErrorToSentry및useToastStore기반 사용자 토스트 처리가 없습니다.- 이 API를 호출하는 온보딩 step 컴포넌트들은
useMutation에onError를 제공하며(예:step-1-nickname.tsx등), 그 경우 전역src/config/query-client.ts의MutationCache.onError(=useToastStore.getState().showToast+sendErrorToSentry)가 스킵됩니다. 즉, 현재 구현에서는 사용자 토스트/센트리 전송이 누락될 수 있습니다. resData.statusCode !== 200일 때도 백엔드의 에러 코드/메시지를 담지 못한 일반Error('Failed to ...')를 던져analyzeError가 의미 있는 userMessage를 만들기 어렵습니다.
🤖 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/api/endpoints/class-onboarding/class-onboarding.ts` around lines 10 - 88,
The
saveClassOnboardingStep1/saveClassOnboardingStep2/saveClassOnboardingStep3/saveClassOnboardingComplete
functions log and rethrow errors directly, which bypasses our global error
handling and loses backend error details; replace the catch blocks to call
analyzeError(err) and sendErrorToSentry(err) and then show a user toast via
useToastStore.getState().showToast(...) (using the userMessage from
analyzeError), and when resData.statusCode !== 200 include the backend error
payload/message (e.g., resData.message or resData.error) in the thrown error so
analyzeError can generate a meaningful userMessage; ensure each function uses
those same symbols (analyzeError, sendErrorToSentry,
useToastStore.getState().showToast) before rethrowing the error.
| import { useMutation } from '@tanstack/react-query'; | ||
| import { Check, Loader2, RefreshCw } from 'lucide-react'; | ||
| import Image from 'next/image'; | ||
| import { useRef, useState } from 'react'; | ||
| import { saveClassOnboardingStep1 } from '@/api/endpoints/class-onboarding/class-onboarding'; | ||
| import { cn } from '@/components/common/ui/(shadcn)/lib/utils'; | ||
| import { useNicknameCheckQuery } from '@/hooks/queries/auth/use-nickname-check'; | ||
| import { useCareersQuery } from '@/hooks/queries/user/use-update-user-profile-mutation'; | ||
| import type { CareerResponse } from '@/types/api/my-page.types'; | ||
| import { | ||
| VIBE_EXPERIENCE_OPTIONS, | ||
| type VibeCodingExperienceLevel, | ||
| } from '@/types/api/class-onboarding.types'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
뮤테이션 훅을 컴포넌트 밖 hooks/queries/로 다시 분리해 주세요.
이번 변경으로 Step 저장 로직이 UI 컴포넌트 안으로 다시 들어왔습니다. 이렇게 되면 API 호출 규약과 에러/상태 처리 패턴이 Step1~4에 다시 흩어집니다. 이 프로젝트 규칙대로 mutation hook을 hooks/queries/에 두고, 컴포넌트는 그 훅만 소비하는 구조로 유지하는 편이 맞습니다.
As per coding guidelines, src/**/*.tsx: Client Components must use TanStack Query hooks from hooks/queries/.
Also applies to: 61-63
🤖 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/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx` around
lines 3 - 13, The component-level mutation logic in step-1-nickname.tsx should
be moved into a dedicated React Query hook under hooks/queries/ (e.g. create and
export useSaveClassOnboardingStep1Mutation or useClassOnboardingStep1Mutation)
that encapsulates the useMutation call, calls saveClassOnboardingStep1, and
centralizes error/response handling and options; then import and call that hook
from the component instead of using useMutation directly; ensure the new hook
exports a consistent API (mutate/mutateAsync, status, error) so Step1's
component only consumes the hook and not the raw mutation implementation.
| const handleEtcClick = () => { | ||
| updateData('job', undefined); | ||
| setEtcMode(true); | ||
| if (!isOtherSelected) { | ||
| updateData('jobs', [...data.jobs, 'CLASS_ONBOARDING_OTHER']); | ||
| } | ||
| }; | ||
|
|
||
| const handleEtcAdd = () => { | ||
| if (!etcInput.trim()) return; | ||
| updateData('job', etcInput.trim()); | ||
| updateData('jobEtcText', etcInput.trim()); | ||
| setEtcMode(false); | ||
| }; |
There was a problem hiding this comment.
기타 직무 입력값을 다시 열 때 기존 값을 복원해 주세요.
jobEtcText를 저장한 뒤 이전 단계에서 돌아오면 입력창은 다시 빈 값으로 열립니다. 실제 제출에는 기존 값이 그대로 실리는데 화면에서는 확인/수정할 수 없어서 상태가 어긋납니다. handleEtcClick에서 data.jobEtcText로 etcInput을 채우거나, 선택 상태일 때 저장된 값을 함께 보여 주세요.
Also applies to: 142-167
🤖 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/components/auth/modals/onboarding-modal/steps/step-2-job.tsx` around
lines 57 - 68, When opening the "etc" input restore the previously saved text so
the UI and state match: in handleEtcClick (which currently calls
setEtcMode(true) and may call updateData for jobs) also set the input state from
data.jobEtcText (e.g. setEtcInput(data.jobEtcText || '')) when the Other option
is selected (isOtherSelected) so the textarea shows the saved value; ensure
handleEtcAdd still trims and saves via updateData('jobEtcText', etcInput.trim())
and closes the mode with setEtcMode(false). Also apply the same restoration
logic to the other occurrence referenced around lines 142-167.
| export function Step4Completion({ nickname }: Step4CompletionProps) { | ||
| const { close } = useOnboardingStore(); | ||
| const { mutate: signUp, isPending } = useSignUpMutation(); | ||
| const { mutateAsync: uploadImage } = useUploadProfileImageMutation(); | ||
| const router = useRouter(); | ||
| const { mutate: complete, isPending } = useMutation({ | ||
| mutationFn: () => | ||
| saveClassOnboardingComplete({ | ||
| confirmedOnboardingCompletion: true, | ||
| }), | ||
| }); | ||
|
|
||
| const handleFinish = () => { | ||
| const request: SignUpRequest = { | ||
| nickname: data.nickname, | ||
| ...(data.profileImageFile | ||
| ? { imageExtension: getImageExtension(data.profileImageFile) } | ||
| : {}), | ||
| ...(data.job ? { jobs: [data.job] } : {}), | ||
| ...(data.career ? { career: data.career } : {}), | ||
| ...(data.goals.length > 0 | ||
| ? { goal: buildGoalString(data.goals, data.goalEtcText) } | ||
| : {}), | ||
| }; | ||
|
|
||
| onSubmittingChange(true); | ||
| signUp(request, { | ||
| onSuccess: async (response) => { | ||
| const newToken = response?.content?.accessToken; | ||
| if (newToken) writeAccessTokenSession(newToken); | ||
| const uploadUrl = response?.content?.uploadUrl; | ||
| try { | ||
| if (uploadUrl && data.profileImageFile) { | ||
| const formData = new FormData(); | ||
| formData.append('file', data.profileImageFile); | ||
| await uploadImage({ uploadUrl, file: formData }); | ||
| } | ||
| } catch { | ||
| useToastStore | ||
| .getState() | ||
| .showToast('프로필 이미지 업로드에 실패했어요.', 'error'); | ||
| } finally { | ||
| close(); | ||
| onSubmittingChange(false); | ||
| } | ||
| }, | ||
| onError: () => { | ||
| onSubmittingChange(false); | ||
| complete(undefined, { | ||
| onSuccess: () => { | ||
| close(); | ||
| router.push('/class'); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
완료 요청도 부모 제출 잠금과 연결해 주세요.
Step1~3와 달리 여기서는 onSubmittingChange가 없어 완료 mutation 동안 모달을 그대로 닫거나 뒤로 갈 수 있습니다. 그 상태에서 요청이 성공하면 나중에 close()와 router.push('/class')가 뒤늦게 실행돼 흐름이 튈 수 있습니다. src/components/auth/modals/onboarding-modal/onboarding-modal.tsx의 제출 잠금과 같은 방식으로 Step4도 연결해 주세요.
🤖 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/components/auth/modals/onboarding-modal/steps/step-4-completion.tsx`
around lines 13 - 29, Step4Completion currently starts the completion mutation
without toggling the parent "submitting" lock, so the modal can be closed or
navigated away mid-request; before calling complete() set the onboarding
submitting flag (use the same API used in onboarding-modal.tsx, e.g. the
onSubmittingChange/setSubmitting method from useOnboardingStore or the
onSubmittingChange prop) to true, and ensure you reset it to false in the
mutation callbacks (onSuccess/onError or onSettled) so the modal/back navigation
remain locked for the duration of saveClassOnboardingComplete; update
handleFinish and the useMutation callbacks in Step4Completion to mirror the
submission-lock pattern used in onboarding-modal.tsx.
Problem
온보딩 4단계(완료) 화면에서
completeAPI 호출이 누락되어 있었습니다.또한 기존
class-onboarding엔드포인트 함수들이.then()체이닝 방식으로 구현되어 에러 처리가 없었고, 코드베이스의 다른 엔드포인트 파일들(채널, 그룹스터디 등)과 패턴이 불일치했습니다.Solution
saveClassOnboardingCompleteAPI 함수 및 관련 타입(ClassOnboardingCompleteRequest,ClassOnboardingStepResponse) 추가async/await+try-catch+console.error패턴으로 통일 (delete-comment.ts등 기존 패턴 기준)as타입 단언 제거 → 명시적Promise<ClassOnboardingStepResponse>반환 타입으로 대체use-class-onboarding-mutation.ts훅 파일 삭제 → 각 컴포넌트에useMutation인라인 적용Changes
Features
src/types/api/class-onboarding.types.tsClassOnboardingCompleteRequest,ClassOnboardingStepResponse등 타입 추가src/api/endpoints/class-onboarding/class-onboarding.tssteps/step-4-completion.tsxBug Fixes
steps/step-1-nickname.tsx~step-4-completion.tsxuseMutation직접 사용Result
POST /v6/class-onboarding/me/complete호출됨MutationCache.onError를 통해 토스트 표시Test plan
POST /v6/class-onboarding/me/complete요청 발생 확인🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트