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], }, });