From b307f53b8e0a9984e6c1dc196846e2e88a254172 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 17:06:00 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20Sentry=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20=EC=84=B1=EB=8A=A5=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=8B=B1=20=EC=97=B0=EB=8F=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 로드 및 라우팅 성능 모니터링을 위해 browserTracingIntegration 설정 추가 --- frontend/src/utils/initSDK.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/utils/initSDK.ts b/frontend/src/utils/initSDK.ts index 4478e1c6c..2f0d113e2 100644 --- a/frontend/src/utils/initSDK.ts +++ b/frontend/src/utils/initSDK.ts @@ -51,6 +51,7 @@ export function initializeSentry() { sendDefaultPii: false, release: import.meta.env.VITE_SENTRY_RELEASE, tracesSampleRate: 0.1, + integrations: [Sentry.browserTracingIntegration()], }); } From 183fcb958306c62dc64c5bc42164f40af6922ebc Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 17:06:52 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동기 런타임, 비동기에러, API에러, 이벤트 핸들러 에러, 타입 에러 테스트 --- .../ErrorTestPage/ErrorTestPage.styles.ts | 173 ++++++++++++++++++ .../src/pages/ErrorTestPage/ErrorTestPage.tsx | 148 +++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 frontend/src/pages/ErrorTestPage/ErrorTestPage.styles.ts create mode 100644 frontend/src/pages/ErrorTestPage/ErrorTestPage.tsx diff --git a/frontend/src/pages/ErrorTestPage/ErrorTestPage.styles.ts b/frontend/src/pages/ErrorTestPage/ErrorTestPage.styles.ts new file mode 100644 index 000000000..8f89d8839 --- /dev/null +++ b/frontend/src/pages/ErrorTestPage/ErrorTestPage.styles.ts @@ -0,0 +1,173 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + max-width: 900px; + margin: 0 auto; + padding: 40px 20px; + min-height: 100vh; +`; + +export const Header = styled.div` + text-align: center; + margin-bottom: 48px; + padding-bottom: 24px; + border-bottom: 2px solid ${({ theme }) => theme.colors.gray[300]}; +`; + +export const Title = styled.h1` + font-size: ${({ theme }) => theme.typography.title.title1.size}; + font-weight: ${({ theme }) => theme.typography.title.title1.weight}; + color: ${({ theme }) => theme.colors.base.black}; + margin-bottom: 12px; +`; + +export const Subtitle = styled.p` + font-size: ${({ theme }) => theme.typography.paragraph.p3.size}; + color: ${({ theme }) => theme.colors.gray[700]}; + line-height: 1.6; +`; + +export const Section = styled.div` + background: white; + border: 1px solid ${({ theme }) => theme.colors.gray[300]}; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + transition: all 0.2s ease; + + &:hover { + border-color: ${({ theme }) => theme.colors.gray[400]}; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + } +`; + +export const SectionTitle = styled.h3` + font-size: ${({ theme }) => theme.typography.title.title5.size}; + font-weight: ${({ theme }) => theme.typography.title.title5.weight}; + color: ${({ theme }) => theme.colors.base.black}; + margin-bottom: 8px; +`; + +export const Description = styled.p` + font-size: ${({ theme }) => theme.typography.paragraph.p5.size}; + color: ${({ theme }) => theme.colors.gray[600]}; + line-height: 1.5; + margin-bottom: 16px; +`; + +interface TestButtonProps { + $variant: 'danger' | 'warning' | 'info'; +} + +export const TestButton = styled.button` + width: 100%; + padding: 16px 24px; + font-size: ${({ theme }) => theme.typography.paragraph.p3.size}; + font-weight: 600; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + + ${({ $variant, theme }) => { + switch ($variant) { + case 'danger': + return ` + background: ${theme.colors.primary[900]}; + color: white; + &:hover { + background: ${theme.colors.primary[800]}; + box-shadow: 0 4px 12px rgba(255, 84, 20, 0.3); + } + `; + case 'warning': + return ` + background: ${theme.colors.secondary[2].main}; + color: ${theme.colors.gray[900]}; + &:hover { + background: ${theme.colors.secondary[5].main}; + box-shadow: 0 4px 12px rgba(255, 160, 77, 0.3); + } + `; + case 'info': + return ` + background: ${theme.colors.secondary[4].main}; + color: white; + &:hover { + background: ${theme.colors.accent[1][900]}; + box-shadow: 0 4px 12px rgba(61, 187, 255, 0.3); + } + `; + } + }} + + &:active { + transform: scale(0.98); + } +`; + +export const InfoBox = styled.div` + background: ${({ theme }) => theme.colors.accent[1][600]}; + border: 1px solid ${({ theme }) => theme.colors.accent[1][700]}; + border-radius: 12px; + padding: 24px; + margin-top: 32px; +`; + +export const InfoTitle = styled.h4` + font-size: ${({ theme }) => theme.typography.paragraph.p1.size}; + font-weight: 600; + color: ${({ theme }) => theme.colors.base.black}; + margin-bottom: 12px; +`; + +export const InfoList = styled.ul` + list-style: none; + padding: 0; + margin: 0; + + li { + font-size: ${({ theme }) => theme.typography.paragraph.p5.size}; + color: ${({ theme }) => theme.colors.gray[800]}; + line-height: 1.6; + margin-bottom: 8px; + padding-left: 20px; + position: relative; + + &:before { + content: '•'; + position: absolute; + left: 8px; + color: ${({ theme }) => theme.colors.primary[900]}; + font-weight: bold; + } + + strong { + color: ${({ theme }) => theme.colors.base.black}; + font-weight: 600; + } + } +`; + +export const BackButton = styled.button` + display: block; + margin: 32px auto 0; + padding: 12px 24px; + font-size: ${({ theme }) => theme.typography.paragraph.p3.size}; + font-weight: 500; + color: ${({ theme }) => theme.colors.gray[700]}; + background: transparent; + border: 1px solid ${({ theme }) => theme.colors.gray[400]}; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${({ theme }) => theme.colors.gray[100]}; + border-color: ${({ theme }) => theme.colors.gray[500]}; + } + + &:active { + transform: scale(0.98); + } +`; diff --git a/frontend/src/pages/ErrorTestPage/ErrorTestPage.tsx b/frontend/src/pages/ErrorTestPage/ErrorTestPage.tsx new file mode 100644 index 000000000..9c1616349 --- /dev/null +++ b/frontend/src/pages/ErrorTestPage/ErrorTestPage.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import * as Styled from './ErrorTestPage.styles'; + +/** + * 에러바운더리 테스트용 페이지 + * 개발 환경에서만 사용 + */ +const ErrorTestPage = () => { + const [shouldThrow, setShouldThrow] = useState(false); + + // 1. 동기 런타임 에러 테스트 + const throwSyncError = () => { + setShouldThrow(true); + }; + + // 2. 비동기 에러 테스트 + const throwAsyncError = async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + throw new Error('비동기 에러 테스트: Promise 내부에서 에러 발생'); + }; + + // 3. API 에러 테스트 (React Query) + const { refetch: triggerQueryError } = useQuery({ + queryKey: ['error-test'], + queryFn: async () => { + throw new Error('React Query 에러 테스트: API 호출 실패'); + }, + enabled: false, + throwOnError: true, + }); + + // 4. 타입 에러 시뮬레이션 + const throwTypeError = () => { + // @ts-ignore + const obj = null; + // @ts-ignore + console.log(obj.property.nested); + }; + + // 5. 이벤트 핸들러 에러 + const throwEventError = () => { + throw new Error('이벤트 핸들러 에러 테스트'); + }; + + if (shouldThrow) { + throw new Error('동기 런타임 에러 테스트: 렌더링 중 에러 발생'); + } + + return ( + + + 🧪 에러바운더리 테스트 페이지 + + 개발 환경에서만 사용 가능합니다. 각 버튼을 클릭하여 에러바운더리 + 동작을 테스트하세요. + + + + + + 🔥 동기 에러 (ErrorBoundary 캐치) + + + 컴포넌트 렌더링 중 발생하는 에러입니다. ErrorBoundary가 캐치합니다. + + + 동기 런타임 에러 발생 + + + + + + ⚡ 이벤트 핸들러 에러 (콘솔 에러) + + + 이벤트 핸들러 내부 에러는 ErrorBoundary가 캐치하지 않습니다. 콘솔에 + 에러가 기록됩니다. + + + 이벤트 핸들러 에러 발생 + + + + + + 🌐 React Query 에러 (ErrorBoundary 캐치) + + + throwOnError: true 설정 시 ErrorBoundary가 캐치합니다. + + triggerQueryError()} + $variant='danger' + > + React Query 에러 발생 + + + + + ⏱️ 비동기 에러 (콘솔 에러) + + Promise 내부 에러는 ErrorBoundary가 캐치하지 않습니다. try-catch나 + .catch()로 처리해야 합니다. + + + 비동기 에러 발생 + + + + + + 💥 타입 에러 (ErrorBoundary 캐치) + + + null/undefined 접근 에러입니다. 렌더링 중 발생하면 캐치됩니다. + + + 타입 에러 발생 + + + + + ℹ️ 테스트 가이드 + +
  • + ErrorBoundary 캐치: 빨간색 버튼 - 에러 폴백 UI가 + 표시됩니다 +
  • +
  • + 콘솔 에러: 노란색 버튼 - 콘솔에 에러가 기록되지만 + 앱은 정상 동작합니다 +
  • +
  • + Sentry 전송: 모든 에러는 Sentry 대시보드에 + 기록됩니다 +
  • +
    +
    + + (window.location.href = '/')}> + ← 메인 페이지로 돌아가기 + +
    + ); +}; + +export default ErrorTestPage; From 7b7c379cd994158b82c6f6c6ef53680505658a52 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 17:07:59 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20React=20Query=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=84=A4=EC=A0=95=EC=97=90=20throwOnError:=20true?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20API=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EB=B0=94=EC=9A=B4=EB=8D=94=EB=A6=AC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eb0b615f2..66cdc1f2a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,9 +22,11 @@ const queryClient = new QueryClient({ queries: { staleTime: 60 * 1000, retry: 1, + throwOnError: true, }, mutations: { retry: 0, + throwOnError: true, }, }, }); From 18f6af22303c99b488e29b4ca0e57a57ee3ab0ac Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 17:08:19 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=94=EC=9A=B4=EB=8D=94=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?Sentry=20=EC=97=B0=EB=8F=99=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sentry.ErrorBoundary로 최상위 컴포넌트 래핑하여 런타임 에러 포착 - GlobalErrorFallback 컴포넌트 연결 - 개발 환경 전용 에러 테스트 라우트(/error-test) 추가 --- frontend/src/App.tsx | 145 ++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 64 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 66cdc1f2a..dcb17c4fe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,7 +15,10 @@ import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage import ClubUnionPage from './pages/ClubUnionPage/ClubUnionPage'; import IntroducePage from './pages/IntroducePage/IntroducePage'; import 'swiper/css'; +import * as Sentry from '@sentry/react'; +import { GlobalErrorFallback } from './components/common/ErrorBoundary/GlobalErrorFallback'; import LegacyClubDetailPage from './pages/ClubDetailPage/LegacyClubDetailPage'; +import ErrorTestPage from './pages/ErrorTestPage/ErrorTestPage'; const queryClient = new QueryClient({ defaultOptions: { @@ -35,70 +38,84 @@ const AdminRoutes = lazy(() => import('@/pages/AdminPage/AdminRoutes')); const App = () => { return ( - - - - - - - - - - - } - /> - {/*기존 웹 & 안드로이드 url (android: v1.1.0)*/} - - - - } - /> - {/*웹 유저에게 신규 상세페이지 보유주기 위한 임시 url*/} - - - - } - /> - {/*새로 빌드해서 배포할 앱 주소 url*/} - - - - } - /> - } /> - } /> - - - - - - } - /> - } - /> - } /> - } /> - - - - + ( + + )} + showDialog={false} + > + + + + + + + + + + + } + /> + {/*기존 웹 & 안드로이드 url (android: v1.1.0)*/} + + + + } + /> + {/*웹 유저에게 신규 상세페이지 보유주기 위한 임시 url*/} + + + + } + /> + {/*새로 빌드해서 배포할 앱 주소 url*/} + + + + } + /> + } /> + } /> + + + + + + } + /> + } + /> + } /> + {/* 개발 환경에서만 사용 가능한 에러 테스트 페이지 */} + {import.meta.env.DEV && ( + } /> + )} + } /> + + + + + ); }; From 41c722298c0cf1135ac6786ea10fa573b3f88393 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 19:20:53 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20mutation=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8A=94=20=EC=97=90=EB=9F=AC=EB=B0=94=EC=9A=B4=EB=8D=94?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=84=ED=8C=8C=20=EC=95=88=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dcb17c4fe..c23b7d859 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,7 +29,7 @@ const queryClient = new QueryClient({ }, mutations: { retry: 0, - throwOnError: true, + throwOnError: false, }, }, }); From 3360d24dfa10f883973eb864dd019fe91519fd6b Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 19:57:23 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=8F=B4=EB=B0=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GlobalErrorFallback.styles.ts | 129 ++++++++++++++++++ .../ErrorBoundary/GlobalErrorFallback.tsx | 74 ++++++++++ 2 files changed, 203 insertions(+) create mode 100644 frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.styles.ts create mode 100644 frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.tsx diff --git a/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.styles.ts b/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.styles.ts new file mode 100644 index 000000000..2845c4618 --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.styles.ts @@ -0,0 +1,129 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); +`; + +export const Content = styled.div` + max-width: 600px; + width: 100%; + text-align: center; + background: white; + border-radius: 16px; + padding: 48px 32px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); +`; + +export const IconWrapper = styled.div` + margin-bottom: 24px; + color: #ff5414; + display: flex; + justify-content: center; + + svg { + width: 64px; + height: 64px; + } +`; + +export const Title = styled.h1` + font-size: 24px; + font-weight: 700; + color: #111111; + margin-bottom: 16px; + line-height: 1.4; +`; + +export const Message = styled.p` + font-size: 16px; + font-weight: 500; + color: #787878; + line-height: 1.6; + margin-bottom: 32px; +`; + +export const ErrorDetails = styled.div` + background: #f5f5f5; + border: 1px solid #ebebeb; + border-radius: 8px; + padding: 16px; + margin-bottom: 32px; + text-align: left; + max-height: 300px; + overflow-y: auto; +`; + +export const ErrorDetailsTitle = styled.div` + font-size: 12px; + font-weight: 600; + color: #989898; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +`; + +export const ErrorMessage = styled.div` + font-size: 14px; + font-weight: 600; + color: #ff5414; + margin-bottom: 12px; + word-break: break-word; +`; + +export const StackTrace = styled.pre` + font-size: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: #4b4b4b; + white-space: pre-wrap; + word-break: break-all; + line-height: 1.5; +`; + +export const ButtonGroup = styled.div` + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +`; + +const BaseButton = styled.button` + padding: 14px 32px; + font-size: 16px; + font-weight: 600; + border-radius: 8px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + min-width: 140px; + font-family: 'Pretendard', sans-serif; + + &:active { + transform: scale(0.98); + } +`; + +export const PrimaryButton = styled(BaseButton)` + background: #ff5414; + color: white; + + &:hover { + background: #ff7543; + box-shadow: 0 4px 12px rgba(255, 84, 20, 0.3); + } +`; + +export const SecondaryButton = styled(BaseButton)` + background: white; + color: #4b4b4b; + border: 1px solid #dcdcdc; + + &:hover { + background: #f5f5f5; + border-color: #c5c5c5; + } +`; diff --git a/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.tsx b/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.tsx new file mode 100644 index 000000000..0fb33b525 --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary/GlobalErrorFallback.tsx @@ -0,0 +1,74 @@ +import * as Styled from './GlobalErrorFallback.styles'; + +interface ErrorFallbackProps { + error: Error; + resetError: () => void; +} + +const WarningIcon = () => ( + + + +); + +const GlobalErrorFallback = ({ error, resetError }: ErrorFallbackProps) => { + const isDev = import.meta.env.DEV; + + const handleReload = () => { + window.location.href = '/'; + }; + + const handleReset = () => { + resetError(); + }; + + return ( + + + + + + + 서비스 이용에 불편을 드려 죄송합니다 + + 예상치 못한 오류가 발생하여 페이지를 표시할 수 없습니다. +
    + 잠시 후 다시 시도해 주세요. +
    + + {isDev && error && ( + + + 개발자 정보 (프로덕션에서는 표시되지 않습니다) + + {error.message} + {error.stack && ( + {error.stack} + )} + + )} + + + + 다시 시도 + + + 홈으로 이동 + + +
    +
    + ); +}; + +export default GlobalErrorFallback; From 0efa1a65c52831b5796ef308083087baa17fa361 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 19:58:36 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=8F=20=EB=A1=9C=EB=94=A9=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20GlobalBoundary=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sentry.ErrorBoundary와 Suspense를 결합한 GlobalBoundary 컴포넌트 생성 - App 최상위에 적용하여 전역 에러 핸들링 및 코드 스플리팅/Suspense 로딩 처리 일원화 - App.tsx 내의 불필요한 개별 Suspense 래퍼 제거 (GlobalBoundary로 위임) --- frontend/src/App.tsx | 13 ++-------- .../common/ErrorBoundary/GlobalBoundary.tsx | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/common/ErrorBoundary/GlobalBoundary.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c23b7d859..600ddb580 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,8 +15,7 @@ import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage import ClubUnionPage from './pages/ClubUnionPage/ClubUnionPage'; import IntroducePage from './pages/IntroducePage/IntroducePage'; import 'swiper/css'; -import * as Sentry from '@sentry/react'; -import { GlobalErrorFallback } from './components/common/ErrorBoundary/GlobalErrorFallback'; +import { GlobalBoundary } from './components/common/ErrorBoundary'; import LegacyClubDetailPage from './pages/ClubDetailPage/LegacyClubDetailPage'; import ErrorTestPage from './pages/ErrorTestPage/ErrorTestPage'; @@ -38,15 +37,7 @@ const AdminRoutes = lazy(() => import('@/pages/AdminPage/AdminRoutes')); const App = () => { return ( - ( - - )} - showDialog={false} - > + diff --git a/frontend/src/components/common/ErrorBoundary/GlobalBoundary.tsx b/frontend/src/components/common/ErrorBoundary/GlobalBoundary.tsx new file mode 100644 index 000000000..6bf44c5b6 --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary/GlobalBoundary.tsx @@ -0,0 +1,25 @@ +import { ReactNode, Suspense } from 'react'; +import * as Sentry from '@sentry/react'; +import Spinner from '../Spinner/Spinner'; +import GlobalErrorFallback from './GlobalErrorFallback'; + +interface GlobalBoundaryProps { + children: ReactNode; +} + +const GlobalBoundary = ({ children }: GlobalBoundaryProps) => { + return ( + ( + + )} + > + }>{children} + + ); +}; + +export default GlobalBoundary; From 6a5d8a2f0dc4dce43c273cef6c62263f8bae326e Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 19:59:04 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20ErrorBoundary=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EB=82=B4=20=ED=8C=8C=EC=9D=BC=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/ErrorBoundary/index.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 frontend/src/components/common/ErrorBoundary/index.ts diff --git a/frontend/src/components/common/ErrorBoundary/index.ts b/frontend/src/components/common/ErrorBoundary/index.ts new file mode 100644 index 000000000..f7da0a3ed --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary/index.ts @@ -0,0 +1,2 @@ +export * from './GlobalErrorFallback'; +export { default as GlobalBoundary } from './GlobalBoundary'; From 0a7e873a25492383beb92212bc1d0fb000bb7b3c Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 13 Feb 2026 21:27:58 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20useQuery=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20=EB=A1=9C=EB=94=A9=20=EC=B2=98=EB=A6=AC=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?Suspense=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx: 각 페이지 라우트의 Suspense(fallback=null) 래퍼 제거 --- frontend/src/App.tsx | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 600ddb580..b053afcdf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from 'react'; +import { lazy } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider } from 'styled-components'; @@ -45,40 +45,15 @@ const App = () => { - - - - } - /> + } /> {/*기존 웹 & 안드로이드 url (android: v1.1.0)*/} - - - - } - /> + } /> {/*웹 유저에게 신규 상세페이지 보유주기 위한 임시 url*/} - - - - } - /> + } /> {/*새로 빌드해서 배포할 앱 주소 url*/} - - - } + element={} /> } /> } /> @@ -106,7 +81,7 @@ const App = () => { - + ); }; From 26e885ff4f01402d6e69e6d482f1544a2c3c9a72 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 14 Feb 2026 16:42:08 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=97=90=EB=9F=AC=EB=A5=BC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에러 발생 시 GlobalErrorFallback 대신 콘텐츠 영역에 에러 메시지와 재시도 버튼 표시 - 헤더/네비게이션을 유지하여 사용자가 쉽게 복구 가능 --- .../src/pages/MainPage/MainPage.styles.ts | 27 +++++++++++++++++++ frontend/src/pages/MainPage/MainPage.tsx | 15 ++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/MainPage/MainPage.styles.ts b/frontend/src/pages/MainPage/MainPage.styles.ts index b8a1a9c1f..641cdddfd 100644 --- a/frontend/src/pages/MainPage/MainPage.styles.ts +++ b/frontend/src/pages/MainPage/MainPage.styles.ts @@ -119,3 +119,30 @@ export const EmptyResult = styled.div` font-size: 0.95rem; } `; + +export const RetryButton = styled.button` + margin-top: 24px; + padding: 12px 32px; + font-size: 16px; + font-weight: 600; + color: white; + background: ${({ theme }) => theme.colors.primary[900]}; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${({ theme }) => theme.colors.primary[800]}; + box-shadow: 0 4px 12px rgba(255, 84, 20, 0.3); + } + + &:active { + transform: scale(0.98); + } + + ${media.mobile} { + padding: 10px 24px; + font-size: 14px; + } +`; diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index 7b88f6f7a..f09224ce6 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -28,7 +28,7 @@ const MainPage = () => { const [active, setActive] = useState<(typeof tabs)[number]>('부경대학교 중앙동아리'); - const { data, error, isLoading } = useGetCardList({ + const { data, error, isLoading, refetch } = useGetCardList({ keyword, recruitmentStatus, category: searchCategory, @@ -46,10 +46,6 @@ const MainPage = () => { return clubs.map((club: Club) => ); }, [clubs, hasData]); - if (error) { - return
    에러가 발생했습니다.
    ; - } - return ( <> @@ -75,10 +71,17 @@ const MainPage = () => { {`전체 ${isLoading ? 0 : totalCount}개의 동아리`} - {isLoading ? ( + ) : error ? ( + + 동아리 목록을 불러오는 중 문제가 발생했습니다. +
    + refetch()}> + 다시 시도 + +
    ) : isEmpty ? ( 앗, 조건에 맞는 동아리가 없어요. From 4ddd46aad6e0d1c32419b733ae7fd982d2649c34 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 14 Feb 2026 16:42:42 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20Query=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A5=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20throwOnError=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 각 페이지에서 에러를 직접 처리하여 레이아웃 유지 및 복구 가능성 향상 --- frontend/src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b053afcdf..bf29ada12 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,11 +24,9 @@ const queryClient = new QueryClient({ queries: { staleTime: 60 * 1000, retry: 1, - throwOnError: true, }, mutations: { retry: 0, - throwOnError: false, }, }, });