Skip to content
Closed
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
6 changes: 6 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@
"아래로 스크롤": "Scroll down",
"이미 많은 사람들이 디베이트 타이머로\n더 나은 토론환경을 만들고 있어요.": "Many people already use\nDebate Timer to make debates better.",
"비회원으로 시작하기": "Continue as guest",
"서비스 점검 중": "Service Under Maintenance",
"죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.": "Sorry, please try again later... 😭 In the meantime, you can still use the timer in offline mode. If you need it, select “{{action}}.”",
"오프라인으로 시작하기": "Start Offline",
"오프라인으로 이어하기": "Continue Offline",
"토론이 끝났습니다. 종료하시겠습니까?": "The debate has ended. Would you like to finish?",
"예": "Yes",
"버그 및 불편사항 제보": "Report bugs or issues",
"디베이트 타이머 사용 중 불편함을 느끼셨나요?": "Did you run into issues while using Debate Timer?",
"접수하기": "Report",
Expand Down
6 changes: 6 additions & 0 deletions public/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@
"아래로 스크롤": "아래로 스크롤",
"이미 많은 사람들이 디베이트 타이머로\n더 나은 토론환경을 만들고 있어요.": "이미 많은 사람들이 디베이트 타이머로\n더 나은 토론환경을 만들고 있어요.",
"비회원으로 시작하기": "비회원으로 시작하기",
"서비스 점검 중": "서비스 점검 중",
"죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.": "죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.",
"오프라인으로 시작하기": "오프라인으로 시작하기",
"오프라인으로 이어하기": "오프라인으로 이어하기",
"토론이 끝났습니다. 종료하시겠습니까?": "토론이 끝났습니다. 종료하시겠습니까?",
"예": "예",
"버그 및 불편사항 제보": "버그 및 불편사항 제보",
"디베이트 타이머 사용 중 불편함을 느끼셨나요?": "디베이트 타이머 사용 중 불편함을 느끼셨나요?",
"접수하기": "접수하기",
Expand Down
39 changes: 39 additions & 0 deletions src/components/FillButton/FillButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { PropsWithChildren } from 'react';

type FillButtonVariant = 'primary' | 'secondary';
type FillButtonSize = 'sm' | 'md' | 'lg';

interface FillButtonProps extends PropsWithChildren {
onClick: () => void;
variant?: FillButtonVariant;
size?: FillButtonSize;
}

const VARIANT_CLASSNAMES: Record<FillButtonVariant, string> = {
primary:
'bg-brand hover:bg-semantic-table hover:text-default-white focus-visible:outline-brand-main focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4',
secondary: 'bg-neutral-200 hover:bg-brand',
};

const SIZE_CLASSNAMES: Record<FillButtonSize, string> = {
sm: 'px-5',
md: 'px-9',
lg: 'px-20',
};

export default function FillButton({
children,
onClick,
variant = 'primary',
size = 'sm',
}: FillButtonProps) {
return (
<button
type="button"
onClick={onClick}
className={`rounded-full border border-neutral-300 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 ${VARIANT_CLASSNAMES[variant]} ${SIZE_CLASSNAMES[size]}`}
>
{children}
</button>
);
}
8 changes: 6 additions & 2 deletions src/layout/components/header/StickyTriSectionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DEFAULT_LANG,
isSupportedLang,
} from '../../../util/languageRouting';
import { isMaintenanceModeEnabled } from '../../../util/maintenanceMode';

type HeaderIcons = 'home' | 'auth';

Expand Down Expand Up @@ -62,7 +63,10 @@ StickyTriSectionHeader.Right = function Right(props: PropsWithChildren) {
const { mutate: logoutMutate } = useLogout(() => navigate(homePath));
const { openModal, closeModal, ModalWrapper } = useModal({});
const { isFullscreen, setFullscreen } = useFullscreen();
const defaultIcons: HeaderIcons[] = ['home', 'auth'];
const isMaintenanceMode = isMaintenanceModeEnabled();
const defaultIcons: HeaderIcons[] = isMaintenanceMode
? ['home']
: ['home', 'auth'];

const handleLoginStart = (keepData: boolean) => {
sessionStorage.setItem('keepGuestTable', String(keepData));
Expand Down Expand Up @@ -101,7 +105,7 @@ StickyTriSectionHeader.Right = function Right(props: PropsWithChildren) {
setFullscreen(false);
}

if (isGuestFlow()) {
if (isGuestFlow() && !isMaintenanceMode) {
deleteSessionCustomizeTableData();
}
navigate(homePath);
Expand Down
37 changes: 37 additions & 0 deletions src/page/HomePage/HomePage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react';
import HomePage from './HomePage';

vi.mock('../LandingPage/LandingPage', () => ({
default: () => <div>일반 랜딩 화면</div>,
}));

vi.mock('../MaintenancePage/MaintenancePage', () => ({
default: () => <div>점검 화면</div>,
}));

describe('홈 화면 선택', () => {
afterEach(() => {
vi.unstubAllEnvs();
});

test.each([undefined, 'false', 'TRUE'])(
'점검 환경 변수가 %s이면 일반 랜딩 화면을 표시한다',
(value) => {
vi.stubEnv('VITE_MAINTENANCE_MODE', value ?? '');

render(<HomePage />);

expect(screen.getByText('일반 랜딩 화면')).toBeInTheDocument();
expect(screen.queryByText('점검 화면')).not.toBeInTheDocument();
},
);

test('점검 환경 변수가 true이면 점검 화면을 표시한다', () => {
vi.stubEnv('VITE_MAINTENANCE_MODE', 'true');

render(<HomePage />);

expect(screen.getByText('점검 화면')).toBeInTheDocument();
expect(screen.queryByText('일반 랜딩 화면')).not.toBeInTheDocument();
});
});
7 changes: 7 additions & 0 deletions src/page/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import LandingPage from '../LandingPage/LandingPage';
import MaintenancePage from '../MaintenancePage/MaintenancePage';
import { isMaintenanceModeEnabled } from '../../util/maintenanceMode';

export default function HomePage() {
return isMaintenanceModeEnabled() ? <MaintenancePage /> : <LandingPage />;
}
6 changes: 3 additions & 3 deletions src/page/LandingPage/components/MainSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import preview from '../../../assets/landing/preview.webm';
import { isLoggedIn } from '../../../util/accessToken';
import FillButton from '../../../components/FillButton/FillButton';

interface MainSectionProps {
onStartWithoutLogin: () => void;
Expand All @@ -23,12 +24,11 @@ export default function MainSection({
<h1 className="text-[min(max(1.5rem,3.5vw),3rem)] font-bold">
{t('토론 진행을 더 쉽고 빠르게')}
</h1>
<button
<FillButton
onClick={isLoggedIn() ? onDashboardButtonClicked : onStartWithoutLogin}
className="rounded-full border border-neutral-300 bg-brand px-5 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 hover:bg-semantic-table hover:text-default-white"
>
{isLoggedIn() ? t('대시보드로 이동') : t('비회원으로 시작하기')}
</button>
</FillButton>
</section>
);
}
8 changes: 5 additions & 3 deletions src/page/LandingPage/components/ReportSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import section501 from '../../../assets/landing/section5-1.png';
import { LANDING_URLS } from '../../../constants/urls';
import FillButton from '../../../components/FillButton/FillButton';

export default function ReportSection() {
const { t } = useTranslation();
Expand All @@ -14,18 +15,19 @@ export default function ReportSection() {
<p className="text-[min(max(0.875rem,1.25vw),1.2rem)] text-neutral-400">
{t('디베이트 타이머 사용 중 불편함을 느끼셨나요?')}
</p>
<button
<FillButton
variant="secondary"
size="md"
onClick={() =>
window.open(
LANDING_URLS.REPORT_FORM_URL,
'_blank',
'noopener,noreferrer',
)
}
className="rounded-full border border-neutral-300 bg-neutral-200 px-9 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 hover:bg-brand"
>
{t('접수하기')}
</button>
</FillButton>
</div>
<img src={section501} alt="section501" className="w-[30%]" />
</div>
Expand Down
8 changes: 3 additions & 5 deletions src/page/LandingPage/components/ReviewSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import ReviewCard from './ReviewCard';
import { REVIEWS } from '../../../constants/reviews';
import FillButton from '../../../components/FillButton/FillButton';

interface ReviewSectionProps {
onStartWithoutLogin: () => void;
Expand Down Expand Up @@ -28,12 +29,9 @@ export default function ReviewSection({
))}
</div>
<div className="flex w-full justify-center">
<button
className="rounded-full border border-neutral-300 bg-brand px-20 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 hover:bg-semantic-table hover:text-default-white"
onClick={onStartWithoutLogin}
>
<FillButton size="lg" onClick={onStartWithoutLogin}>
{t('비회원으로 시작하기')}
</button>
</FillButton>
</div>
</section>
);
Expand Down
10 changes: 4 additions & 6 deletions src/page/LandingPage/components/TableSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import section301 from '../../../assets/landing/debate_info_setting.png';
import section302 from '../../../assets/landing/table_list.png';
import FillButton from '../../../components/FillButton/FillButton';

interface TableSectionProps {
onLogin: () => void;
Expand Down Expand Up @@ -46,12 +47,9 @@ export default function TableSection({ onLogin }: TableSectionProps) {
<p className="whitespace-pre-line">
{t('시간표를 저장하려면,\n디베이트 타이머에 로그인해 보세요!')}
</p>
<button
className="mt-14 rounded-full border border-neutral-300 bg-brand px-5 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 hover:bg-semantic-table hover:text-default-white"
onClick={onLogin}
>
{t('3초 로그인')}
</button>
<div className="mt-14">
<FillButton onClick={onLogin}>{t('3초 로그인')}</FillButton>
</div>
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import section501 from '../../../assets/landing/section5-1.png';
import { LANDING_URLS } from '../../../constants/urls';
import FillButton from '../../../components/FillButton/FillButton';

export default function TemplateApplicationSection() {
const { t } = useTranslation();
Expand All @@ -13,18 +14,19 @@ export default function TemplateApplicationSection() {
<p className="text-[min(max(0.875rem,1.25vw),1.2rem)] text-neutral-400">
{t('새로운 템플릿도 신청해 볼까요?')}
</p>
<button
<FillButton
variant="secondary"
size="md"
onClick={() =>
window.open(
LANDING_URLS.TEMPLATE_REGISTER_URL,
'_blank',
'noopener,noreferrer',
)
}
className="rounded-full border border-neutral-300 bg-neutral-200 px-9 py-2 text-[min(max(0.875rem,1.25vw),1.2rem)] font-medium text-default-black transition-all duration-100 hover:bg-brand"
>
{t('신청하기')}
</button>
</FillButton>
</div>
<img src={section501} alt="section501" className="w-[30%]" />
</section>
Expand Down
114 changes: 114 additions & 0 deletions src/page/MaintenancePage/MaintenancePage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createInstance } from 'i18next';
import { I18nextProvider } from 'react-i18next';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { SAMPLE_TABLE_DATA } from '../../constants/sample_table';
import { getSessionCustomizeTableData } from '../../util/sessionStorage';
import MaintenancePage from './MaintenancePage';

const STORAGE_KEY = 'DebateTableData';

async function renderMaintenancePage(language: 'ko' | 'en' = 'ko') {
const i18n = createInstance();
await i18n.init({
lng: language,
fallbackLng: 'ko',
resources: {
ko: {
translation: {
'서비스 점검 중': '서비스 점검 중',
"죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.":
"죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.",
'오프라인으로 시작하기': '오프라인으로 시작하기',
'오프라인으로 이어하기': '오프라인으로 이어하기',
},
},
en: {
translation: {
'서비스 점검 중': 'Service Under Maintenance',
"죄송합니다. 나중에 다시 시도해주세요... 😭 대신, 오프라인 모드로 타이머를 사용해볼 수 있으니, 필요하신 경우 '{{action}}' 버튼을 클릭해주세요.":
'Sorry, please try again later... 😭 In the meantime, you can still use the timer in offline mode. If you need it, select “{{action}}.”',
'오프라인으로 시작하기': 'Start Offline',
'오프라인으로 이어하기': 'Continue Offline',
},
},
},
});

return render(
<I18nextProvider i18n={i18n}>
<MemoryRouter initialEntries={[language === 'ko' ? '/home' : '/en/home']}>
<Routes>
<Route path="/home" element={<MaintenancePage />} />
<Route path="/en/home" element={<MaintenancePage />} />
<Route
path="/overview/customize/guest"
element={<div>게스트 개요</div>}
/>
<Route
path="/en/overview/customize/guest"
element={<div>Guest overview</div>}
/>
</Routes>
</MemoryRouter>
</I18nextProvider>,
);
}

describe('점검 화면', () => {
afterEach(() => {
sessionStorage.clear();
});

test('게스트 세션이 없으면 샘플 데이터로 오프라인 흐름을 시작한다', async () => {
const user = userEvent.setup();
await renderMaintenancePage();

expect(
screen.getByRole('heading', { name: '서비스 점검 중' }),
).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: '오프라인으로 시작하기' }),
);

expect(await screen.findByText('게스트 개요')).toBeInTheDocument();
expect(getSessionCustomizeTableData()).toEqual({
id: -1,
...SAMPLE_TABLE_DATA,
});
});

test('기존 게스트 세션이 있으면 데이터를 덮어쓰지 않고 이어간다', async () => {
const user = userEvent.setup();
const existingData = {
id: -1,
info: { ...SAMPLE_TABLE_DATA.info, name: '수정한 시간표' },
table: SAMPLE_TABLE_DATA.table,
};
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(existingData));
await renderMaintenancePage();

expect(
screen.getByText(/'오프라인으로 이어하기' 버튼/),
).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: '오프라인으로 이어하기' }),
);

expect(await screen.findByText('게스트 개요')).toBeInTheDocument();
expect(getSessionCustomizeTableData()).toEqual(existingData);
});

test('영어에서는 영어 안내와 CTA를 표시한다', async () => {
await renderMaintenancePage('en');

expect(
screen.getByRole('heading', { name: 'Service Under Maintenance' }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Start Offline' }),
).toBeInTheDocument();
expect(screen.getByText(/select “Start Offline.”/)).toBeInTheDocument();
});
});
Loading
Loading