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
57 changes: 57 additions & 0 deletions src/pages/common/ErrorPage/ErrorPage.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import styled from '@emotion/styled'

import { theme } from '@/shared/styles/theme'

export const Container = styled.div`
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 40px 20px;
text-align: center;
background: ${theme.colors.background};
color: ${theme.colors.textPrimary};
`

export const Title = styled.h1`
font-size: 28px;
font-weight: 700;
margin: 0;
`

export const Description = styled.p`
font-size: 16px;
color: ${theme.colors.textColor3};
margin: 0;
`

export const ButtonRow = styled.div`
display: flex;
gap: 12px;
margin-top: 8px;
`

const BaseButton = styled.button`
border: 0;
border-radius: 10px;
padding: 10px 16px;
font-size: 14px;
cursor: pointer;

&:focus-visible {
outline: 2px solid ${theme.colors.primary2};
outline-offset: 2px;
}
`

export const PrimaryButton = styled(BaseButton)`
background: ${theme.colors.primary};
color: ${theme.colors.white};
`

export const SecondaryButton = styled(BaseButton)`
background: ${theme.colors.lightGray};
color: ${theme.colors.textPrimary};
`
25 changes: 25 additions & 0 deletions src/pages/common/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useNavigate, useRouteError } from 'react-router-dom'

import * as S from './ErrorPage.styles'

export default function ErrorPage() {
const navigate = useNavigate()
const error = useRouteError() as { status?: number; statusText?: string; message?: string } | null

const description = error?.statusText || error?.message || '요청하신 페이지를 찾을 수 없어요.'

return (
<S.Container>
<S.Title>문제가 발생했어요</S.Title>
<S.Description>{description}</S.Description>
<S.ButtonRow>
<S.SecondaryButton type="button" onClick={() => navigate(-1)}>
뒤로가기
</S.SecondaryButton>
<S.PrimaryButton type="button" onClick={() => navigate('/')}>
홈으로
</S.PrimaryButton>
</S.ButtonRow>
</S.Container>
)
}
11 changes: 10 additions & 1 deletion src/routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { createBrowserRouter } from 'react-router-dom'

import ErrorPage from '@/pages/common/ErrorPage/ErrorPage'

import AuthRoutes from './AuthRoutes'
import MainRoutes from './MainRoutes'

export const router = createBrowserRouter([AuthRoutes, MainRoutes])
export const router = createBrowserRouter([
AuthRoutes,
MainRoutes,
{
path: '*',
element: <ErrorPage />,
},
])
19 changes: 19 additions & 0 deletions src/shared/api/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from 'axios'

export const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_SERVER_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
})

//TODO: @yujin5959 나중에 이부분 해주세용~
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error)
},
)

export default axiosInstance
142 changes: 142 additions & 0 deletions src/shared/hooks/customQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type {
InfiniteData,
MutationFunction,
QueryFunction,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
UseSuspenseQueryOptions,
} from '@tanstack/react-query'
import { useInfiniteQuery, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'

type DefaultQueryOptions = {
staleTime?: number
retry?: number
}

const STALE_TIME = 1000 * 60 * 3
const RETRY = 1
const MUTATION_RETRY = 1

// 기본적인 쿼리 흐름을 정리한 커스텀 훅들:
// 1) useCustomQuery: 일반 `useQuery` 래퍼로, 스테일 시간·재시도 횟수·포커스 시 리패치 제한을 기본값으로 잡아줍니다.
// 최초 키/펑션 이외 값을 옵션에 넘겨도 기본 설정을 덮어쓰지 않고 안전하게 적용합니다.
// 2) useCustomInfiniteQuery: 페이지 단위 API를 위한 `useInfiniteQuery` 래퍼로, 위와 동일한 기본값에 더해 `keepPreviousData` 없이 연속 페이지를 유지합니다.

export const useCustomQuery = <
TQueryFnData,
TError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryKey' | 'queryFn'> &
DefaultQueryOptions,
) => {
const safeOptions =
options ??
({} as Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryKey' | 'queryFn'> &
DefaultQueryOptions)

const {
staleTime,
retry,
// refetchOnWindowFocus: _refetchOnWindowFocus, // 제거: 사용되지 않음
...restOptions
} = safeOptions

return useQuery<TQueryFnData, TError, TData, TQueryKey>({
queryKey,
queryFn,
staleTime: staleTime ?? STALE_TIME,
retry: retry ?? RETRY,
refetchOnWindowFocus: false,
...restOptions,
})
}

