Skip to content
88 changes: 88 additions & 0 deletions src/api/endpoints/class-onboarding/class-onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { axiosInstance } from '@/api/client/axios';
import type {
ClassOnboardingCompleteRequest,
ClassOnboardingStep1Request,
ClassOnboardingStep2Request,
ClassOnboardingStep3Request,
ClassOnboardingStepResponse,
} from '@/types/api/class-onboarding.types';

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

70 changes: 42 additions & 28 deletions src/components/auth/modals/onboarding-modal/onboarding-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
import { useOnboardingStore } from '@/stores/use-onboarding-store';
import type {
ClassOnboardingCareer,
ClassOnboardingInterest,
ClassOnboardingJob,
VibeCodingExperienceLevel,
} from '@/types/api/class-onboarding.types';
import { Step1Nickname } from './steps/step-1-nickname';
import { Step2Job } from './steps/step-2-job';
import { Step3Goals } from './steps/step-3-goals';
Expand All @@ -16,27 +22,32 @@ interface OnboardingData {
nickname: string;
profileImageUrl?: string;
profileImageFile?: File;
career?: string;
job?: string;
goals: string[];
goalEtcText?: string;
termsAgreed: boolean;
privacyAgreed: boolean;
marketingAgreed: boolean;
vibeCodingExperienceLevel?: VibeCodingExperienceLevel;
jobs: ClassOnboardingJob[];
jobEtcText?: string;
career?: ClassOnboardingCareer;
interests: ClassOnboardingInterest[];
interestEtcText?: string;
privacyConsent: boolean;
termsConsent: boolean;
marketingConsent: boolean;
}

const initialData: OnboardingData = {
nickname: '',
jobs: [],
interests: [],
privacyConsent: false,
termsConsent: false,
marketingConsent: false,
};

export function OnboardingModal() {
const { isOpen, close } = useOnboardingStore();
const [mounted, setMounted] = useState(false);
const [currentStep, setCurrentStep] = useState<Step>(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [data, setData] = useState<OnboardingData>({
nickname: '',
goals: [],
termsAgreed: false,
privacyAgreed: false,
marketingAgreed: false,
});
const [data, setData] = useState<OnboardingData>(initialData);

useEffect(() => {
setMounted(true);
Expand All @@ -46,13 +57,7 @@ export function OnboardingModal() {
useEffect(() => {
if (isOpen) {
setCurrentStep(1);
setData({
nickname: '',
goals: [],
termsAgreed: false,
privacyAgreed: false,
marketingAgreed: false,
});
setData(initialData);
}
}, [isOpen]);

Expand All @@ -78,20 +83,29 @@ export function OnboardingModal() {
data={data}
updateData={updateData}
onNext={handleNext}
onSubmittingChange={setIsSubmitting}
/>
);
case 2:
return (
<Step2Job data={data} updateData={updateData} onNext={handleNext} />
<Step2Job
data={data}
updateData={updateData}
onNext={handleNext}
onSubmittingChange={setIsSubmitting}
/>
);
case 3:
return (
<Step3Goals data={data} updateData={updateData} onNext={handleNext} />
<Step3Goals
data={data}
updateData={updateData}
onNext={handleNext}
onSubmittingChange={setIsSubmitting}
/>
);
case 4:
return (
<Step4Completion data={data} onSubmittingChange={setIsSubmitting} />
);
return <Step4Completion nickname={data.nickname} />;
}
};

Expand All @@ -105,7 +119,7 @@ export function OnboardingModal() {
/>

{/* Modal panel */}
<div className="relative flex max-h-[90vh] w-full max-w-7500 flex-col overflow-hidden rounded-200 bg-white">
<div className="relative flex max-h-modal w-full max-w-7500 flex-col overflow-hidden rounded-200 bg-white">
{/* Header */}
<div className="flex items-center justify-between px-500 pt-400 pb-300">
{/* Back button */}
Expand All @@ -116,7 +130,7 @@ export function OnboardingModal() {
className="rounded-100 p-100 text-gray-500 transition-colors hover:bg-gray-100"
aria-label="이전"
>
<X className="h-300 w-300 rotate-[135deg]" />
<X className="h-300 w-300 rotate-135" />
</button>
) : (
<div className="size-500" />
Expand Down
Loading
Loading