diff --git a/docs/conventions/component-convention.md b/docs/conventions/component-convention.md index 401208a..6d048bf 100644 --- a/docs/conventions/component-convention.md +++ b/docs/conventions/component-convention.md @@ -40,6 +40,7 @@ interface RentalItemCardProps { - 예: Figma `Chip/Chip` → `import { Chip } from '@wanteddev/wds'` - 아이콘은 `@wanteddev/wds-icon`에서 가져온다. - **WDS 컴포넌트 내부를 임의로 오버라이드하지 않는다.** 간격·배치 같은 레이아웃 조정은 감싸는 wrapper에서 한다. +- **화면 헤더는 화면에서 `TopNavigation`을 직접 새로 조립하지 않고 `src/components/ui/ScreenHeader.tsx`를 거친다.** Figma의 Top Navigation 패턴(타이틀 정렬, leading/trailing 조합, 게시판류의 토글형 타이틀 등)을 `ScreenHeader`의 props(`variant`/`title`/`leading`/`trailing`)로 고르게 돼 있다 — 화면마다 손으로 다시 조립하면 컨벤션이 흩어진다. 새 헤더 패턴이 필요하면 `ScreenHeader`부터 확장한다(`docs/plans/unified-screen-header.md` 참고). ## 4. Stream 고유 UI (신규 컴포넌트) diff --git a/src/components/ui/ScreenHeader.tsx b/src/components/ui/ScreenHeader.tsx index 18e3877..a67881f 100644 --- a/src/components/ui/ScreenHeader.tsx +++ b/src/components/ui/ScreenHeader.tsx @@ -1,33 +1,88 @@ -import { TopNavigation, TopNavigationButton } from "@wanteddev/wds"; -import { IconBell, IconSearch } from "@wanteddev/wds-icon"; +import { TopNavigation, Typography } from "@wanteddev/wds"; +import type { ReactNode } from "react"; -interface ScreenHeaderProps { - title: string; +interface ScreenHeaderToggleTitle { + options: string[]; + activeIndex: number; + onChange?: (index: number) => void; } -// Figma: Top Navigation/Resource/Contents의 타이틀 + 검색/알림 아이콘 부분 — 행사·빌릴게 등 -// 여러 화면에서 완전히 동일하게 반복되는 진짜 공통 패턴이라 재사용 컴포넌트로 뺐다. -// "Tool" 슬롯(세그먼트 토글 등)은 화면마다 값·동작이 달라서(대여/반납 vs 행사/신청내역) -// 여기 포함하지 않고 각 화면이 자기 본문에서 직접 그린다. -// background 기본값(true)은 iOS 반투명 스타일이라 뒤 배경이 비쳐 보인다. Figma는 별도 배경 없이 -// 화면 배경을 그대로 쓴다. -function ScreenHeader({ title }: ScreenHeaderProps) { +type ScreenHeaderTitle = string | ScreenHeaderToggleTitle; + +function isToggleTitle( + title: ScreenHeaderTitle, +): title is ScreenHeaderToggleTitle { + return typeof title !== "string"; +} + +// Figma: 게시판류 화면의 "공지 | 열린피드백" 같은 2단 탭 타이틀(nodeId 1256:81792 "Board Title"). +// 활성 옵션은 Label/Strong(검정), 비활성은 Label/Disable(흐림) — 둘 다 같은 Title 3/Bold(24px). +function ScreenHeaderToggleTitle({ + options, + activeIndex, + onChange, +}: ScreenHeaderToggleTitle) { + return ( +
+ {options.map((option, index) => { + const active = index === activeIndex; + return ( + + ); + })} +
+ ); +} + +type ScreenHeaderProps = + | { + variant?: "display"; + title?: ScreenHeaderTitle; + trailing?: ReactNode; + } + | { + variant: "normal"; + title?: ScreenHeaderTitle; + leading?: ReactNode; + trailing?: ReactNode; + }; + +// Figma: Top Navigation/Resource/Contents — 화면마다 따로 조립하던 헤더를 여기 하나로 모았다. +// WDS `TopNavigation`을 감싸는 얇은 조합 레이어일 뿐, 내부 스타일은 오버라이드하지 않는다 +// (component-convention.md "WDS 컴포넌트 내부를 임의로 오버라이드하지 않는다"). +// +// variant="display"(기본값)에서는 leading을 받지 않는다 — WDS 쪽 스타일 자체가 display일 때 +// leading/trailing 포지셔닝(topNavigationLeftIconStyle/RightIconStyle)을 안 줘서 레이아웃이 +// 깨진다. leading이 필요한 화면(뒤로가기·닫기 버튼 등)은 variant="normal"을 쓴다. +// +// title이 문자열이면 그대로 렌더링하고, { options, activeIndex } 형태(활성 상태가 있는 경우)면 +// 게시판류의 토글형 2단 타이틀로 렌더링한다. +// +// search variant(타이틀 자리가 검색 필드로 바뀌는 패턴)는 이번 범위에서 뺐다 — +// docs/plans/unified-screen-header.md 참고. 화면이 실제로 생기면 그때 추가한다. +function ScreenHeader(props: ScreenHeaderProps) { + const { title, trailing } = props; + const leading = props.variant === "normal" ? props.leading : undefined; + return ( - - - - - - - - } - variant="display" + leadingContent={leading} + trailingContent={trailing} + variant={props.variant ?? "display"} > - {title} + {title !== undefined && + (isToggleTitle(title) ? : title)} ); } diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index 17c716a..a6725e6 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -1,4 +1,9 @@ -import { SegmentedControl, SegmentedControlItem } from "@wanteddev/wds"; +import { + SegmentedControl, + SegmentedControlItem, + TopNavigationButton, +} from "@wanteddev/wds"; +import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { startTransition, useState } from "react"; import ScreenHeader from "@/components/ui/ScreenHeader"; @@ -18,7 +23,21 @@ function BililgeListScreen() { const [rentalItem, setRentalItem] = useState(null); const [rentalSheetOpen, setRentalSheetOpen] = useState(false); - useScreenHeader(); + useScreenHeader( + + + + + + + + + } + />, + ); return (
diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 350b856..3f44520 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -1,3 +1,5 @@ +import { TopNavigationButton } from "@wanteddev/wds"; +import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { Link } from "react-router-dom"; import ScreenHeader from "@/components/ui/ScreenHeader"; @@ -5,7 +7,21 @@ import { useScreenHeader } from "@/components/ui/useScreenHeader"; // 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다. function HomeScreen() { - useScreenHeader(); + useScreenHeader( + + + + + + + + + } + />, + ); return (