diff --git a/index.html b/index.html index 6af003d..fc90aaa 100644 --- a/index.html +++ b/index.html @@ -18,9 +18,8 @@ 이어도 반드시 붙여야 캐시가 재사용된다). 나머지 셋은 일부러 preload하지 않는다 — 표제용 제주한라산(195K)·인용용 제주명조(241K)는 - 아직 적용 대상 컴포넌트가 없어 쓰이지 않고, 손글씨체(469K)는 포스트잇 전용이라 첫 화면에 - 없는 경우가 많다. 전부 preload하면 정작 필요한 본문 서체와 대역폭을 다투어 초기 렌더가 - 밀린다. + 쓰는 화면에서만 요청하고, 손글씨체(469K)는 포스트잇 전용이라 첫 화면에 없는 경우가 많다. + 전부 preload하면 정작 필요한 본문 서체와 대역폭을 다투어 초기 렌더가 밀린다. 380: 본문 서체가 잘난고딕(154K)에서 제주고딕(92K)으로 바뀌면서 이 preload 한 건의 전송량도 그만큼 줄었다. --> diff --git a/src/features/home/components/HomeSheetPanels.test.tsx b/src/features/home/components/HomeSheetPanels.test.tsx new file mode 100644 index 0000000..2c77838 --- /dev/null +++ b/src/features/home/components/HomeSheetPanels.test.tsx @@ -0,0 +1,39 @@ +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { PaperCoverFace } from './HomeSheetPanels'; + +const { preloadMock } = vi.hoisted(() => ({ preloadMock: vi.fn() })); + +vi.mock('react-dom', async (importOriginal) => ({ + ...(await importOriginal()), + preload: preloadMock, +})); + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + preloadMock.mockClear(); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe('PaperCoverFace', () => { + it('이동 책이 렌더될 때만 제주명조를 preload한다', () => { + act(() => root.render()); + + expect(preloadMock).toHaveBeenCalledExactlyOnceWith('/fonts/jeju-myeongjo.woff2', { + as: 'font', + type: 'font/woff2', + crossOrigin: 'anonymous', + }); + }); +}); diff --git a/src/features/home/components/HomeSheetPanels.tsx b/src/features/home/components/HomeSheetPanels.tsx index 4c49912..81fe86b 100644 --- a/src/features/home/components/HomeSheetPanels.tsx +++ b/src/features/home/components/HomeSheetPanels.tsx @@ -1,3 +1,4 @@ +import { preload } from 'react-dom'; import { Link } from '@tanstack/react-router'; import { usePlaceRecordSheet } from '@/contexts/usePlaceRecordSheet'; import { PaperNoteParts } from '@/shared/ui/PaperNoteParts'; @@ -152,6 +153,14 @@ export function HomeRightType() { * 표지 조판이 달라지면 "같은 시리즈의 책"이라는 인상이 깨진다. 그래서 복제하지 않고 내보낸다. */ export function PaperCoverFace({ title }: { title: string }) { + // 이 표지가 실제로 필요한 화면에서만 제주명조를 미리 요청한다. index.html에서 전역 + // preload하면 로그인·약관처럼 표지를 렌더하지 않는 진입도 241KB 폰트를 받게 된다. + preload('/fonts/jeju-myeongjo.woff2', { + as: 'font', + type: 'font/woff2', + crossOrigin: 'anonymous', + }); + return ( <> diff --git a/src/pages/LoginPage.test.tsx b/src/pages/LoginPage.test.tsx new file mode 100644 index 0000000..935c089 --- /dev/null +++ b/src/pages/LoginPage.test.tsx @@ -0,0 +1,78 @@ +import { act, type ReactNode } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { LoginPage } from './LoginPage'; + +const { savePreLoginPathMock, redirectToProviderLoginMock } = vi.hoisted(() => ({ + savePreLoginPathMock: vi.fn(), + redirectToProviderLoginMock: vi.fn(), +})); + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children }: { children: ReactNode }) => {children}, +})); + +vi.mock('@/features/auth/lib/preLoginPath', () => ({ + savePreLoginPath: savePreLoginPathMock, +})); + +vi.mock('@/features/auth/lib/redirectToProviderLogin', () => ({ + redirectToProviderLogin: redirectToProviderLoginMock, +})); + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + vi.stubGlobal( + 'matchMedia', + vi.fn(() => ({ matches: true }) as MediaQueryList), + ); + savePreLoginPathMock.mockClear(); + redirectToProviderLoginMock.mockClear(); + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); + vi.unstubAllGlobals(); +}); + +describe('LoginPage', () => { + it('공식 핀로그 로고와 줄 단위로 고정된 소개 문구를 표시한다', () => { + act(() => root.render()); + + const logo = container.querySelector('img[alt="핀로그"]'); + expect(logo?.getAttribute('src')).toContain('logo-full.png'); + expect(logo?.width).toBe(1447); + expect(logo?.height).toBe(1087); + + const firstLine = Array.from(container.querySelectorAll('span')).find( + (element) => element.textContent === '장소의 기억을 한 권의 책으로,', + ); + const secondLine = Array.from(container.querySelectorAll('span')).find( + (element) => element.textContent === '기록하고 모으고 새로운 이야기를 발견하세요', + ); + + expect(firstLine?.classList.contains('block')).toBe(true); + expect(secondLine?.classList.contains('block')).toBe(true); + expect(firstLine?.parentElement?.classList.contains('break-keep')).toBe(true); + expect(container.querySelectorAll('button[aria-label$="계정으로 로그인"]')).toHaveLength(3); + }); + + it('로그인 버튼의 기존 인증 진입 흐름을 유지한다', () => { + act(() => root.render()); + + const googleButton = container.querySelector( + 'button[aria-label="Google 계정으로 로그인"]', + ); + act(() => googleButton?.click()); + + expect(savePreLoginPathMock).toHaveBeenCalledOnce(); + expect(redirectToProviderLoginMock).toHaveBeenCalledExactlyOnceWith('google'); + }); +}); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 6021ae6..c36866c 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,12 +1,12 @@ import type { ReactNode } from 'react'; import { Link } from '@tanstack/react-router'; +import pinLogLogo from '@/assets/logo-full.png'; import { PaperStage } from '@/features/paper/components/PaperStage'; import { CoverFrame, CoverLabelBetweenRules, CoverPaper, CoverRule, - CoverTitle, } from '@/features/feed/components/covers/coverParts'; import { savePreLoginPath } from '@/features/auth/lib/preLoginPath'; import { @@ -149,9 +149,13 @@ export function LoginPage() { 기억이 머무는 자리 - - 핀로그 - + @@ -160,10 +164,11 @@ export function LoginPage() { {/* 열린 표지 안쪽 — 소셜 로그인. */} - 장소의 기억을 한 권의 책으로, 기록하고 모으고 새로운 이야기를 발견하세요 + 장소의 기억을 한 권의 책으로, + 기록하고 모으고 새로운 이야기를 발견하세요
- 장소의 기억을 한 권의 책으로, 기록하고 모으고 새로운 이야기를 발견하세요 + 장소의 기억을 한 권의 책으로, + 기록하고 모으고 새로운 이야기를 발견하세요