Skip to content

온보딩 step-4 complete API 연동 및 에러 처리 통일#712

Open
HA-SEUNG-JEONG wants to merge 6 commits into
developfrom
fix/alpha-test
Open

온보딩 step-4 complete API 연동 및 에러 처리 통일#712
HA-SEUNG-JEONG wants to merge 6 commits into
developfrom
fix/alpha-test

Conversation

@HA-SEUNG-JEONG
Copy link
Copy Markdown
Contributor

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented May 27, 2026

Problem

온보딩 4단계(완료) 화면에서 complete API 호출이 누락되어 있었습니다.
또한 기존 class-onboarding 엔드포인트 함수들이 .then() 체이닝 방식으로 구현되어 에러 처리가 없었고, 코드베이스의 다른 엔드포인트 파일들(채널, 그룹스터디 등)과 패턴이 불일치했습니다.

Solution

  • saveClassOnboardingComplete API 함수 및 관련 타입(ClassOnboardingCompleteRequest, ClassOnboardingStepResponse) 추가
  • step-4 컴포넌트에 complete API 연동
  • 모든 엔드포인트 함수를 async/await + try-catch + console.error 패턴으로 통일 (delete-comment.ts 등 기존 패턴 기준)
  • as 타입 단언 제거 → 명시적 Promise<ClassOnboardingStepResponse> 반환 타입으로 대체
  • thin wrapper였던 use-class-onboarding-mutation.ts 훅 파일 삭제 → 각 컴포넌트에 useMutation 인라인 적용

Changes

Features

File Description
src/types/api/class-onboarding.types.ts ClassOnboardingCompleteRequest, ClassOnboardingStepResponse 등 타입 추가
src/api/endpoints/class-onboarding/class-onboarding.ts 4개 POST 엔드포인트 함수 — async/await + try-catch 패턴, 명시적 반환 타입
steps/step-4-completion.tsx complete API 연동 — 성공 시 라우팅, 스토어 초기화

Bug Fixes

File Description
steps/step-1-nickname.tsx ~ step-4-completion.tsx 얇은 훅 래퍼 의존 제거 → useMutation 직접 사용

Result

  • 온보딩 4단계 완료 버튼 클릭 시 POST /v6/class-onboarding/me/complete 호출됨
  • API 실패 시 글로벌 MutationCache.onError를 통해 토스트 표시
  • 엔드포인트 에러 처리가 코드베이스 전반과 일관된 패턴으로 통일됨

Test plan

  • 온보딩 step-1 ~ step-3 정상 제출 후 step-4 진입 확인
  • step-4 완료 버튼 클릭 시 POST /v6/class-onboarding/me/complete 요청 발생 확인
  • API 실패 시 에러 토스트 표시 확인
  • 온보딩 완료 후 정상 라우팅 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 클래스 온보딩 단계별 저장 기능 추가
    • 온보딩 첫 단계에 바이브 코딩 경험 수준 선택 옵션 추가
    • 직무 및 관심 분야 다중 선택 기능 추가
    • 온보딩 완료 단계 간소화

Review Change Stack

HA-SEUNG-JEONG and others added 6 commits May 27, 2026 22:25
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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment May 27, 2026 2:45pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

클래스 온보딩 API 저장 엔드포인트 4개가 추가되고, 온보딩 모달의 상태 구조(동의 플래그, 배열 필드) 및 Step1~Step4 컴포넌트가 일관된 뮤테이션 기반 제출 패턴으로 재구성되었습니다. Step1은 바이브 경험수준 선택, Step2는 직무 다중선택, Step3은 관심사 선택, Step4는 완료 뮤테이션으로 동작합니다.

Changes

Class Onboarding API and Modal Refactoring

