Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a4d004a
fix: 헤더 padding 변경
seongwon030 Feb 10, 2026
927e5f0
fix: banner margin-top수정
seongwon030 Feb 10, 2026
cf95d8b
fix: 카테고리버튼 스타일 수정
seongwon030 Feb 10, 2026
94d1a63
fix: 섹션바 margin 변경
seongwon030 Feb 10, 2026
97a2917
fix: 섹션 왼쪽 "부경대학교 중앙동아리" 폰트 수정
seongwon030 Feb 10, 2026
bf57c75
fix: 검색창 height 변경
seongwon030 Feb 10, 2026
f59d130
fix: 페이지 상단 pull-to-refresh 동작 비활성화
seongwon030 Feb 10, 2026
ee2e4a1
fix: 카테고리 버튼 모바일 font weight 진하게 설정
seongwon030 Feb 10, 2026
05ea395
feat: 상세페이지 소개내용 line-height 설정
seongwon030 Feb 10, 2026
cff2f09
refactor: linkifyText 경로 작은따옴표로 변경
seongwon030 Feb 10, 2026
6af1986
Revert "refactor: linkifyText 경로 작은따옴표로 변경"
seongwon030 Feb 10, 2026
eb96731
fix: 뒤로가기 동작은 가능하도록 변경
seongwon030 Feb 10, 2026
b307f53
feat: Sentry 브라우저 성능 트레이싱 연동 추가
seongwon030 Feb 13, 2026
183fcb9
feat: 에러 테스트 페이지 추가
seongwon030 Feb 13, 2026
7b7c379
feat: React Query 전역 설정에 throwOnError: true 추가하여 API 에러를 바운더리로 전파
seongwon030 Feb 13, 2026
18f6af2
feat: 전역 에러 바운더리 및 Sentry 연동 적용
seongwon030 Feb 13, 2026
41c7222
fix: mutation에서는 에러바운더리 전파 안 함
seongwon030 Feb 13, 2026
3360d24
feat: 전역에러 폴백 페이지 추가
seongwon030 Feb 13, 2026
0efa1a6
refactor: 전역 에러 및 로딩 처리를 위한 GlobalBoundary 구현
seongwon030 Feb 13, 2026
6a5d8a2
feat: ErrorBoundary폴더 내 파일 export
seongwon030 Feb 13, 2026
0a7e873
refactor: useQuery 기반 로딩 처리로 전환 및 불필요한 Suspense 제거
seongwon030 Feb 13, 2026
26e885f
feat: 메인페이지 에러를 페이지 내부에서 처리하도록 개선
seongwon030 Feb 14, 2026
4ddd46a
refactor: Query 에러를 페이지별로 처리하도록 throwOnError 제거
seongwon030 Feb 14, 2026
bc6f48e
Merge pull request #1182 from Moadong/feature/#1142-main-ui-fix-MOA-603
seongwon030 Feb 14, 2026
0f05285
Merge pull request #1196 from Moadong/feature/#1193-error-Boundary-se…
seongwon030 Feb 14, 2026
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
113 changes: 48 additions & 65 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,7 +15,9 @@ import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage
import ClubUnionPage from './pages/ClubUnionPage/ClubUnionPage';
import IntroducePage from './pages/IntroducePage/IntroducePage';
import 'swiper/css';
import { GlobalBoundary } from './components/common/ErrorBoundary';
import LegacyClubDetailPage from './pages/ClubDetailPage/LegacyClubDetailPage';
import ErrorTestPage from './pages/ErrorTestPage/ErrorTestPage';

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -33,70 +35,51 @@ const AdminRoutes = lazy(() => import('@/pages/AdminPage/AdminRoutes'));

const App = () => {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<BrowserRouter>
<GlobalStyles />
<ScrollToTop />
<ScrollToTopButton />
<Routes>
<Route
path='/'
element={
<Suspense fallback={null}>
<MainPage />
</Suspense>
}
/>
{/*기존 웹 & 안드로이드 url (android: v1.1.0)*/}
<Route
path='/club/:clubId'
element={
<Suspense fallback={null}>
<LegacyClubDetailPage />
</Suspense>
}
/>
{/*웹 유저에게 신규 상세페이지 보유주기 위한 임시 url*/}
<Route
path='/clubDetail/:clubId'
element={
<Suspense fallback={null}>
<ClubDetailPage />
</Suspense>
}
/>
{/*새로 빌드해서 배포할 앱 주소 url*/}
<Route
path='/webview/club/:clubId'
element={
<Suspense fallback={null}>
<ClubDetailPage />
</Suspense>
}
/>
<Route path='/introduce' element={<IntroducePage />} />
<Route path='/admin/login' element={<LoginTab />} />
<Route
path='/admin/*'
element={
<AdminClubProvider>
<PrivateRoute>
<AdminRoutes />
</PrivateRoute>
</AdminClubProvider>
}
/>
<Route
path='/application/:clubId/:applicationFormId'
element={<ApplicationFormPage />}
/>
<Route path='/club-union' element={<ClubUnionPage />} />
<Route path='*' element={<Navigate to='/' replace />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
<GlobalBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<BrowserRouter>
<GlobalStyles />
<ScrollToTop />
<ScrollToTopButton />
<Routes>
<Route path='/' element={<MainPage />} />
{/*기존 웹 & 안드로이드 url (android: v1.1.0)*/}
<Route path='/club/:clubId' element={<LegacyClubDetailPage />} />
{/*웹 유저에게 신규 상세페이지 보유주기 위한 임시 url*/}
<Route path='/clubDetail/:clubId' element={<ClubDetailPage />} />
{/*새로 빌드해서 배포할 앱 주소 url*/}
<Route
path='/webview/club/:clubId'
element={<ClubDetailPage />}
/>
<Route path='/introduce' element={<IntroducePage />} />
<Route path='/admin/login' element={<LoginTab />} />
<Route
path='/admin/*'
element={
<AdminClubProvider>
<PrivateRoute>
<AdminRoutes />
</PrivateRoute>
</AdminClubProvider>
}
/>
<Route
path='/application/:clubId/:applicationFormId'
element={<ApplicationFormPage />}
/>
<Route path='/club-union' element={<ClubUnionPage />} />
{/* 개발 환경에서만 사용 가능한 에러 테스트 페이지 */}
{import.meta.env.DEV && (
<Route path='/error-test' element={<ErrorTestPage />} />
)}
<Route path='*' element={<Navigate to='/' replace />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
</GlobalBoundary>
);
};

Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/common/ErrorBoundary/GlobalBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Sentry.ErrorBoundary
fallback={(errorData) => (
<GlobalErrorFallback
error={errorData.error as Error}
resetError={errorData.resetError}
/>
)}
>
<Suspense fallback={<Spinner />}>{children}</Suspense>
</Sentry.ErrorBoundary>
);
};

