Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/conventions/component-convention.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (신규 컴포넌트)

Expand Down
101 changes: 78 additions & 23 deletions src/components/ui/ScreenHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2">
{options.map((option, index) => {
const active = index === activeIndex;
return (
<button key={option} onClick={() => onChange?.(index)} type="button">
<Typography
color={
active ? "semantic.label.strong" : "semantic.label.disable"
}
variant="title3"
weight="bold"
>
{option}
</Typography>
</button>
);
})}
</div>
);
}

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 (
<TopNavigation
background={false}
trailingContent={
<>
<TopNavigationButton aria-label="검색" variant="icon">
<IconSearch />
</TopNavigationButton>
<TopNavigationButton aria-label="알림" variant="icon">
<IconBell />
</TopNavigationButton>
</>
}
variant="display"
leadingContent={leading}
trailingContent={trailing}
variant={props.variant ?? "display"}
>
{title}
{title !== undefined &&
(isToggleTitle(title) ? <ScreenHeaderToggleTitle {...title} /> : title)}
</TopNavigation>
);
}
Expand Down
23 changes: 21 additions & 2 deletions src/features/bililge/BililgeListScreen.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,7 +23,21 @@ function BililgeListScreen() {
const [rentalItem, setRentalItem] = useState<BililgeItem | null>(null);
const [rentalSheetOpen, setRentalSheetOpen] = useState(false);

useScreenHeader(<ScreenHeader title="빌릴게" />);
useScreenHeader(
<ScreenHeader
title="빌릴게"
trailing={
<>
<TopNavigationButton aria-label="검색" variant="icon">
<IconSearch />
</TopNavigationButton>
<TopNavigationButton aria-label="알림" variant="icon">
<IconBell />
</TopNavigationButton>
</>
}
/>,
);

return (
<div className="flex h-full flex-col">
Expand Down
18 changes: 17 additions & 1 deletion src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { TopNavigationButton } from "@wanteddev/wds";
import { IconBell, IconSearch } from "@wanteddev/wds-icon";
import { Link } from "react-router-dom";

import ScreenHeader from "@/components/ui/ScreenHeader";
import { useScreenHeader } from "@/components/ui/useScreenHeader";

// 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다.
function HomeScreen() {
useScreenHeader(<ScreenHeader title="STREAM" />);
useScreenHeader(
<ScreenHeader
title="STREAM"
trailing={
<>
<TopNavigationButton aria-label="검색" variant="icon">
<IconSearch />
</TopNavigationButton>
<TopNavigationButton aria-label="알림" variant="icon">
<IconBell />
</TopNavigationButton>
</>
}
/>,
);

return (
<div className="flex flex-col items-center justify-center gap-4 py-20">
Expand Down
Loading