Skip to content
Merged
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
9 changes: 9 additions & 0 deletions public/locale/en/maintenance.json
Original file line number Diff line number Diff line change
@@ -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."
}
9 changes: 9 additions & 0 deletions public/locale/ko/maintenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "서비스 점검 안내",
"description": "더 나은 서비스 제공을 위해\n시스템 점검을 진행하고 있습니다.",
"maintenance_time": "점검 일시",
"maintenance_impact": "점검 영향",
"impact_details": "게임, 랭킹, 프로필 페이지 등 서비스 전체 이용 불가",
"notice": "변동 사항이 있을 경우",
"official_sns": "공식 SNS를 통해 안내해 드리겠습니다"
}
122 changes: 68 additions & 54 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -66,72 +69,83 @@ const App = () => {
}
};

if (window.location.pathname.includes('biz')) return;
if (window.location.pathname.includes('biz') || isInMaintenance()) return;
updateProfile();
}, []);

return (
<>
<ErrorBoundary fallback={ErrorPage}>
<Suspense fallback={<Loading type={'page'} />}>
<Routes>
{/*Main*/}
<Route path={PATH.MAIN} element={<MainPage />} />

{/*OAuth*/}
<Route path={PATH.OAUTH_SUCCESS} element={<OAuthPage />} />
<Route
path={PATH.OAUTH_FAILURE}
element={<ErrorPage message={'소셜 로그인에 실패했습니다.'} />}
/>

<Route element={<GameLayout />}>
{/*Game*/}
<Route path={PATH.SNACK_GAME} element={<SnackGamePage />} />
<Route path={PATH.APPLE_GAME} element={<AppleGamePage />} />

{/*Ranking*/}
<Route path={PATH.SNACK_GAME_RANKING} element={<RankingPage />} />
<MaintenanceGuard>
<Suspense fallback={<Loading type={'page'} />}>
<Routes>
{/*Main*/}
<Route path={PATH.MAIN} element={<MainPage />} />

{/*OAuth*/}
<Route path={PATH.OAUTH_SUCCESS} element={<OAuthPage />} />
<Route
path={PATH.OAUTH_FAILURE}
element={<ErrorPage message={'소셜 로그인에 실패했습니다.'} />}
/>

{/*Maintenance*/}
<Route
path={PATH.MAINTENANCE}
element={<ServiceMaintenancePage />}
/>

<Route element={<GameLayout />}>
{/*Game*/}
<Route path={PATH.SNACK_GAME} element={<SnackGamePage />} />
<Route path={PATH.APPLE_GAME} element={<AppleGamePage />} />

{/*Ranking*/}
<Route
path={PATH.SNACK_GAME_RANKING}
element={<RankingPage />}
/>

<Route element={<PrivateRoute />}>
{/* User */}
<Route path={PATH.USER} element={<UserPage />} />
</Route>
</Route>

<Route>
{/*Game*/}
<Route
path={PATH.SNACK_GAME_BIZ}
element={<SnackGameBizPage />}
/>
</Route>

<Route element={<PrivateRoute />}>
{/* User */}
<Route path={PATH.USER} element={<UserPage />} />
{/*Setting*/}
<Route path={PATH.SETTING} element={<SettingPage />} />

{/* Withdraw */}
<Route path={PATH.WITHDRAW} element={<WithdrawPage />} />

{/* Notices */}
<Route path={PATH.NOTICE} element={<NoticePage />} />
</Route>
</Route>

<Route>
{/*Game*/}
{/* Policy */}
<Route path={PATH.POLICY} element={<PolicyPage />} />

{/*Error*/}
<Route
path={PATH.SNACK_GAME_BIZ}
element={<SnackGameBizPage />}
path={PATH.NOT_FOUND_ERROR}
element={
<ErrorPage error={new Error('존재하지 않는 페이지입니다!')} />
}
/>
</Route>

<Route element={<PrivateRoute />}>
{/*Setting*/}
<Route path={PATH.SETTING} element={<SettingPage />} />

{/* Withdraw */}
<Route path={PATH.WITHDRAW} element={<WithdrawPage />} />

{/* Notices */}
<Route path={PATH.NOTICE} element={<NoticePage />} />
</Route>

{/* Policy */}
<Route path={PATH.POLICY} element={<PolicyPage />} />

{/*Error*/}
<Route
path={PATH.NOT_FOUND_ERROR}
element={
<ErrorPage error={new Error('존재하지 않는 페이지입니다!')} />
}
/>
</Routes>
<Modal />
</Suspense>
<Toast />
</Routes>
<Modal />
</Suspense>
<Toast />
</MaintenanceGuard>
</ErrorBoundary>
</>
);
Expand Down
6 changes: 6 additions & 0 deletions src/constants/maintenance.constant.ts
Original file line number Diff line number Diff line change
@@ -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;
};
3 changes: 3 additions & 0 deletions src/constants/path.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',