Layer / File(s) Summary
API contracts and option types
src/types/api/class-onboarding.types.ts
온보딩 API 요청/응답 인터페이스(ClassOnboardingStep1Request ~ Step4Request, ClassOnboardingStepResponse)와 선택 옵션 상수(VIBE_EXPERIENCE_OPTIONS, JOB_OPTIONS, CAREER_OPTIONS, INTEREST_OPTIONS)를 정의하고, 각 옵션 배열의 값들로부터 타입(VibeCodingExperienceLevel, ClassOnboardingJob, ClassOnboardingCareer, ClassOnboardingInterest) 추출.
Onboarding API endpoint implementations
src/api/endpoints/class-onboarding/class-onboarding.ts
Step1~Step4에 대한 POST 엔드포인트 함수(saveClassOnboardingStep1, Step2, Step3, Complete) 구현, 각 함수는 해당 스텝의 요청 타입을 받아 /v6/class-onboarding/me/step-* 경로로 저장하고 statusCode 검증 후 content 반환 또는 예외 발생.
Modal state shape and step orchestration
src/components/auth/modals/onboarding-modal/onboarding-modal.tsx
온보딩 모달 상태 타입(OnboardingData) 재구성: 동의 플래그(termsConsent, privacyConsent, marketingConsent) 및 배열 필드(jobs, interests) 추가, initialData 상수로 초기화, 모달 열림 시 상태 리셋, Step1~3에 onSubmittingChange 콜백 전달, Step4에는 nickname 단일 값 전달.
Step 1: Vibe experience level and consent flow
src/components/auth/modals/onboarding-modal/steps/step-1-nickname.tsx
커리어 선택을 바이브 코딩 경험수준 선택으로 변경, 약관 동의 체크박스 추가(termsConsent, privacyConsent, marketingConsent), useMutationsaveClassOnboardingStep1 연동, canProceed 조건 재정의, 제출 성공/실패 시 onSubmittingChange 상태 관리.
Step 2: Multi-select jobs and career with mutation
src/components/auth/modals/onboarding-modal/steps/step-2-job.tsx
단일 job 필드를 jobs 배열로 변경, 직무 선택은 JOB_OPTIONS 순회로 토글, 커리어는 CAREER_OPTIONS에서 선택, 기타 입력(jobEtcText) 조건부 저장, useMutationsaveClassOnboardingStep2 연동, canProceedisPending 반영.
Step 3: Interest options and mutation flow
src/components/auth/modals/onboarding-modal/steps/step-3-goals.tsx
goals 필드를 interests 필드로 변경, 선택 옵션 소스를 GOAL_OPTIONS에서 INTEREST_OPTIONS로 교체, 기타 입력(interestEtcText) 조건부 표시, useMutationsaveClassOnboardingStep3 연동, 버튼 disabled 및 스타일에 isPending 반영.
Step 4: Complete onboarding with single mutation
src/components/auth/modals/onboarding-modal/steps/step-4-completion.tsx
회원가입/이미지 업로드 흐름 제거, saveClassOnboardingComplete 단일 뮤테이션으로 통합, props 구조를 nickname 단일 값으로 단순화, 성공 시 모달 닫고 /class 라우팅.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • code-zero-to-one/study-platform-client#685: 메인 PR이 OnboardingModal의 Step1~Step4 및 상태/props 구조를 바꿔 saveClassOnboardingStep*/saveClassOnboardingComplete로 저장 뮤테이션 흐름을 연결한 점이, retrieved PR에서 동일한 온보딩 모달·스텝 컴포넌트(특히 onboarding-modal/step-*.tsx) 구현을 추가한 변경과 직접 코드 레벨로 겹칩니다.

  • code-zero-to-one/study-platform-client#689: Main PR's onboarding modal/step components (e.g., src/components/auth/modals/onboarding-modal/** and their step props/flows) build directly on the onboarding modal implementation introduced in the retrieved PR (#689), so the changes overlap at the same UI/module code paths.

Suggested labels

🐛 bug

Poem

🐰 온보딩 흐름이 정리되고,
API 엔드포인트가 네 길로 나뉘어,
각 단계가 뮤테이션으로 춤을 춘다.
바이브, 직무, 관심사, 그리고 완료—
토끼가 응원하는 깔끔한 재설계! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 온보딩 step-4 complete API 연동과 에러 처리 통일을 명확히 설명하며, 변경 사항의 핵심(4단계 API 연동 + 에러 처리 패턴 통일)을 정확히 반영합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/alpha-test

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e10bb15 and 2442ebb.

📒 Files selected for processing (7)
  • src/api/endpoints/class-onboarding/class-onboarding.ts
  • src/components/auth/modals/onboarding-modal/onboarding-modal.tsx
  • 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
  • src/types/api/class-onboarding.types.ts

Comment on lines +10 to +88
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;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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
done

Repository: 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
done

Repository: 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 || true

Repository: 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~saveClassOnboardingCompletecatch에서 console.errorthrow만 하고, utils/error-handler.tsanalyzeError/sendErrorToSentryuseToastStore 기반 사용자 토스트 처리가 없습니다.
  • 이 API를 호출하는 온보딩 step 컴포넌트들은 useMutationonError를 제공하며(예: step-1-nickname.tsx 등), 그 경우 전역 src/config/query-client.tsMutationCache.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.

Comment on lines +3 to +13
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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines 57 to 68
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);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

기타 직무 입력값을 다시 열 때 기존 값을 복원해 주세요.

jobEtcText를 저장한 뒤 이전 단계에서 돌아오면 입력창은 다시 빈 값으로 열립니다. 실제 제출에는 기존 값이 그대로 실리는데 화면에서는 확인/수정할 수 없어서 상태가 어긋납니다. handleEtcClick에서 data.jobEtcTextetcInput을 채우거나, 선택 상태일 때 저장된 값을 함께 보여 주세요.

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.

Comment on lines +13 to 29
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');
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

완료 요청도 부모 제출 잠금과 연결해 주세요.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant