From ce6bb0df16c07c6cc94c12d8801a405986c3beb8 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Mon, 6 Jul 2026 18:31:04 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Feat:=20Header=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/constants/routes.ts | 1 + src/shared/styles/base/colors.css | 2 + src/shared/styles/base/z-index.css | 1 + src/widgets/common/header/Header.stories.tsx | 64 +++++++++++++ src/widgets/common/header/Header.tsx | 96 ++++++++++++++++++++ src/widgets/common/header/index.ts | 1 + 6 files changed, 165 insertions(+) create mode 100644 src/widgets/common/header/Header.stories.tsx create mode 100644 src/widgets/common/header/Header.tsx create mode 100644 src/widgets/common/header/index.ts diff --git a/src/shared/constants/routes.ts b/src/shared/constants/routes.ts index 0855431..d3500ed 100644 --- a/src/shared/constants/routes.ts +++ b/src/shared/constants/routes.ts @@ -6,5 +6,6 @@ */ export const ROUTES = { LANDING: '/', + LOGIN: '/login', WRITING: '/writing', } as const; diff --git a/src/shared/styles/base/colors.css b/src/shared/styles/base/colors.css index df8c0eb..d02c7a1 100644 --- a/src/shared/styles/base/colors.css +++ b/src/shared/styles/base/colors.css @@ -41,6 +41,8 @@ --color-white: #ffffff; /* Shadow Tokens */ + --shadow-header-login-button: 0 0 24px rgba(118, 186, 255, 0.28); + --shadow-header-login-button-hover: 0 8px 28px rgba(118, 186, 255, 0.36); --shadow-tooltip-arrow: 0 0 16px rgba(118, 186, 255, 0.18); --shadow-tooltip: 0 14px 44px rgba(0, 0, 0, 0.42), 0 0 0 1px rgba(118, 186, 255, 0.08); } diff --git a/src/shared/styles/base/z-index.css b/src/shared/styles/base/z-index.css index 27edf16..eb603a4 100644 --- a/src/shared/styles/base/z-index.css +++ b/src/shared/styles/base/z-index.css @@ -5,6 +5,7 @@ 예시: z-(--z-index-surface-content) */ --z-index-form-floating: 10; + --z-index-header: 30; --z-index-surface-overlay: 40; --z-index-surface-content: 50; --z-index-tooltip: 60; diff --git a/src/widgets/common/header/Header.stories.tsx b/src/widgets/common/header/Header.stories.tsx new file mode 100644 index 0000000..2c0ba3e --- /dev/null +++ b/src/widgets/common/header/Header.stories.tsx @@ -0,0 +1,64 @@ +import { DefaultProfileImage } from '@/shared/assets/images'; +import { ROUTES } from '@/shared/constants/routes'; + +import { Header } from './Header'; + +import type { Meta, StoryObj } from '@storybook/nextjs'; + +const meta = { + title: 'Widgets/Common/Header', + component: Header, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (Story) => ( +
+ +
+

Header Preview

+

+ 스크롤 시 Header 배경과 blur가 적용됩니다. Storybook에서는 Scrolled 스토리로 상태를 + 고정해서 확인할 수 있습니다. +