Expand Down
19 changes: 19 additions & 0 deletions src/pages/MaintenanceGuard.tsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to={PATH.MAINTENANCE} replace />;
}

return <>{children}</>;
};
83 changes: 83 additions & 0 deletions src/pages/maintenance/ServiceMaintenancePage.tsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to={PATH.MAIN} replace />;
}

return (
<>
<Helmet>
<title>Snack Game || 점검 중</title>
</Helmet>
<div className="flex min-h-screen flex-col items-center justify-center bg-primary-light p-6">
<div className="flex flex-col items-center">
<ImageWithFallback
sources={[
{ srcSet: MainAvifImage, type: 'avif' },
{ srcSet: MainWebpImage, type: 'webp' },
]}
src={MainWebpImage}
alt="Snack Game Character"
className="h-48 w-48"
/>

<div className="text-center">
<h1 className="mb-4 text-2xl font-bold text-primary-deep-dark">
{t('title')}
</h1>
<p className="whitespace-pre-line text-base text-gray-600">
{t('description')}
</p>
</div>

<Spacing size={2} />

<div className="w-full max-w-md rounded-lg border bg-white p-6">
<ul className="space-y-3 text-left">
<li className="text-sm text-gray-700">
<span className="font-semibold">{t('maintenance_time')}:</span>{' '}
{`${MAINTENANCE_START.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} ~ ${MAINTENANCE_END.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} (KST)`}
</li>
<li className="text-sm text-gray-700">
<span className="font-semibold">
{t('maintenance_impact')}:
</span>{' '}
{t('impact_details')}
</li>
<li className="text-sm text-gray-500">
* {t('notice')}{' '}
<a
href={PATH.INSTAGRAM}
target="_blank"
rel="noopener noreferrer"
className=" text-primary underline"
>
{t('official_sns')}
</a>
</li>
</ul>
</div>
</div>
</div>
</>
);
};

export default ServiceMaintenancePage;
12 changes: 10 additions & 2 deletions src/utils/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';

i18next

Check warning on line 7 in src/utils/i18n/i18n.ts

View workflow job for this annotation

GitHub Actions / Build Test

Caution: `i18next` also has a named export `use`. Check if you meant to write `import {use} from 'i18next'` instead
.use(LanguageDetector)
.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',
Expand All @@ -29,14 +37,14 @@
'subdomain',
],
caches: ['localStorage'],
convertDetectedLanguage: (lng) => lng.split('-')[0]
convertDetectedLanguage: (lng) => lng.split('-')[0],
},
});

i18next.use({

Check warning on line 44 in src/utils/i18n/i18n.ts

View workflow job for this annotation

GitHub Actions / Build Test

Caution: `i18next` also has a named export `use`. Check if you meant to write `import {use} from 'i18next'` instead
type: 'postProcessor',
name: 'seasonHandler',
process: (value: string, _: unknown, options: Record<string, any>) => {

Check warning on line 47 in src/utils/i18n/i18n.ts

View workflow job for this annotation

GitHub Actions / Build Test

Unexpected any. Specify a different type
if (options.season === 0) {
return options.lng === 'ko' ? '베타 시즌' : 'Beta Season';
}
Expand Down
Loading