diff --git a/frontend/src/features/feedback/lib/downloadPdf.test.ts b/frontend/src/features/feedback/lib/downloadPdf.test.ts new file mode 100644 index 00000000..fcdea35b --- /dev/null +++ b/frontend/src/features/feedback/lib/downloadPdf.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// 캡처 시점에 이 어떤 팔레트였는지 기록해 둔다. +let schemeDuringCapture: string | undefined +const save = vi.fn() + +vi.mock('html2canvas-pro', () => ({ + default: vi.fn(async () => { + schemeDuringCapture = document.documentElement.dataset.seedUserColorScheme + return { width: 800, height: 600, toDataURL: () => 'data:image/png;base64,x' } + }), +})) + +vi.mock('jspdf', () => ({ + // `new jsPDF(...)` 로 호출되므로 화살표 함수는 쓸 수 없다(생성자 불가). + jsPDF: vi.fn(function () { + return { + internal: { pageSize: { getWidth: () => 400, getHeight: () => 600 } }, + addImage: vi.fn(), + addPage: vi.fn(), + save, + } + }), +})) + +const { downloadElementAsPdf } = await import('./downloadPdf') + +describe('downloadElementAsPdf', () => { + beforeEach(() => { + schemeDuringCapture = undefined + save.mockClear() + localStorage.clear() + document.documentElement.removeAttribute('data-seed-color-mode') + document.documentElement.removeAttribute('data-seed-user-color-scheme') + }) + + // 다크 화면을 그대로 캡처하면 검은 배경에 밝은 글씨인 PDF 가 나온다 — 인쇄·공유용으로 못 쓴다. + it('다크모드여도 라이트 팔레트로 캡처한다', async () => { + localStorage.setItem('stackup.color-mode', 'dark-only') + document.documentElement.dataset.seedColorMode = 'dark-only' + document.documentElement.dataset.seedUserColorScheme = 'dark' + + await downloadElementAsPdf(document.createElement('div'), 'r.pdf') + + expect(schemeDuringCapture).toBe('light') + expect(save).toHaveBeenCalledWith('r.pdf') + }) + + it('캡처가 끝나면 원래 컬러 모드로 되돌린다', async () => { + localStorage.setItem('stackup.color-mode', 'dark-only') + document.documentElement.dataset.seedColorMode = 'dark-only' + document.documentElement.dataset.seedUserColorScheme = 'dark' + + await downloadElementAsPdf(document.createElement('div'), 'r.pdf') + + expect(document.documentElement.dataset.seedColorMode).toBe('dark-only') + expect(document.documentElement.dataset.seedUserColorScheme).toBe('dark') + }) + + // 라이트 사용자는 팔레트를 건드릴 이유가 없다(불필요한 리페인트·깜빡임 방지). + it('라이트 모드에서는 팔레트를 건드리지 않는다', async () => { + document.documentElement.dataset.seedColorMode = 'system' + document.documentElement.dataset.seedUserColorScheme = 'light' + + await downloadElementAsPdf(document.createElement('div'), 'r.pdf') + + expect(schemeDuringCapture).toBe('light') + expect(document.documentElement.dataset.seedColorMode).toBe('system') + }) +}) diff --git a/frontend/src/features/feedback/lib/downloadPdf.ts b/frontend/src/features/feedback/lib/downloadPdf.ts index 90831b53..a43e31ff 100644 --- a/frontend/src/features/feedback/lib/downloadPdf.ts +++ b/frontend/src/features/feedback/lib/downloadPdf.ts @@ -1,6 +1,26 @@ // DOM 요소를 캡처해 A4 PDF 로 저장(클라이언트). 한글은 DOM 렌더를 래스터화하므로 폰트 임베딩 불필요. // jspdf/html2canvas-pro 는 무거우므로 다운로드 시점에 동적 import (메인 번들 비대화 방지). // Tailwind v4 의 oklch 색을 지원하려면 html2canvas-pro 사용. +import { applyColorMode, isDarkApplied, readStoredColorMode } from '@/shared/lib/color-mode' + +/** + * 콜백이 도는 동안만 라이트 팔레트를 적용한다. 원래 모드는 무슨 일이 있어도 되돌린다. + * + *

SEED 는 `` 속성으로 팔레트를 고르므로 속성만 바꾸면 되지만, CSS 변수가 다시 + * 계산되어 화면에 반영될 시간을 한 프레임 준다 — 바로 캡처하면 이전 색이 찍힌다. + */ +async function withLightPalette(run: () => Promise): Promise { + if (!isDarkApplied()) return run() + const previous = readStoredColorMode() + applyColorMode('light-only') + try { + await new Promise((resolve) => requestAnimationFrame(() => resolve(null))) + return await run() + } finally { + applyColorMode(previous) + } +} + export async function downloadElementAsPdf( el: HTMLElement, filename: string, @@ -10,7 +30,10 @@ export async function downloadElementAsPdf( import('html2canvas-pro'), ]) - const canvas = await html2canvas(el, { scale: 2 }) + // html2canvas 는 '지금 화면에 보이는 대로' 캡처한다. 다크모드 사용자가 그대로 뽑으면 + // 검은 배경에 밝은 글씨인 PDF 가 나온다 — 출력·공유용으로는 쓸 수 없다. + // 캡처하는 동안만 라이트 팔레트를 강제하고 원래 설정으로 되돌린다. + const canvas = await withLightPalette(() => html2canvas(el, { scale: 2 })) const img = canvas.toDataURL('image/png') const pdf = new jsPDF({ unit: 'px', format: 'a4' })