export default GlobalBoundary;
Original file line number Diff line number Diff line change
@@ -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;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as Styled from './GlobalErrorFallback.styles';

interface ErrorFallbackProps {
error: Error;
resetError: () => void;
}

const WarningIcon = () => (
<svg
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
stroke='currentColor'
>
<path
d='M12 9V14M12 17.5V18M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22Z'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);

const GlobalErrorFallback = ({ error, resetError }: ErrorFallbackProps) => {
const isDev = import.meta.env.DEV;

const handleReload = () => {
window.location.href = '/';
};

const handleReset = () => {
resetError();
};

return (
<Styled.Container>
<Styled.Content>
<Styled.IconWrapper>
<WarningIcon />
</Styled.IconWrapper>

<Styled.Title>서비스 이용에 불편을 드려 죄송합니다</Styled.Title>
<Styled.Message>
예상치 못한 오류가 발생하여 페이지를 표시할 수 없습니다.
<br />
잠시 후 다시 시도해 주세요.
</Styled.Message>

{isDev && error && (
<Styled.ErrorDetails>
<Styled.ErrorDetailsTitle>
개발자 정보 (프로덕션에서는 표시되지 않습니다)
</Styled.ErrorDetailsTitle>
<Styled.ErrorMessage>{error.message}</Styled.ErrorMessage>
{error.stack && (
<Styled.StackTrace>{error.stack}</Styled.StackTrace>
)}
</Styled.ErrorDetails>
)}

<Styled.ButtonGroup>
<Styled.PrimaryButton onClick={handleReset}>
다시 시도
</Styled.PrimaryButton>
<Styled.SecondaryButton onClick={handleReload}>
홈으로 이동
</Styled.SecondaryButton>
</Styled.ButtonGroup>
</Styled.Content>
</Styled.Container>
);
};

export default GlobalErrorFallback;
2 changes: 2 additions & 0 deletions frontend/src/components/common/ErrorBoundary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './GlobalErrorFallback';
export { default as GlobalBoundary } from './GlobalBoundary';
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

export *는 default export를 재-export하지 않습니다.

GlobalErrorFallback.tsxexport default만 사용하고 있어서, Line 1의 export * from './GlobalErrorFallback'는 실제로 아무것도 내보내지 않습니다. 현재 App.tsx에서 GlobalBoundary만 import하므로 동작에는 문제 없지만, 이 barrel에서 GlobalErrorFallback을 import하려는 경우 실패합니다.

♻️ 수정 제안
-export * from './GlobalErrorFallback';
+export { default as GlobalErrorFallback } from './GlobalErrorFallback';
 export { default as GlobalBoundary } from './GlobalBoundary';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export * from './GlobalErrorFallback';
export { default as GlobalBoundary } from './GlobalBoundary';
export { default as GlobalErrorFallback } from './GlobalErrorFallback';
export { default as GlobalBoundary } from './GlobalBoundary';
🤖 Prompt for AI Agents
In `@frontend/src/components/common/ErrorBoundary/index.ts` around lines 1 - 2,
The barrel currently uses "export * from './GlobalErrorFallback'" which does not
re-export the default export from GlobalErrorFallback.tsx, so add an explicit
re-export of the default (e.g., export { default as GlobalErrorFallback } from
'./GlobalErrorFallback';) alongside the existing export for GlobalBoundary so
consumers can import GlobalErrorFallback from this index; update the export line
that references GlobalErrorFallback and keep the export of GlobalBoundary
unchanged.

7 changes: 5 additions & 2 deletions frontend/src/components/common/Header/Header.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const Header = styled.header<{ isScrolled: boolean }>`
left: 0;
right: 0;
width: 100%;
height: 62px;
padding: 10px 20px;
padding: 18px 0;
background-color: white;
z-index: ${Z_INDEX.header};

Expand All @@ -21,6 +20,10 @@ export const Header = styled.header<{ isScrolled: boolean }>`
height: 56px;
padding: 10px 20px;
}

${media.mobile} {
padding: 8px 20px;
}
`;

export const Container = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const SearchBoxContainer = styled.form<{ $isFocused: boolean }>`
align-items: center;
justify-content: center;
width: 345px;
height: 36px;
height: 40px;
padding: 3px 20px;
border: 1px solid transparent;
border-radius: 41px;
Expand Down
Loading
Loading