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
5 changes: 2 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
이어도 반드시 붙여야 캐시가 재사용된다).

나머지 셋은 일부러 preload하지 않는다 — 표제용 제주한라산(195K)·인용용 제주명조(241K)는
아직 적용 대상 컴포넌트가 없어 쓰이지 않고, 손글씨체(469K)는 포스트잇 전용이라 첫 화면에
없는 경우가 많다. 전부 preload하면 정작 필요한 본문 서체와 대역폭을 다투어 초기 렌더가
밀린다.
쓰는 화면에서만 요청하고, 손글씨체(469K)는 포스트잇 전용이라 첫 화면에 없는 경우가 많다.
전부 preload하면 정작 필요한 본문 서체와 대역폭을 다투어 초기 렌더가 밀린다.

380: 본문 서체가 잘난고딕(154K)에서 제주고딕(92K)으로 바뀌면서 이 preload 한 건의
전송량도 그만큼 줄었다. -->
Expand Down
39 changes: 39 additions & 0 deletions src/features/home/components/HomeSheetPanels.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import('react-dom')>()),
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(<PaperCoverFace title="책장" />));

expect(preloadMock).toHaveBeenCalledExactlyOnceWith('/fonts/jeju-myeongjo.woff2', {
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
});
});
});
9 changes: 9 additions & 0 deletions src/features/home/components/HomeSheetPanels.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<>
<span className="pl-cover-frame" aria-hidden="true" />
Expand Down
78 changes: 78 additions & 0 deletions src/pages/LoginPage.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <a href="#">{children}</a>,
}));

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(<LoginPage />));

const logo = container.querySelector<HTMLImageElement>('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(<LoginPage />));

const googleButton = container.querySelector<HTMLButtonElement>(
'button[aria-label="Google 계정으로 로그인"]',
);
act(() => googleButton?.click());

expect(savePreLoginPathMock).toHaveBeenCalledOnce();
expect(redirectToProviderLoginMock).toHaveBeenCalledExactlyOnceWith('google');
});
});
17 changes: 11 additions & 6 deletions src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -149,9 +149,13 @@ export function LoginPage() {
<CoverFrame tone="gold" />
<div className="flex flex-col items-center gap-[0.6em]">
<CoverLabelBetweenRules>기억이 머무는 자리</CoverLabelBetweenRules>
<CoverTitle size="md" align="center" tracking="tight" className="font-display">
핀로그
</CoverTitle>
<img
src={pinLogLogo}
alt="핀로그"
width={1447}
height={1087}
className="-my-[0.55em] h-auto w-[min(72%,6.2em)]"
/>
<CoverRule width="short" />
</div>
</CoverPaper>
Expand All @@ -160,10 +164,11 @@ export function LoginPage() {
{/* 열린 표지 안쪽 — 소셜 로그인. */}
<div className="mt-9 flex w-[min(92vw,400px)] flex-col items-center gap-6">
<p
className="login-item-in max-w-xs text-center text-sm text-ink-gray"
className="login-item-in max-w-sm break-keep text-center text-sm leading-relaxed text-ink-gray"
style={{ animationDelay: '0.66s' }}
>
장소의 기억을 한 권의 책으로, 기록하고 모으고 새로운 이야기를 발견하세요
<span className="block">장소의 기억을 한 권의 책으로,</span>
<span className="block">기록하고 모으고 새로운 이야기를 발견하세요</span>
</p>

<div className="flex w-full flex-col gap-3">
Expand Down
Loading