From e3725eaf66da09b7ca45c1a6f33c4fda1d37d829 Mon Sep 17 00:00:00 2001 From: malloc72p Date: Tue, 30 Jun 2026 09:49:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(blog):=20=EB=8B=A4=ED=81=AC=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=8F=84=EC=9E=85=20=E2=80=94=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=ED=85=8C=EB=A7=88=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - global.css: --color-surface 토큰 + .dark에서 색 변수 오버라이드(텍스트 그레이는 밝게, 배경 그레이는 어둡게, surface/border/brand/blue). Tailwind v4 유틸이 var()를 참조하므로 컴포넌트 대부분 변경 없이 전체가 전환됨 - ThemeToggle: html.dark + localStorage 토글, themechange 이벤트 발행 - layout: FOUC 방지 인라인 스크립트(paint 전 테마 결정) + suppressHydrationWarning - giscus: 사이트 테마 동적 연동(themechange 구독) - bg-white → bg-surface(5곳): 흰 텍스트(--color-white)와 충돌하던 surface만 토큰화 - e2e/theme: 토글·새로고침 유지 검증 Co-Authored-By: Claude Opus 4.8 --- apps/blog/e2e/theme.spec.ts | 28 ++++++++++ apps/blog/src/app/global.css | 40 +++++++++++++- apps/blog/src/app/layout.tsx | 9 ++- .../blog/src/components/article-container.tsx | 2 +- apps/blog/src/components/main-header.tsx | 4 +- apps/blog/src/components/mobile-sidebar.tsx | 2 +- .../post-detail/giscus-comments.tsx | 17 +++++- .../src/components/search/search-modal.tsx | 4 +- apps/blog/src/components/theme-toggle.tsx | 55 +++++++++++++++++++ 9 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 apps/blog/e2e/theme.spec.ts create mode 100644 apps/blog/src/components/theme-toggle.tsx diff --git a/apps/blog/e2e/theme.spec.ts b/apps/blog/e2e/theme.spec.ts new file mode 100644 index 0000000..164b7ca --- /dev/null +++ b/apps/blog/e2e/theme.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; + +test.describe('다크모드', () => { + // 기본(시스템) 테마를 light로 고정해 토글 동작을 결정적으로 검증한다. + test.use({ colorScheme: 'light' }); + + test('테마 토글로 다크모드를 켜고 끌 수 있고, 새로고침 후에도 유지된다', async ({ page }) => { + await page.goto('/'); + + const html = page.locator('html'); + const toggle = page.getByRole('button', { name: /모드로 전환/ }); + + // 초기에는 라이트(html.dark 없음). + await expect(html).not.toHaveClass(/dark/); + + // 토글 → 다크 적용. + await toggle.click(); + await expect(html).toHaveClass(/dark/); + + // 새로고침해도 localStorage로 다크가 유지되어야 한다(FOUC 방지 스크립트). + await page.reload(); + await expect(html).toHaveClass(/dark/); + + // 다시 토글 → 라이트로 복귀. + await page.getByRole('button', { name: /모드로 전환/ }).click(); + await expect(html).not.toHaveClass(/dark/); + }); +}); diff --git a/apps/blog/src/app/global.css b/apps/blog/src/app/global.css index cd70226..a02c873 100644 --- a/apps/blog/src/app/global.css +++ b/apps/blog/src/app/global.css @@ -12,16 +12,54 @@ /* 다크 서피스 단일 토큰. 헤더·드롭다운·사이드바·뱃지의 검정 계열(black/gray-900/950)을 통일한다. */ --color-ink: #0a0a0a; /* 중립 구조색. 값은 기존 Tailwind gray와 동일해 시각 변화 없이 단일 출처로 모은다. */ - --color-surface-muted: #f9fafb; /* gray-50: 카드 배경 */ + --color-surface: #ffffff; /* 페이지·카드 기본 배경 */ + --color-surface-muted: #f9fafb; /* gray-50: 옅은 카드 배경 */ --color-border: #e5e7eb; /* gray-200: 카드/모달 경계 */ --color-border-subtle: #f3f4f6; /* gray-100: 옅은 구분선 */ } +/* === 다크 테마 (#90) === + Tailwind v4 유틸은 var(--color-*)를 참조하므로, html.dark에서 색 변수만 덮어쓰면 + 컴포넌트 변경 없이 전체가 전환된다. 텍스트 그레이(400~900)는 밝게, 배경 그레이(50~200)는 + 어둡게 뒤집는다. 항상 어두운 헤더(ink)·흰 텍스트(text-white)는 별도 토큰이라 그대로 유지된다. */ +.dark { + /* 시맨틱 surface */ + --color-surface: #0f1014; + --color-surface-muted: #17181d; + /* 배경 그레이(라이트 배경 → 다크) */ + --color-gray-50: #17181d; + --color-gray-100: #1f2026; + --color-gray-200: #2a2b32; + /* 텍스트 그레이(어두운 글자 → 밝은 글자) */ + --color-gray-400: #6e7079; + --color-gray-500: #8b8d96; + --color-gray-600: #a6a8b1; + --color-gray-700: #c4c6cd; + --color-gray-800: #d9dade; + --color-gray-900: #e9eaed; + --color-black: #e9eaed; /* text-black → 밝게 */ + /* 경계 */ + --color-border: #2a2b32; + --color-border-subtle: #232429; + /* 브랜드: 다크 배경에서 비텍스트 액센트(포커스 링)는 살짝 밝게 */ + --color-brand: #818cf8; + /* 본문 링크(blue) — 다크에서 밝게 */ + --color-blue-600: #93b4ff; + --color-blue-700: #a8c3ff; + --color-blue-800: #c2d5ff; +} + html, body { height: 100%; } +/* 페이지 전체 배경/기본 글자색을 토큰에 연결해 테마 전환 시 함께 바뀌게 한다. */ +body { + background-color: var(--color-surface); + color: var(--color-gray-900); +} + /* 동작 줄이기(prefers-reduced-motion) 설정 시 전역 트랜지션/애니메이션/부드러운 스크롤을 사실상 비활성화한다. 어지럼증(전정기관 장애) 사용자를 위한 WCAG 2.3.3 대응. */ @media (prefers-reduced-motion: reduce) { diff --git a/apps/blog/src/app/layout.tsx b/apps/blog/src/app/layout.tsx index 2c9fd88..01ec5a8 100644 --- a/apps/blog/src/app/layout.tsx +++ b/apps/blog/src/app/layout.tsx @@ -75,9 +75,16 @@ export const metadata = { * 폰트 설정 및 공통 메타데이터 설정은 이곳에서 한다. */ export default async function RootLayout({ children }: PropsWithChildren) { + // 테마 스크립트가 하이드레이션 전에 html.class를 바꾸므로 suppressHydrationWarning으로 className 불일치 경고를 억제한다. return ( - + + {/* 테마 깜빡임(FOUC) 방지: paint 전에 localStorage/시스템 설정으로 html.dark를 결정한다. */} +