Skip to content
Merged
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
22 changes: 21 additions & 1 deletion src/api/apiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,20 @@ export async function fetchWrapper(
});

if (!response.ok) {
throw new CustomError(`HTTP error! status: ${response.status}`, await response.json());
const err = new CustomError(
`HTTP error! status: ${response.status}`,
await safeJson(response),
);
(err as any).statusCode = response.status;
throw err;
}
} else {
const err = new CustomError(
`HTTP error! status: ${response.status}`,
await safeJson(response),
);
(err as any).statusCode = response.status;
throw err;
}
}

Expand All @@ -69,3 +81,11 @@ export async function fetchWrapper(
throw error;
}
}

async function safeJson(res: Response) {
try {
return await res.json();
} catch {
return null;
}
}
1 change: 1 addition & 0 deletions src/api/authApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const postSignup = async (name: string, email: string, password: string)
name: name,
email: email,
password: password,
eulaEnabled: true,
};

try {
Expand Down
47 changes: 47 additions & 0 deletions src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { useEffect, useMemo } from 'react';

import { useRouter } from 'next/navigation';

import ErrorFallback from '@/components/ui/ErrorFallback';

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const router = useRouter();

const target = useMemo(() => {
const code = (error as any)?.statusCode ?? (error as any)?.status;
if (typeof code !== 'number') return null;
if (code >= 500 && code < 600) return '/error/500error';
if (code >= 400 && code < 500) return '/error/400error';
return null;
}, [error]);

useEffect(() => {
if (target) router.replace(target);
}, [target, router]);

return (
<html suppressHydrationWarning>
<body className="bg-muted/30 flex min-h-dvh items-center justify-center">
{target ? null : (
<ErrorFallback
type="general"
title="문제가 발생했어요"
subTitle="잠시 후 다시 시도해 주세요."
primaryLabel="다시 시도"
onRetry={reset}
secondaryLabel="홈으로"
onNavigate={() => router.push('/')}
/>
)}
</body>
</html>
);
}
18 changes: 18 additions & 0 deletions src/app/error/400error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';
import ErrorFallback from '@/components/ui/ErrorFallback';

export default function ClientErrorPage() {
return (
<div className="bg-background flex h-screen w-screen items-center justify-center">
<ErrorFallback
type="notFound"
title="페이지를 찾을 수 없어요"
subTitle="요청하신 페이지가 존재하지 않습니다"
primaryLabel="홈으로"
secondaryLabel="이전 페이지"
onNavigate={() => (window.location.href = '/')}
navigateHref="/"
/>
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/error/500error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';
import ErrorFallback from '@/components/ui/ErrorFallback';

export default function ServerErrorPage() {
return (
<div className="bg-background flex h-screen w-screen items-center justify-center">
<ErrorFallback
type="general"
title="문제가 발생했어요"
subTitle="잠시 후 다시 시도해 주세요"
primaryLabel="다시 시도"
secondaryLabel="홈으로"
onNavigate={() => (window.location.href = '/')}
navigateHref="/"
/>
</div>
);
}
34 changes: 34 additions & 0 deletions src/app/providers/RQErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';
import React from 'react';

export default class RQErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

componentDidCatch(error: any) {
const status = error?.statusCode ?? error?.status ?? error?.response?.status;

if (typeof status === 'number') {
if (status >= 500) {
window.location.replace('/error/500error');
} else if (status >= 400) {
window.location.replace('/error/400error');
}
}
}

render() {
if (this.state.hasError) return null;

return this.props.children;
}
}
30 changes: 22 additions & 8 deletions src/app/providers/ReactQueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@

import { useState } from 'react';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';

const ReactQueryProvider = ({ children }: React.PropsWithChildren) => {
const handleGlobalError = (error: unknown) => {
const e = error as any;
const status = e?.statusCode ?? e?.status ?? e?.response?.status;

const url =
e?.config?.url || e?.response?.url || e?.message?.includes('/auth/login') ? '/auth/login' : '';

console.log('[🧩 ReactQuery GlobalError]', status, url);

if (url.includes('/auth/login')) return;

if (!status) return;
if (status >= 500) window.location.replace('/error/500error');
else if (status >= 400) window.location.replace('/error/400error');
};

export default function ReactQueryProvider({ children }: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({ onError: handleGlobalError }),
mutationCache: new MutationCache({ onError: handleGlobalError }),
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
queries: { retry: 0, staleTime: 60_000 },
},
}),
);

return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};

export default ReactQueryProvider;
}
20 changes: 13 additions & 7 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,21 @@ export default function LoginForm() {
const password = watch('password');
const isFormValid = email.trim() !== '' && password.trim() !== '';

const login = useLogin({
onError: () => {
setIsModalOpen(true);
},
});
const login = useLogin({ onError: () => {} });

const onSubmit = (formData: LoginFormData) => {
const onSubmit = async (formData: LoginFormData) => {
setIsModalOpen(false);
login.mutate(formData);
try {
await login.mutateAsync(formData);
} catch (err: any) {
const status = err?.statusCode ?? err?.status ?? err?.response?.status;

if (status >= 500) {
window.location.replace('/error/500error');
return;
}
setIsModalOpen(true);
}
};

if (login.isPending || login.isSuccess) return <CustomLoading />;
Expand Down
Loading