+
+
+
+ ), + ], + args: { + authenticatedLogoHref: ROUTES.WRITING, + loginHref: ROUTES.LOGIN, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Guest: Story = {}; + +export const GuestScrolled: Story = { + args: { + isBackgroundBlurred: true, + }, +}; + +export const LoggedIn: Story = { + args: { + user: { + name: '이서정', + profileImageUrl: DefaultProfileImage.src, + }, + }, +}; + +export const LoggedInScrolled: Story = { + args: { + isBackgroundBlurred: true, + user: { + name: '이서정', + profileImageUrl: DefaultProfileImage.src, + }, + }, +}; diff --git a/src/widgets/common/header/Header.tsx b/src/widgets/common/header/Header.tsx new file mode 100644 index 0000000..a501756 --- /dev/null +++ b/src/widgets/common/header/Header.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import type { ComponentPropsWithoutRef } from 'react'; + +import { Profile, type UserProfile } from '@/entities/user'; +import { ROUTES } from '@/shared/constants/routes'; +import { cn } from '@/shared/styles/utils/cn'; +import { LinkButton } from '@/shared/ui/button'; +import { TextLogo } from '@/shared/ui/logo'; + +interface HeaderProps extends ComponentPropsWithoutRef<'header'> { + user?: UserProfile | null; + logoHref?: string; + loginHref?: string; + authenticatedLogoHref?: string; + isBackgroundBlurred?: boolean; + blurThreshold?: number; +} + +/** + * ## Header + * + * @description + * 서비스 상단에서 사용하는 공통 Header 위젯입니다. + * 게스트는 로그인 CTA를, 로그인 사용자는 프로필을 표시합니다. + * + * @param user - 로그인 사용자 정보. 없으면 게스트 Header로 렌더링합니다. + * @param logoHref - 로고 클릭 시 이동할 경로. 기본값은 로그인 여부에 따라 결정합니다. + * @param authenticatedLogoHref - 로그인 상태일 때 로고 클릭 시 이동할 기본 경로입니다. + * @param isBackgroundBlurred - 전달하면 스크롤 감지 대신 blur 상태를 외부에서 제어합니다. + * @param blurThreshold - 스크롤 blur가 활성화되는 기준 scrollY 값입니다. + */ +export function Header({ + authenticatedLogoHref = ROUTES.WRITING, + blurThreshold = 0, + className, + isBackgroundBlurred, + loginHref = ROUTES.LOGIN, + logoHref, + user, + ...props +}: HeaderProps) { + const [hasScrolled, setHasScrolled] = useState(false); + const isLoggedIn = Boolean(user); + const resolvedLogoHref = logoHref ?? (isLoggedIn ? authenticatedLogoHref : ROUTES.LANDING); + const shouldBlur = isBackgroundBlurred ?? hasScrolled; + + useEffect(() => { + if (isBackgroundBlurred !== undefined) { + return; + } + + const handleScroll = () => { + setHasScrolled(window.scrollY > blurThreshold); + }; + + handleScroll(); + window.addEventListener('scroll', handleScroll, { passive: true }); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [blurThreshold, isBackgroundBlurred]); + + return ( +
+
+ + + {user ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/widgets/common/header/index.ts b/src/widgets/common/header/index.ts new file mode 100644 index 0000000..29429dc --- /dev/null +++ b/src/widgets/common/header/index.ts @@ -0,0 +1 @@ +export { Header } from './Header'; From afbe6620baa112e0446e8151c7ba71262fde96e3 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Tue, 7 Jul 2026 20:04:57 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=9C=A0=EC=A0=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B3=B5=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/common/header/Header.stories.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/widgets/common/header/Header.stories.tsx b/src/widgets/common/header/Header.stories.tsx index 2c0ba3e..cd2fda9 100644 --- a/src/widgets/common/header/Header.stories.tsx +++ b/src/widgets/common/header/Header.stories.tsx @@ -15,7 +15,7 @@ const meta = { (Story) => (
-
+

Header Preview

스크롤 시 Header 배경과 blur가 적용됩니다. Storybook에서는 Scrolled 스토리로 상태를 @@ -36,6 +36,11 @@ export default meta; type Story = StoryObj; +const loggedInUser = { + name: '이서정', + profileImageUrl: DefaultProfileImage.src, +}; + export const Guest: Story = {}; export const GuestScrolled: Story = { @@ -46,19 +51,13 @@ export const GuestScrolled: Story = { export const LoggedIn: Story = { args: { - user: { - name: '이서정', - profileImageUrl: DefaultProfileImage.src, - }, + user: loggedInUser, }, }; export const LoggedInScrolled: Story = { args: { isBackgroundBlurred: true, - user: { - name: '이서정', - profileImageUrl: DefaultProfileImage.src, - }, + user: loggedInUser, }, };