From 7c5b6966091e599656b832c2638f1a7b992f1c2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?=
<104981505+xeoxxn@users.noreply.github.com>
Date: Thu, 10 Sep 2026 15:01:08 +0900
Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=ED=99=94=EB=A9=B4=20=ED=97=A4?=
=?UTF-8?q?=EB=8D=94=EB=A5=BC=20ScreenHeader=20=ED=95=98=EB=82=98=EB=A1=9C?=
=?UTF-8?q?=20=ED=86=B5=ED=95=A9(variant/title/leading/trailing=20props)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Figma의 Top Navigation 패턴(타이틀 정렬, leading/trailing 조합, 게시판류
토글형 2단 타이틀)을 props로 고르게 만들었다. display variant에서는
leading을 타입으로 막았다 — WDS 쪽 스타일이 display일 때 leading/trailing
포지셔닝을 안 줘서 레이아웃이 깨진다.
---
docs/conventions/component-convention.md | 1 +
src/components/ui/ScreenHeader.tsx | 112 +++++++++++++++------
src/features/bililge/BililgeListScreen.tsx | 23 ++++-
src/features/home/HomeScreen.tsx | 18 +++-
4 files changed, 120 insertions(+), 34 deletions(-)
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 e989900..a67881f 100644
--- a/src/components/ui/ScreenHeader.tsx
+++ b/src/components/ui/ScreenHeader.tsx
@@ -1,40 +1,90 @@
-import { TopNavigationButton, Typography } 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(nodeId 1765:71193)의 타이틀 + 검색/알림 아이콘 부분 — 행사·빌릴게 등
-// 여러 화면에서 완전히 동일하게 반복되는 진짜 공통 패턴이라 재사용 컴포넌트로 뺐다.
-// "Tool" 슬롯(세그먼트 토글 등)은 화면마다 값·동작이 달라서(대여/반납 vs 행사/신청내역)
-// 여기 포함하지 않고 각 화면이 자기 본문에서 직접 그린다.
-//
-// 이 Top Navigation은 WDS Top Navigation/Resource/Contents가 아니라 Stream 로컬
-// 컴포넌트다 — 세로 패딩 12px + Title 3/Bold(32px)로 총 56px인데, WDS display variant는
-// 세로 패딩이 16px 고정이라 64px이 된다. 그래서 레이아웃만 직접 구현하고, 아이콘 버튼
-// (TopNavigationButton)은 그대로 재사용한다.
-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 (
-
-
- {title}
-
-
-
-
-
-
-
-
-
+
+ {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 (
+
+ {title !== undefined &&
+ (isToggleTitle(title) ? : title)}
+
+ );
+}
+
export default ScreenHeader;
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 (
From bfa23b1fb23c2db24949eb0a551ae6d5d1954e46 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?=
<104981505+xeoxxn@users.noreply.github.com>
Date: Mon, 14 Sep 2026 19:49:19 +0900
Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EB=B9=8C=EB=A6=B4=EA=B2=8C/?=
=?UTF-8?q?=ED=99=88=20=ED=97=A4=EB=8D=94=EB=A5=BC=20Figma=20=EB=A1=9C?=
=?UTF-8?q?=EC=BB=AC=20Top=20Navigation=20=EC=82=AC=EC=96=91(56px)?=
=?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Figma의 Top Navigation이 WDS Top Navigation/Resource/Contents에서
Stream 로컬 컴포넌트(nodeId 1765:71193)로 바뀌었다. 세로 패딩 12px +
Title 3/Bold(32px)로 총 56px인데, WDS display variant는 세로 패딩이
16px로 고정돼 있어 64px이 된다. variant="display"(기본값) 경로만
직접 구현으로 바꾸고, leading이 필요한 variant="normal"은 그대로
WDS TopNavigation을 재사용한다.
---
src/components/ui/ScreenHeader.tsx | 61 ++++++++++++++++++++++--------
1 file changed, 45 insertions(+), 16 deletions(-)
diff --git a/src/components/ui/ScreenHeader.tsx b/src/components/ui/ScreenHeader.tsx
index a67881f..4c18e9e 100644
--- a/src/components/ui/ScreenHeader.tsx
+++ b/src/components/ui/ScreenHeader.tsx
@@ -57,13 +57,14 @@ type ScreenHeaderProps =
trailing?: ReactNode;
};
-// Figma: Top Navigation/Resource/Contents — 화면마다 따로 조립하던 헤더를 여기 하나로 모았다.
-// WDS `TopNavigation`을 감싸는 얇은 조합 레이어일 뿐, 내부 스타일은 오버라이드하지 않는다
-// (component-convention.md "WDS 컴포넌트 내부를 임의로 오버라이드하지 않는다").
+// variant="display"(기본값, 빌릴게/홈)는 더 이상 WDS `Top Navigation/Resource/Contents`가 아니다 —
+// Figma가 별도 Stream 로컬 컴포넌트(nodeId 1765:71193 "Top Navigation")로 바뀌었다: 세로 패딩
+// 12px(기존 WDS display variant는 16px 고정이라 오버라이드 불가) + Title 3/Bold(32px)가 정확히
+// 들어가서 총 56px. leading은 이 패턴에서 쓴 적이 없어 그대로 받지 않는다.
//
-// variant="display"(기본값)에서는 leading을 받지 않는다 — WDS 쪽 스타일 자체가 display일 때
-// leading/trailing 포지셔닝(topNavigationLeftIconStyle/RightIconStyle)을 안 줘서 레이아웃이
-// 깨진다. leading이 필요한 화면(뒤로가기·닫기 버튼 등)은 variant="normal"을 쓴다.
+// variant="normal"(모달형 닫기 버튼 등, leading 필요)은 아직 WDS `TopNavigation`을 그대로 쓴다 —
+// Figma 쪽 해당 패턴은 안 바뀌었다(component-convention.md "WDS 컴포넌트 내부를 임의로
+// 오버라이드하지 않는다" 원칙 유지).
//
// title이 문자열이면 그대로 렌더링하고, { options, activeIndex } 형태(활성 상태가 있는 경우)면
// 게시판류의 토글형 2단 타이틀로 렌더링한다.
@@ -72,18 +73,46 @@ type ScreenHeaderProps =
// docs/plans/unified-screen-header.md 참고. 화면이 실제로 생기면 그때 추가한다.
function ScreenHeader(props: ScreenHeaderProps) {
const { title, trailing } = props;
- const leading = props.variant === "normal" ? props.leading : undefined;
+
+ if (props.variant === "normal") {
+ return (
+
+ {title !== undefined &&
+ (isToggleTitle(title) ? (
+
+ ) : (
+ title
+ ))}
+
+ );
+ }
return (
-
- {title !== undefined &&
- (isToggleTitle(title) ? : title)}
-
+
+
+ {title !== undefined &&
+ (isToggleTitle(title) ? (
+
+ ) : (
+
+ {title}
+
+ ))}
+
+ {trailing !== undefined && (
+
{trailing}
+ )}
+
);
}