diff --git a/public/locale/en/maintenance.json b/public/locale/en/maintenance.json
new file mode 100644
index 00000000..7ff370eb
--- /dev/null
+++ b/public/locale/en/maintenance.json
@@ -0,0 +1,9 @@
+{
+ "title": "Service Maintenance Notice",
+ "description": "System maintenance is in progress\nto improve our service.",
+ "maintenance_time": "Maintenance Time",
+ "maintenance_impact": "Service Impact",
+ "impact_details": "All services, including games, rankings, and profiles, will be unavailable.",
+ "notice": "Any updates will be announced",
+ "official_sns": "on our official SNS channels."
+}
diff --git a/public/locale/ko/maintenance.json b/public/locale/ko/maintenance.json
new file mode 100644
index 00000000..8e213ffb
--- /dev/null
+++ b/public/locale/ko/maintenance.json
@@ -0,0 +1,9 @@
+{
+ "title": "서비스 점검 안내",
+ "description": "더 나은 서비스 제공을 위해\n시스템 점검을 진행하고 있습니다.",
+ "maintenance_time": "점검 일시",
+ "maintenance_impact": "점검 영향",
+ "impact_details": "게임, 랭킹, 프로필 페이지 등 서비스 전체 이용 불가",
+ "notice": "변동 사항이 있을 경우",
+ "official_sns": "공식 SNS를 통해 안내해 드리겠습니다"
+}
diff --git a/src/App.tsx b/src/App.tsx
index ae2b9662..b3683b9c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,9 +9,12 @@ import Loading from '@components/Loading/Loading';
import Modal from '@components/Modal/Modal';
import Toast from '@components/Toast/Toast';
import GameLayout from '@pages/GameLayout';
+import ServiceMaintenancePage from '@pages/maintenance/ServiceMaintenancePage';
+import { MaintenanceGuard } from '@pages/MaintenanceGuard';
import { PrivateRoute } from '@pages/PrivateRoute';
import { resetUserState, userState } from '@utils/atoms/member.atom';
+import { isInMaintenance } from '@constants/maintenance.constant';
import PATH from '@constants/path.constant';
import useLocalStorage from '@hooks/useLocalStorage';
@@ -66,72 +69,83 @@ const App = () => {
}
};
- if (window.location.pathname.includes('biz')) return;
+ if (window.location.pathname.includes('biz') || isInMaintenance()) return;
updateProfile();
}, []);
return (
<>
- }>
-
- {/*Main*/}
- } />
-
- {/*OAuth*/}
- } />
- }
- />
-
- }>
- {/*Game*/}
- } />
- } />
-
- {/*Ranking*/}
- } />
+
+ }>
+
+ {/*Main*/}
+ } />
+
+ {/*OAuth*/}
+ } />
+ }
+ />
+
+ {/*Maintenance*/}
+ }
+ />
+
+ }>
+ {/*Game*/}
+ } />
+ } />
+
+ {/*Ranking*/}
+ }
+ />
+
+ }>
+ {/* User */}
+ } />
+
+
+
+
+ {/*Game*/}
+ }
+ />
+
}>
- {/* User */}
- } />
+ {/*Setting*/}
+ } />
+
+ {/* Withdraw */}
+ } />
+
+ {/* Notices */}
+ } />
-
-
- {/*Game*/}
+ {/* Policy */}
+ } />
+
+ {/*Error*/}
}
+ path={PATH.NOT_FOUND_ERROR}
+ element={
+
+ }
/>
-
-
- }>
- {/*Setting*/}
- } />
-
- {/* Withdraw */}
- } />
-
- {/* Notices */}
- } />
-
-
- {/* Policy */}
- } />
-
- {/*Error*/}
-
- }
- />
-
-
-
-
+
+
+
+
+
>
);
diff --git a/src/constants/maintenance.constant.ts b/src/constants/maintenance.constant.ts
new file mode 100644
index 00000000..06ebe98d
--- /dev/null
+++ b/src/constants/maintenance.constant.ts
@@ -0,0 +1,6 @@
+export const MAINTENANCE_START = new Date('2026-02-09T20:30:00+09:00');
+export const MAINTENANCE_END = new Date('2026-02-09T23:59:00+09:00');
+
+export const isInMaintenance = (now = new Date()) => {
+ return now >= MAINTENANCE_START && now <= MAINTENANCE_END;
+};
diff --git a/src/constants/path.constant.ts b/src/constants/path.constant.ts
index 465e9141..2dc3fb48 100644
--- a/src/constants/path.constant.ts
+++ b/src/constants/path.constant.ts
@@ -11,6 +11,7 @@ const PATH = {
POLICY: '/policy',
WITHDRAW: '/user/setting/withdraw',
NOTICE: '/user/setting/notices',
+ MAINTENANCE: '/maintenance',
SNACK_GAME: '/snack-game',
SNACK_GAME_BIZ: '/snack-game/biz',
@@ -27,6 +28,8 @@ const PATH = {
NOTICE_RSS: 'https://notice.snackga.me/index.xml',
+ INSTAGRAM: 'https://www.instagram.com/snackga._.me/',
+
GOOGLE: '/oauth2/authorization/google',
KAKAO: '/oauth2/authorization/kakao',
diff --git a/src/pages/MaintenanceGuard.tsx b/src/pages/MaintenanceGuard.tsx
new file mode 100644
index 00000000..8d9029ce
--- /dev/null
+++ b/src/pages/MaintenanceGuard.tsx
@@ -0,0 +1,19 @@
+import { ReactNode } from 'react';
+import { useLocation, Navigate } from 'react-router-dom';
+
+import { isInMaintenance } from '@constants/maintenance.constant';
+import PATH from '@constants/path.constant';
+
+const isWhitelisted = (pathname: string): boolean => {
+ return pathname === PATH.MAIN || pathname === PATH.MAINTENANCE;
+};
+
+export const MaintenanceGuard = ({ children }: { children: ReactNode }) => {
+ const location = useLocation();
+
+ if (isInMaintenance() && !isWhitelisted(location.pathname)) {
+ return ;
+ }
+
+ return <>{children}>;
+};
diff --git a/src/pages/maintenance/ServiceMaintenancePage.tsx b/src/pages/maintenance/ServiceMaintenancePage.tsx
new file mode 100644
index 00000000..63e9c016
--- /dev/null
+++ b/src/pages/maintenance/ServiceMaintenancePage.tsx
@@ -0,0 +1,83 @@
+import { Helmet } from 'react-helmet-async';
+import { useTranslation } from 'react-i18next';
+import { Navigate } from 'react-router-dom';
+
+import MainAvifImage from '@assets/images/main.avif';
+import MainWebpImage from '@assets/images/main.webp';
+import ImageWithFallback from '@components/ImageWithFallback/ImageWithFallback';
+import Spacing from '@components/Spacing/Spacing';
+
+import {
+ MAINTENANCE_START,
+ MAINTENANCE_END,
+ isInMaintenance,
+} from '@constants/maintenance.constant';
+import PATH from '@constants/path.constant';
+
+const ServiceMaintenancePage = () => {
+ const { t } = useTranslation('maintenance');
+
+ if (!isInMaintenance()) {
+ return ;
+ }
+
+ return (
+ <>
+
+ Snack Game || 점검 중
+
+
+
+
+
+
+
+ {t('title')}
+
+
+ {t('description')}
+
+
+
+
+
+
+
+ -
+ {t('maintenance_time')}:{' '}
+ {`${MAINTENANCE_START.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} ~ ${MAINTENANCE_END.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} (KST)`}
+
+ -
+
+ {t('maintenance_impact')}:
+ {' '}
+ {t('impact_details')}
+
+ -
+ * {t('notice')}{' '}
+
+ {t('official_sns')}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ServiceMaintenancePage;
diff --git a/src/utils/i18n/i18n.ts b/src/utils/i18n/i18n.ts
index 2eca4107..bc7e24a3 100644
--- a/src/utils/i18n/i18n.ts
+++ b/src/utils/i18n/i18n.ts
@@ -9,7 +9,15 @@ i18next
.use(initReactI18next)
.use(HttpBackend)
.init({
- ns: ['game', 'landing', 'ranking', 'setting', 'translation', 'user'],
+ ns: [
+ 'game',
+ 'landing',
+ 'ranking',
+ 'setting',
+ 'translation',
+ 'user',
+ 'maintenance',
+ ],
fallbackLng: 'ko',
backend: {
loadPath: '/locale/{{lng}}/{{ns}}.json',
@@ -29,7 +37,7 @@ i18next
'subdomain',
],
caches: ['localStorage'],
- convertDetectedLanguage: (lng) => lng.split('-')[0]
+ convertDetectedLanguage: (lng) => lng.split('-')[0],
},
});