export const useCustomSuspenseQuery = <
TQueryFnData,
TError extends Error = Error,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<
UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn'
> &
DefaultQueryOptions,
) => {
const safeOptions =
options ??
({} as Omit<
UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn'
> &
DefaultQueryOptions)

const {
staleTime,
retry,
// refetchOnWindowFocus: _refetchOnWindowFocus, // 제거: 사용되지 않음
...restOptions
} = safeOptions

return useSuspenseQuery<TQueryFnData, TError, TData, TQueryKey>({
queryKey,
queryFn,
staleTime: staleTime ?? STALE_TIME,
retry: retry ?? RETRY,
refetchOnWindowFocus: false,
...restOptions,
})
}

export const useCustomInfiniteQuery = <
TQueryFnData,
TError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey, TPageParam>,
infiniteOptions?: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> &
DefaultQueryOptions,
) => {
const safeOptions =
infiniteOptions ??
({} as UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> &
DefaultQueryOptions)

const { staleTime, retry, queryKey: _queryKey, queryFn: _queryFn, ...restOptions } = safeOptions

return useInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>({
queryKey,
queryFn,
staleTime: staleTime ?? STALE_TIME,
retry: retry ?? RETRY,
refetchOnWindowFocus: false,
...restOptions,
})
}

export const useCustomMutation = <TData, TError, TVariables = void, TContext = unknown>(
mutationFn: MutationFunction<TData, TVariables>,
options?: UseMutationOptions<TData, TError, TVariables, TContext> & DefaultQueryOptions,
) => {
const safeOptions =
options ?? ({} as UseMutationOptions<TData, TError, TVariables, TContext> & DefaultQueryOptions)
const { retry, ...restOptions } = safeOptions

return useMutation<TData, TError, TVariables, TContext>({
mutationFn,
retry: retry ?? MUTATION_RETRY,
...restOptions,
})
}
78 changes: 78 additions & 0 deletions src/shared/ui/common/AsyncBoundary/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { ErrorInfo, ReactNode } from 'react'
import { Component, Suspense } from 'react'
import { useLocation } from 'react-router-dom'

import AsyncBoundaryFallback from './AsyncBoundaryFallback'

type AsyncBoundaryProps = {
children: ReactNode
fallback: ReactNode
errorFallback?: (error: Error, reset: () => void) => ReactNode
resetKeys?: Array<unknown>
}

type AsyncBoundaryState = {
error?: Error
}

class ErrorBoundary extends Component<
{
children: ReactNode
fallback?: (error: Error, reset: () => void) => ReactNode
resetKeys?: Array<unknown>
},
AsyncBoundaryState
> {
state: AsyncBoundaryState = {}

static getDerivedStateFromError(error: Error) {
return { error }
}

componentDidCatch(error: Error, info: ErrorInfo) {
console.error(error, info)
}

componentDidUpdate(prevProps: Readonly<{ resetKeys?: Array<unknown> }>) {
const { error } = this.state
const { resetKeys } = this.props
if (!error || !resetKeys || !prevProps.resetKeys) return

const hasResetKeyChanged =
resetKeys.length !== prevProps.resetKeys.length ||
resetKeys.some((key, index) => !Object.is(key, prevProps.resetKeys?.[index]))

if (hasResetKeyChanged) {
this.resetErrorBoundary()
}
}

resetErrorBoundary = () => {
this.setState({ error: undefined })
}

render() {
const { error } = this.state
if (error) {
const { fallback } = this.props
if (fallback) {
return fallback(error, this.resetErrorBoundary)
}
return <AsyncBoundaryFallback error={error} onReset={this.resetErrorBoundary} />
}
return this.props.children
}
}

const AsyncBoundary = ({ children, fallback, errorFallback, resetKeys }: AsyncBoundaryProps) => {
const location = useLocation()
const derivedResetKeys = resetKeys ?? [location.pathname, location.search, location.hash]

return (
<ErrorBoundary fallback={errorFallback} resetKeys={derivedResetKeys}>
<Suspense fallback={fallback}>{children}</Suspense>
</ErrorBoundary>
)
}

export default AsyncBoundary
53 changes: 53 additions & 0 deletions src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import styled from '@emotion/styled'

import { theme } from '@/shared/styles/theme'

export const Container = styled.div`
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px;
border-radius: 16px;
background: ${theme.colors.white};
color: ${theme.colors.textPrimary};
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.08);
text-align: center;
`

export const Title = styled.h2`
margin: 0;
font-size: 20px;
font-weight: 700;
`

export const Description = styled.p`
margin: 0;
color: ${theme.colors.textColor3};
font-size: 14px;
`

export const ErrorMessage = styled.p`
margin: 0;
color: ${theme.colors.textColor2};
font-size: 12px;
opacity: 0.8;
`

export const Button = styled.button`
margin-top: 6px;
border: 0;
border-radius: 10px;
padding: 10px 16px;
font-size: 14px;
cursor: pointer;
background: ${theme.colors.primary};
color: ${theme.colors.white};

&:focus-visible {
outline: 2px solid ${theme.colors.primary};
outline-offset: 2px;
}
`
Loading