Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { useRouter } from 'next/navigation';
import { startTransition } from 'react';
import Button from '@/shared/ui/button';

export default function ErrorBoundary({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
const router = useRouter();

return (
<div className="flex h-[calc(100vh-45px)] flex-col items-center justify-center gap-400">
<div className="flex flex-col items-center justify-center gap-150">
<h1 className="font-designer-28b">
서비스 이용에 불편드려 죄송합니다.
</h1>
<div className="flex flex-col items-center gap-50">
<span className="font-designer-16m text-text-subtle">
에러가 발생하여 페이지를 표시할 수 없습니다.
</span>
<span className="font-designer-16m text-text-subtle">
잠시 뒤에 다시 시도해주세요.
</span>
</div>
</div>
<Button
size="large"
onClick={() => {
startTransition(() => {
router.refresh(); // 서버 컴포넌트들을 다시 렌더링
reset(); // 에러 상태 초기화
Comment on lines +34 to +36
Copy link
Contributor Author

Choose a reason for hiding this comment

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

router.refresh 메서드가 비동기적으로 동작하여, 서버 컴포넌트를 다시 렌더링하지 못하는 문제가 있었어요.
refresh 메서드가 promise를 반환하지 않기 때문에 async await을 사용할 수 없었어요.
그래서 startTransition을 써서 refresh, reset 작업의 우선순위를 뒤로 늦췄어요. refresh 실행한 다음에 reset이 일어난다 까지는 찾지 못했으나, React 내부적으로 refresh가 reset 작업보다 먼저 처리하는 것 같아요.
참고 블로그

startTransition의 콜백 안에 두 메서드 다 넣는 경우도 있고, reset 만 넣는 경우도 있었어요.

});
}}
>
새로고침
</Button>
</div>
);
}
10 changes: 10 additions & 0 deletions src/entities/review/model/use-review-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UserPositiveKeywordsRequest,
} from '@/entities/review/api/review-types';
import { getKoreaDate } from '@/shared/lib/time';
import { isApiError } from '@/shared/tanstack-query/api-error';
import {
addStudyReview,
getUserPositiveKeywords,
Expand All @@ -32,6 +33,15 @@ export const useAddStudyReviewMutation = () => {
// todo: 모달로 변경
alert('후기 작성이 완료되었습니다.');
},
onError: (error) => {
if (isApiError(error)) {
if (error.errorCode === 'CMM001') {
alert('이미 후기를 작성했습니다.');
} else if (error.errorCode === 'CMM003') {
alert('스터디 또는 스터디 멤버가 존재하지 않습니다.');
}
}
},
});
};

Expand Down
11 changes: 10 additions & 1 deletion src/entities/user/model/use-user-profile-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/entities/user/api/get-user-profile';
import type { GetUserProfileResponse } from '@/entities/user/api/types';
import { hashValue } from '@/shared/lib/hash';
import { isApiError } from '@/shared/tanstack-query/api-error';

export const useUserProfileQuery = (memberId: number) => {
return useQuery<GetUserProfileResponse>({
Expand Down Expand Up @@ -45,10 +46,18 @@ export const usePatchAutoMatchingMutation = () => {
return { prev };
},

onError: (_err, { memberId }, ctx) => {
onError: (error, { memberId }, ctx) => {
if (ctx?.prev) {
qc.setQueryData(['userProfile', memberId], ctx.prev);
}

if (isApiError(error)) {
if (error.errorCode === 'MEM004') {
alert('아직 스터디를 신청하지 않았습니다. 먼저 스터디 신청해주세요.');
} else if (error.errorCode === 'MEM001') {
alert('회원 정보가 존재하지 않습니다.');
}
}
},

onSuccess: async (_data, { memberId, autoMatching }) => {
Expand Down
8 changes: 8 additions & 0 deletions src/features/auth/model/use-auth-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { logout, signUp, uploadProfileImage } from '@/features/auth/api/auth';
import { hashValue } from '@/shared/lib/hash';
import { isApiError } from '@/shared/tanstack-query/api-error';
import { deleteCookie, getCookie } from '@/shared/tanstack-query/cookie';
import { SignUpResponse } from './types';

Expand All @@ -16,6 +17,13 @@ export const useSignUpMutation = () => {
{ name: string; imageExtension: string }
>({
mutationFn: (data: any) => signUp(data),
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The parameter type is specified as any, which defeats TypeScript's type safety. Define a proper type for the signup data or use the existing types from your API layer.

Suggested change
mutationFn: (data: any) => signUp(data),
mutationFn: (data: { name: string; imageExtension: string }) => signUp(data),

Copilot uses AI. Check for mistakes.
onError: (error) => {
if (isApiError(error)) {
if (error.errorCode === 'DUPLICATE_MEMBER') {
alert('이미 가입된 회원입니다.');
}
}
},
});
};

Expand Down
10 changes: 10 additions & 0 deletions src/features/my-page/model/use-update-user-profile-mutation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { isApiError } from '@/shared/tanstack-query/api-error';
import {
UpdateUserProfileInfoRequest,
UpdateUserProfileRequest,
Expand All @@ -24,6 +25,15 @@ export const useUpdateUserProfileMutation = (memberId: number) => {
onSuccess: () => {
router.refresh();
},
onError: (error) => {
if (isApiError(error)) {
if (error.errorCode === 'MPR001') {
alert('관심사가 중복됐습니다.');
} else if (error.errorCode === 'MEM001') {
alert('회원 정보가 존재하지 않습니다.');
}
}
},
});
};

Expand Down