From ad195850464eaa56f902546227b114e122346903 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: Wed, 9 Sep 2026 13:05:17 +0900 Subject: [PATCH 01/23] =?UTF-8?q?refactor:=20Bottom=20Nav=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=20=ED=83=AD=EC=9D=84=20=ED=99=94=EB=A9=B4=20state=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=8C=8C=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TEAM-BEAT/BEAT-Client의 Layout 컴포넌트 + react-router 패턴을 참고했다. ScreenLayout이 useLocation으로 현재 경로에서 활성 탭을 계산하고 useNavigate로 직접 이동시켜서, 화면마다 중복되던 bottomNavValue state와 handleBottomNavValueChange 핸들러를 제거한다. URL이 유일한 진실의 원천이 된다 --- src/components/ui/ScreenLayout.tsx | 33 +++++++++++++++++------- src/features/home/HomeScreen.tsx | 16 +----------- src/features/rental/RentalListScreen.tsx | 14 ---------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index 8fc52b6..c49dde5 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -1,30 +1,45 @@ import type { ReactNode } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; import BottomNav, { type BottomNavValue } from "@/components/ui/BottomNav"; interface ScreenLayoutProps { header: ReactNode; children: ReactNode; - bottomNavValue: BottomNavValue; - onBottomNavValueChange: (value: BottomNavValue) => void; +} + +// Bottom Nav 탭 ↔ 라우트 경로 매핑. 화면이 늘어나면 여기에 추가한다. +const BOTTOM_NAV_PATHS: Record = { + board: "/board", + event: "/event", + home: "/", + rental: "/rental", +}; + +function getBottomNavValueFromPath(pathname: string): BottomNavValue { + const matched = ( + Object.entries(BOTTOM_NAV_PATHS) as [BottomNavValue, string][] + ).find(([, path]) => path === pathname); + return matched?.[0] ?? "home"; } // 홈/행사/게시판/빌릴게 등 모든 화면이 공유하는 뼈대 — 고정 크기(375x812) 프레임 안에서 // header·Bottom Nav는 고정, 본문만 스크롤된다. Top Navigation·필터·리스트 등 화면마다 // 다른 내용은 header/children으로 각 화면이 채운다(공통인 건 프레임 비율과 Bottom Nav뿐). -function ScreenLayout({ - header, - children, - bottomNavValue, - onBottomNavValueChange, -}: ScreenLayoutProps) { +// Bottom Nav의 활성 탭은 화면마다 state로 들고 있지 않고 현재 라우트에서 파생시킨다 — +// URL이 진실의 원천이라, 화면마다 중복되던 상태·네비게이션 핸들러가 필요 없어진다. +function ScreenLayout({ header, children }: ScreenLayoutProps) { + const location = useLocation(); + const navigate = useNavigate(); + const bottomNavValue = getBottomNavValueFromPath(location.pathname); + return (
{header}
{children}
navigate(BOTTOM_NAV_PATHS[value])} value={bottomNavValue} />
diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 7a89447..2438785 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -1,30 +1,16 @@ -import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; -import type { BottomNavValue } from "@/components/ui/BottomNav"; import ScreenLayout from "@/components/ui/ScreenLayout"; // 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다. function HomeScreen() { - const [bottomNavValue, setBottomNavValue] = useState("home"); - const navigate = useNavigate(); - - const handleBottomNavValueChange = (value: BottomNavValue) => { - setBottomNavValue(value); - if (value === "rental") { - navigate("/rental"); - } - }; - return (

STREAM

} - onBottomNavValueChange={handleBottomNavValueChange} >

diff --git a/src/features/rental/RentalListScreen.tsx b/src/features/rental/RentalListScreen.tsx index 25e9086..80a88f9 100644 --- a/src/features/rental/RentalListScreen.tsx +++ b/src/features/rental/RentalListScreen.tsx @@ -6,9 +6,7 @@ import { } from "@wanteddev/wds"; import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import type { BottomNavValue } from "@/components/ui/BottomNav"; import ScreenLayout from "@/components/ui/ScreenLayout"; import RentalCategoryFilter from "@/features/rental/components/RentalCategoryFilter"; import RentalItemCard from "@/features/rental/components/RentalItemCard"; @@ -18,20 +16,9 @@ import { RENTAL_ITEMS } from "@/features/rental/constants/rentalItems"; function RentalListScreen() { const [tab, setTab] = useState("rent"); const [category, setCategory] = useState("전체"); - const [bottomNavValue, setBottomNavValue] = - useState("rental"); - const navigate = useNavigate(); - - const handleBottomNavValueChange = (value: BottomNavValue) => { - setBottomNavValue(value); - if (value === "home") { - navigate("/"); - } - }; return ( } - onBottomNavValueChange={handleBottomNavValueChange} >

From 664738136947a4f1d175d0dc02a0f5ee52d2b471 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: Wed, 9 Sep 2026 13:16:23 +0900 Subject: [PATCH 02/23] =?UTF-8?q?refactor:=20ScreenLayout=EC=9D=84=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EB=A0=88=EB=B2=A8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99,=20useScreenHeader=20=ED=9B=85=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 화면이 을 직접 감싸는 방식은 새 화면을 추가할 때 감싸는 걸 깜빡하면 레이아웃이 화면마다 들쭉날쭉해질 수 있었다. App.tsx에서 ScreenLayout을 부모 route로 두고 화면들을 자식 route(Outlet)로 넣어서, 그 밑 화면은 구조적으로 무조건 같은 뼈대(375x812+Bottom Nav)를 받도록 바꾼다. 화면마다 다른 헤더(Top Navigation 등)는 useScreenHeader 훅으로 ScreenLayout에 등록한다(Context 기반, 새 의존성 없음) --- src/app/App.tsx | 7 ++- src/components/ui/ScreenLayout.tsx | 44 ++++++++--------- src/components/ui/screenHeaderContext.ts | 10 ++++ src/components/ui/useScreenHeader.ts | 22 +++++++++ src/features/home/HomeScreen.tsx | 32 ++++++------- src/features/rental/RentalListScreen.tsx | 60 ++++++++++++------------ 6 files changed, 105 insertions(+), 70 deletions(-) create mode 100644 src/components/ui/screenHeaderContext.ts create mode 100644 src/components/ui/useScreenHeader.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 8dd6739..1e2a4b3 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,6 @@ import { Route, Routes } from "react-router-dom"; +import ScreenLayout from "@/components/ui/ScreenLayout"; import HomeScreen from "@/features/home/HomeScreen"; import RentalListScreen from "@/features/rental/RentalListScreen"; @@ -8,8 +9,10 @@ function App() { return (
- } path="/" /> - } path="/rental" /> + }> + } path="/" /> + } path="/rental" /> +
); diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index c49dde5..05e3c10 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -1,12 +1,9 @@ import type { ReactNode } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; import BottomNav, { type BottomNavValue } from "@/components/ui/BottomNav"; - -interface ScreenLayoutProps { - header: ReactNode; - children: ReactNode; -} +import { ScreenHeaderContext } from "@/components/ui/screenHeaderContext"; // Bottom Nav 탭 ↔ 라우트 경로 매핑. 화면이 늘어나면 여기에 추가한다. const BOTTOM_NAV_PATHS: Record = { @@ -23,27 +20,32 @@ function getBottomNavValueFromPath(pathname: string): BottomNavValue { return matched?.[0] ?? "home"; } -// 홈/행사/게시판/빌릴게 등 모든 화면이 공유하는 뼈대 — 고정 크기(375x812) 프레임 안에서 -// header·Bottom Nav는 고정, 본문만 스크롤된다. Top Navigation·필터·리스트 등 화면마다 -// 다른 내용은 header/children으로 각 화면이 채운다(공통인 건 프레임 비율과 Bottom Nav뿐). -// Bottom Nav의 활성 탭은 화면마다 state로 들고 있지 않고 현재 라우트에서 파생시킨다 — -// URL이 진실의 원천이라, 화면마다 중복되던 상태·네비게이션 핸들러가 필요 없어진다. -function ScreenLayout({ header, children }: ScreenLayoutProps) { +// 홈/행사/게시판/빌릴게 등 Bottom Nav가 있는 화면 전용 라우트 레이아웃 — App.tsx에서 부모 +// route로 두고 화면들을 자식 route(Outlet)로 넣는다. 화면이 직접 이 컴포넌트를 임포트해서 +// 감쌀 필요가 없어서, "일부 화면만 감싸는 걸 깜빡"하는 불일치가 구조적으로 불가능해진다. +// 화면마다 다른 헤더(Top Navigation 등)는 useScreenHeader 훅으로 이 레이아웃에 등록한다. +// Bottom Nav의 활성 탭도 화면 state가 아니라 현재 라우트에서 파생시킨다. +function ScreenLayout() { + const [header, setHeader] = useState(null); const location = useLocation(); const navigate = useNavigate(); const bottomNavValue = getBottomNavValueFromPath(location.pathname); return ( -
-
{header}
-
{children}
-
- navigate(BOTTOM_NAV_PATHS[value])} - value={bottomNavValue} - /> + +
+
{header}
+
+ +
+
+ navigate(BOTTOM_NAV_PATHS[value])} + value={bottomNavValue} + /> +
-
+ ); } diff --git a/src/components/ui/screenHeaderContext.ts b/src/components/ui/screenHeaderContext.ts new file mode 100644 index 0000000..737c2f6 --- /dev/null +++ b/src/components/ui/screenHeaderContext.ts @@ -0,0 +1,10 @@ +import { + createContext, + type Dispatch, + type ReactNode, + type SetStateAction, +} from "react"; + +export type SetScreenHeader = Dispatch>; + +export const ScreenHeaderContext = createContext(null); diff --git a/src/components/ui/useScreenHeader.ts b/src/components/ui/useScreenHeader.ts new file mode 100644 index 0000000..16a5cb1 --- /dev/null +++ b/src/components/ui/useScreenHeader.ts @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; +import { useContext, useEffect } from "react"; + +import { ScreenHeaderContext } from "@/components/ui/screenHeaderContext"; + +// 화면(라우트)이 자신의 헤더(Top Navigation 등)를 ScreenLayout의 header 슬롯에 등록한다. +// ScreenLayout이 라우터에서 부모 route로 화면을 감싸기 때문에, 화면은 을 +// 직접 감쌀 필요 없이 이 훅만 호출하면 된다 — 감싸는 걸 깜빡하는 불일치가 구조적으로 없어진다. +export function useScreenHeader(header: ReactNode) { + const setHeader = useContext(ScreenHeaderContext); + + if (!setHeader) { + throw new Error( + "useScreenHeader는 ScreenLayout의 하위 라우트에서만 쓸 수 있다", + ); + } + + useEffect(() => { + setHeader(header); + return () => setHeader(null); + }, [header, setHeader]); +} diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 2438785..8af96d1 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -1,26 +1,24 @@ import { Link } from "react-router-dom"; -import ScreenLayout from "@/components/ui/ScreenLayout"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; // 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다. function HomeScreen() { + useScreenHeader( +
+

STREAM

+
, + ); + return ( - -

STREAM

-
- } - > -
-

- 홈 화면은 아직 준비 중이에요 -

- - 빌릴게 화면 보기 - -
-
+
+

+ 홈 화면은 아직 준비 중이에요 +

+ + 빌릴게 화면 보기 + +
); } diff --git a/src/features/rental/RentalListScreen.tsx b/src/features/rental/RentalListScreen.tsx index 80a88f9..58b10d8 100644 --- a/src/features/rental/RentalListScreen.tsx +++ b/src/features/rental/RentalListScreen.tsx @@ -7,7 +7,7 @@ import { import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { useState } from "react"; -import ScreenLayout from "@/components/ui/ScreenLayout"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; import RentalCategoryFilter from "@/features/rental/components/RentalCategoryFilter"; import RentalItemCard from "@/features/rental/components/RentalItemCard"; import { RENTAL_ITEMS } from "@/features/rental/constants/rentalItems"; @@ -17,36 +17,36 @@ function RentalListScreen() { const [tab, setTab] = useState("rent"); const [category, setCategory] = useState("전체"); - return ( - - - 대여 - 반납 - -
- } - trailingContent={ - <> - - - - - - - - } - variant="display" - > - 빌릴게 - + useScreenHeader( + // background 기본값(true)은 iOS 반투명 스타일이라 뒤 배경이 비쳐 보인다. Figma는 별도 배경 없이 화면 배경을 그대로 쓴다. + + + 대여 + 반납 + +
+ } + trailingContent={ + <> + + + + + + + } + variant="display" > + 빌릴게 + , + ); + + return ( + <>
@@ -67,7 +67,7 @@ function RentalListScreen() { 반납 화면은 아직 준비 중이에요 )} - + ); } From 873015607b845d6866d9d167671dc53e2f7bb67b 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: Wed, 9 Sep 2026 15:15:24 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20ScreenHeader=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Figma에서 행사·빌릴게 화면을 대조해보니 Top Navigation의 타이틀+검색/알림 아이콘 부분은 화면마다 완전히 동일한 공통 패턴이었다(세그먼트 토글 같은 Tool 슬롯은 화면마다 값·동작이 달라 제외). ScreenHeader로 분리해 재사용하고, HomeScreen도 이걸 쓰도록 정리한다 --- src/components/ui/ScreenHeader.tsx | 35 ++++++++++++++++++++++++++++++ src/features/home/HomeScreen.tsx | 7 ++---- 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/ScreenHeader.tsx diff --git a/src/components/ui/ScreenHeader.tsx b/src/components/ui/ScreenHeader.tsx new file mode 100644 index 0000000..18e3877 --- /dev/null +++ b/src/components/ui/ScreenHeader.tsx @@ -0,0 +1,35 @@ +import { TopNavigation, TopNavigationButton } from "@wanteddev/wds"; +import { IconBell, IconSearch } from "@wanteddev/wds-icon"; + +interface ScreenHeaderProps { + title: string; +} + +// Figma: Top Navigation/Resource/Contents의 타이틀 + 검색/알림 아이콘 부분 — 행사·빌릴게 등 +// 여러 화면에서 완전히 동일하게 반복되는 진짜 공통 패턴이라 재사용 컴포넌트로 뺐다. +// "Tool" 슬롯(세그먼트 토글 등)은 화면마다 값·동작이 달라서(대여/반납 vs 행사/신청내역) +// 여기 포함하지 않고 각 화면이 자기 본문에서 직접 그린다. +// background 기본값(true)은 iOS 반투명 스타일이라 뒤 배경이 비쳐 보인다. Figma는 별도 배경 없이 +// 화면 배경을 그대로 쓴다. +function ScreenHeader({ title }: ScreenHeaderProps) { + return ( + + + + + + + + + } + variant="display" + > + {title} + + ); +} + +export default ScreenHeader; diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 8af96d1..8eb1ff7 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -1,14 +1,11 @@ import { Link } from "react-router-dom"; +import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; // 홈 화면 콘텐츠는 아직 없어서, 라우팅이 실제로 동작하는지 확인할 placeholder만 둔다. function HomeScreen() { - useScreenHeader( -
-

STREAM

-
, - ); + useScreenHeader(); return (
From 5655e5161f0295cc12424ccdfaba5f45bbae339e 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: Wed, 9 Sep 2026 15:15:33 +0900 Subject: [PATCH 04/23] =?UTF-8?q?refactor:=20=EB=8C=80=EC=97=AC/=EB=B0=98?= =?UTF-8?q?=EB=82=A9=20=ED=86=A0=EA=B8=80=EC=9D=84=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=99=94=EB=A9=B4=20=EC=9A=94=EC=86=8C?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC,=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95+=EB=AA=A9=EB=A1=9D=EB=A7=8C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대여/반납 세그먼트 토글은 화면마다 다른 값/동작을 가지는 페이지 전용 요소라 useScreenHeader가 아니라 RentalListScreen 본문에서 직접 그리도록 옮긴다. 토글+카테고리 필터는 고정하고 물품 목록만 스크롤되도록 화면 내부 레이아웃을 분리했다(ScreenLayout의 Outlet 래퍼는 overflow-hidden으로 바꿔 스크롤 처리를 각 화면에 위임). 무한 스크롤 목록이라 스크롤바는 scrollbar-hidden 유틸리티로 숨긴다 --- src/components/ui/ScreenLayout.tsx | 3 +- src/features/rental/RentalListScreen.tsx | 86 +++++++++--------------- src/index.css | 10 +++ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index 05e3c10..cc8fbf2 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -35,7 +35,8 @@ function ScreenLayout() {
{header}
-
+ {/* 스크롤 처리는 각 화면이 스스로 결정한다(예: 상단 토글/필터는 고정하고 목록만 스크롤) */} +
diff --git a/src/features/rental/RentalListScreen.tsx b/src/features/rental/RentalListScreen.tsx index 58b10d8..98247b3 100644 --- a/src/features/rental/RentalListScreen.tsx +++ b/src/features/rental/RentalListScreen.tsx @@ -1,12 +1,7 @@ -import { - SegmentedControl, - SegmentedControlItem, - TopNavigation, - TopNavigationButton, -} from "@wanteddev/wds"; -import { IconBell, IconSearch } from "@wanteddev/wds-icon"; +import { SegmentedControl, SegmentedControlItem } from "@wanteddev/wds"; import { useState } from "react"; +import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; import RentalCategoryFilter from "@/features/rental/components/RentalCategoryFilter"; import RentalItemCard from "@/features/rental/components/RentalItemCard"; @@ -17,57 +12,42 @@ function RentalListScreen() { const [tab, setTab] = useState("rent"); const [category, setCategory] = useState("전체"); - useScreenHeader( - // background 기본값(true)은 iOS 반투명 스타일이라 뒤 배경이 비쳐 보인다. Figma는 별도 배경 없이 화면 배경을 그대로 쓴다. - - - 대여 - 반납 - -
- } - trailingContent={ - <> - - - - - - - - } - variant="display" - > - 빌릴게 - , - ); + useScreenHeader(); return ( - <> -
+
+ {/* 대여/반납 토글 + 카테고리 필터는 화면마다 값·동작이 달라 헤더가 아니라 화면이 직접 그린다. + 목록만 스크롤되도록 여기는 고정(shrink-0)한다. */} +
+ + 대여 + 반납 + +
+ +
- {tab === "rent" ? ( -
- {RENTAL_ITEMS.map((item) => ( - - ))} -
- ) : ( -
- 반납 화면은 아직 준비 중이에요 -
- )} - +
+ {tab === "rent" ? ( +
+ {RENTAL_ITEMS.map((item) => ( + + ))} +
+ ) : ( +
+ 반납 화면은 아직 준비 중이에요 +
+ )} +
+
); } diff --git a/src/index.css b/src/index.css index 509d881..2cd04fe 100644 --- a/src/index.css +++ b/src/index.css @@ -22,3 +22,13 @@ /* WDS 시맨틱 세트에 없는 값(Figma "Icons/Primary") — 홈 인디케이터 전용, 값 자체는 Figma에서 확인됨 */ --color-icons-primary: #0f172a; } + +/* 무한 스크롤 목록 등 스크롤은 동작하되 스크롤바는 안 보이게 할 때 쓰는 재사용 유틸리티 */ +@utility scrollbar-hidden { + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } +} From 983e481da961cf1b61ce2219e0ba09dfa86c5058 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: Wed, 9 Sep 2026 16:49:50 +0900 Subject: [PATCH 05/23] =?UTF-8?q?docs:=20=EC=9A=A9=EC=96=B4=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드 엔드포인트 네이밍과 프론트 도메인 용어(라우트 경로, features 폴더명 등)를 통일하기 위한 용어 사전을 만들고, coding-style.md 네이밍 규칙에서 참조하게 한다 --- docs/conventions/coding-style.md | 1 + docs/conventions/terminology.md | 38 +++++++++++++++++++ .../8pin-charger.svg | 0 .../alcohol-swab.svg | 0 .../band-aid.svg | 0 .../curling-iron.svg | 0 .../default.svg | 0 .../eye-drops.svg | 0 .../hair-dryer.svg | 0 .../laptop-charger.svg | 0 .../{rental-items => bililge-items}/mask.svg | 0 .../ointment.svg | 0 .../pain-relief-patch.svg | 0 .../{rental-items => bililge-items}/pill.svg | 0 .../power-bank.svg | 0 .../sanitary-pad.svg | 0 .../umbrella.svg | 0 .../usb-c-charger.svg | 0 ...ntal-selected.svg => bililge-selected.svg} | 0 .../BililgeListScreen.tsx} | 0 .../components/BililgeCategoryFilter.tsx} | 0 .../components/BililgeItemCard.tsx} | 0 .../constants/bililgeItems.ts} | 0 23 files changed, 39 insertions(+) create mode 100644 docs/conventions/terminology.md rename src/assets/icons/{rental-items => bililge-items}/8pin-charger.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/alcohol-swab.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/band-aid.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/curling-iron.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/default.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/eye-drops.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/hair-dryer.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/laptop-charger.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/mask.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/ointment.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/pain-relief-patch.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/pill.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/power-bank.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/sanitary-pad.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/umbrella.svg (100%) rename src/assets/icons/{rental-items => bililge-items}/usb-c-charger.svg (100%) rename src/assets/icons/bottom-nav/{rental-selected.svg => bililge-selected.svg} (100%) rename src/features/{rental/RentalListScreen.tsx => bililge/BililgeListScreen.tsx} (100%) rename src/features/{rental/components/RentalCategoryFilter.tsx => bililge/components/BililgeCategoryFilter.tsx} (100%) rename src/features/{rental/components/RentalItemCard.tsx => bililge/components/BililgeItemCard.tsx} (100%) rename src/features/{rental/constants/rentalItems.ts => bililge/constants/bililgeItems.ts} (100%) diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index 32a4b6f..3b04a4d 100644 --- a/docs/conventions/coding-style.md +++ b/docs/conventions/coding-style.md @@ -16,6 +16,7 @@ - 훅: `useXxx`. 일반 함수/변수: camelCase. 상수: `UPPER_SNAKE_CASE`. - 불리언: `is` / `has` / `should` 접두사. - 폴더: 도메인명 소문자 또는 kebab-case. +- **도메인 이름은 임의로 번역/축약하지 않는다.** `features/<기능>/` 폴더명, 라우트 경로, 도메인 접두사가 붙는 컴포넌트/타입명은 `docs/conventions/terminology.md`(용어 사전)의 코드 용어를 그대로 쓴다 — 백엔드 API 엔드포인트 네이밍과 맞추기 위함이다. 사전에 없는 새 도메인이면 먼저 그 문서에 추가한 뒤 코드에 반영한다. ## 폴더 구조 diff --git a/docs/conventions/terminology.md b/docs/conventions/terminology.md new file mode 100644 index 0000000..3c1f89a --- /dev/null +++ b/docs/conventions/terminology.md @@ -0,0 +1,38 @@ +# 용어 사전 (한글 도메인명 ↔ 코드 용어) + +> 코드에서 도메인 이름을 임의로 짓지 않기 위한 사전. 백엔드 API 엔드포인트 네이밍과 프론트 코드(라우트 경로, `src/features/` 폴더명, 컴포넌트/타입 접두사 등)가 같은 용어를 쓰게 맞춘다. + +## 원칙 + +- 새 기능/화면을 만들 때 도메인 이름이 필요하면, 영어로 대충 번역하지 말고 **반드시 이 표부터 확인한다.** +- 여기 없는 새 도메인이 생기면 백엔드와 협의해서 엔드포인트 네이밍을 먼저 정하고, 그걸 이 표에 추가한 뒤 코드에 반영한다 — 프론트에서 먼저 임의로 정하지 않는다. +- 표의 "코드 용어"는 백엔드 엔드포인트 세그먼트(`/api/{코드 용어}/...`)와 동일한 문자열이다. 프론트에서는 아래 "적용 범위"에 그대로 쓴다. + +## 사전 + +| 한글 도메인명 | 코드 용어 | +| --- | --- | +| 공통 | `auth` | +| 사용자 | `users` | +| 게시판(공지) | `notices` | +| 게시판(열린피드백) | `feedbacks` | +| 빌릴게 | `bililge` | +| 사물함 | `lockers` | +| 슬랑제 | `seulrangjes` | +| 아카이빙 | `archives` | +| 챗봇 | `chat` | +| 파일 | `files` | +| 행사(event) | `events` | +| 회비 납부 | `fee` | + +## 적용 범위 + +- 백엔드 API 엔드포인트 경로: `/api/{코드 용어}/...` +- 프론트 라우트 경로: `/{코드 용어}` +- `src/features/{코드 용어}/` 폴더명 +- 그 안의 컴포넌트/타입/상수 이름의 도메인 접두사 — 코드 용어를 PascalCase로 바꿔서 쓴다 (예: `bililge` → `Bililge...`, `bililgeItems` 같은 camelCase 변수명도 동일) +- Figma 노드/화면 이름은 그대로 한글을 쓴다(디자이너와의 소통 채널이라 번역하지 않음) — 코드 파일 최상단의 `// Figma: ...` 주석에서만 한글 화면명을 남긴다. + +## 반영 사례 + +- "빌릴게" 기능: 처음엔 `rental`로 임의 명명했다가, 이 사전을 도입하면서 `src/features/rental/` → `src/features/bililge/`로 전체 리네이밍했다(#18). diff --git a/src/assets/icons/rental-items/8pin-charger.svg b/src/assets/icons/bililge-items/8pin-charger.svg similarity index 100% rename from src/assets/icons/rental-items/8pin-charger.svg rename to src/assets/icons/bililge-items/8pin-charger.svg diff --git a/src/assets/icons/rental-items/alcohol-swab.svg b/src/assets/icons/bililge-items/alcohol-swab.svg similarity index 100% rename from src/assets/icons/rental-items/alcohol-swab.svg rename to src/assets/icons/bililge-items/alcohol-swab.svg diff --git a/src/assets/icons/rental-items/band-aid.svg b/src/assets/icons/bililge-items/band-aid.svg similarity index 100% rename from src/assets/icons/rental-items/band-aid.svg rename to src/assets/icons/bililge-items/band-aid.svg diff --git a/src/assets/icons/rental-items/curling-iron.svg b/src/assets/icons/bililge-items/curling-iron.svg similarity index 100% rename from src/assets/icons/rental-items/curling-iron.svg rename to src/assets/icons/bililge-items/curling-iron.svg diff --git a/src/assets/icons/rental-items/default.svg b/src/assets/icons/bililge-items/default.svg similarity index 100% rename from src/assets/icons/rental-items/default.svg rename to src/assets/icons/bililge-items/default.svg diff --git a/src/assets/icons/rental-items/eye-drops.svg b/src/assets/icons/bililge-items/eye-drops.svg similarity index 100% rename from src/assets/icons/rental-items/eye-drops.svg rename to src/assets/icons/bililge-items/eye-drops.svg diff --git a/src/assets/icons/rental-items/hair-dryer.svg b/src/assets/icons/bililge-items/hair-dryer.svg similarity index 100% rename from src/assets/icons/rental-items/hair-dryer.svg rename to src/assets/icons/bililge-items/hair-dryer.svg diff --git a/src/assets/icons/rental-items/laptop-charger.svg b/src/assets/icons/bililge-items/laptop-charger.svg similarity index 100% rename from src/assets/icons/rental-items/laptop-charger.svg rename to src/assets/icons/bililge-items/laptop-charger.svg diff --git a/src/assets/icons/rental-items/mask.svg b/src/assets/icons/bililge-items/mask.svg similarity index 100% rename from src/assets/icons/rental-items/mask.svg rename to src/assets/icons/bililge-items/mask.svg diff --git a/src/assets/icons/rental-items/ointment.svg b/src/assets/icons/bililge-items/ointment.svg similarity index 100% rename from src/assets/icons/rental-items/ointment.svg rename to src/assets/icons/bililge-items/ointment.svg diff --git a/src/assets/icons/rental-items/pain-relief-patch.svg b/src/assets/icons/bililge-items/pain-relief-patch.svg similarity index 100% rename from src/assets/icons/rental-items/pain-relief-patch.svg rename to src/assets/icons/bililge-items/pain-relief-patch.svg diff --git a/src/assets/icons/rental-items/pill.svg b/src/assets/icons/bililge-items/pill.svg similarity index 100% rename from src/assets/icons/rental-items/pill.svg rename to src/assets/icons/bililge-items/pill.svg diff --git a/src/assets/icons/rental-items/power-bank.svg b/src/assets/icons/bililge-items/power-bank.svg similarity index 100% rename from src/assets/icons/rental-items/power-bank.svg rename to src/assets/icons/bililge-items/power-bank.svg diff --git a/src/assets/icons/rental-items/sanitary-pad.svg b/src/assets/icons/bililge-items/sanitary-pad.svg similarity index 100% rename from src/assets/icons/rental-items/sanitary-pad.svg rename to src/assets/icons/bililge-items/sanitary-pad.svg diff --git a/src/assets/icons/rental-items/umbrella.svg b/src/assets/icons/bililge-items/umbrella.svg similarity index 100% rename from src/assets/icons/rental-items/umbrella.svg rename to src/assets/icons/bililge-items/umbrella.svg diff --git a/src/assets/icons/rental-items/usb-c-charger.svg b/src/assets/icons/bililge-items/usb-c-charger.svg similarity index 100% rename from src/assets/icons/rental-items/usb-c-charger.svg rename to src/assets/icons/bililge-items/usb-c-charger.svg diff --git a/src/assets/icons/bottom-nav/rental-selected.svg b/src/assets/icons/bottom-nav/bililge-selected.svg similarity index 100% rename from src/assets/icons/bottom-nav/rental-selected.svg rename to src/assets/icons/bottom-nav/bililge-selected.svg diff --git a/src/features/rental/RentalListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx similarity index 100% rename from src/features/rental/RentalListScreen.tsx rename to src/features/bililge/BililgeListScreen.tsx diff --git a/src/features/rental/components/RentalCategoryFilter.tsx b/src/features/bililge/components/BililgeCategoryFilter.tsx similarity index 100% rename from src/features/rental/components/RentalCategoryFilter.tsx rename to src/features/bililge/components/BililgeCategoryFilter.tsx diff --git a/src/features/rental/components/RentalItemCard.tsx b/src/features/bililge/components/BililgeItemCard.tsx similarity index 100% rename from src/features/rental/components/RentalItemCard.tsx rename to src/features/bililge/components/BililgeItemCard.tsx diff --git a/src/features/rental/constants/rentalItems.ts b/src/features/bililge/constants/bililgeItems.ts similarity index 100% rename from src/features/rental/constants/rentalItems.ts rename to src/features/bililge/constants/bililgeItems.ts From bf4693b67ebb312fe6cdf07fa950541051aa9ced 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: Wed, 9 Sep 2026 16:50:03 +0900 Subject: [PATCH 06/23] =?UTF-8?q?refactor:=20rental=20=E2=86=92=20bililge?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 용어 사전(docs/conventions/terminology.md)에 맞춰 '빌릴게' 도메인의 rental 네이밍을 bililge로 전부 바꾼다: features/rental → features/bililge, 컴포넌트/타입/상수명(RentalXxx → BililgeXxx), 라우트 경로(/rental → /bililge), BottomNavValue의 'rental' → 'bililge', asset 폴더/파일명까지. Figma 노드 실제 이름을 그대로 옮긴 주석(예: 'Rental Item Card')은 용어 사전 규칙대로 그대로 둔다 --- src/app/App.tsx | 4 +-- src/components/ui/BottomNav.tsx | 8 ++--- src/components/ui/ScreenLayout.tsx | 2 +- src/features/bililge/BililgeListScreen.tsx | 16 ++++----- .../components/BililgeCategoryFilter.tsx | 13 ++++--- .../bililge/components/BililgeItemCard.tsx | 8 ++--- .../bililge/constants/bililgeItems.ts | 34 +++++++++---------- src/features/home/HomeScreen.tsx | 2 +- 8 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 1e2a4b3..616d406 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,8 +1,8 @@ import { Route, Routes } from "react-router-dom"; import ScreenLayout from "@/components/ui/ScreenLayout"; +import BililgeListScreen from "@/features/bililge/BililgeListScreen"; import HomeScreen from "@/features/home/HomeScreen"; -import RentalListScreen from "@/features/rental/RentalListScreen"; // 데스크톱에서 보기 좋게 아이폰 화면 크기로 가운데 정렬만 해준다. function App() { @@ -11,7 +11,7 @@ function App() { }> } path="/" /> - } path="/rental" /> + } path="/bililge" />
diff --git a/src/components/ui/BottomNav.tsx b/src/components/ui/BottomNav.tsx index 8bfe241..e9f2669 100644 --- a/src/components/ui/BottomNav.tsx +++ b/src/components/ui/BottomNav.tsx @@ -5,12 +5,12 @@ import { IconTicket, } from "@wanteddev/wds-icon"; +import bililgeSelected from "@/assets/icons/bottom-nav/bililge-selected.svg"; import boardSelected from "@/assets/icons/bottom-nav/board-selected.svg"; import eventSelected from "@/assets/icons/bottom-nav/event-selected.svg"; import homeSelected from "@/assets/icons/bottom-nav/home-selected.svg"; -import rentalSelected from "@/assets/icons/bottom-nav/rental-selected.svg"; -export type BottomNavValue = "home" | "event" | "board" | "rental"; +export type BottomNavValue = "home" | "event" | "board" | "bililge"; interface BottomNavProps { value: BottomNavValue; @@ -47,8 +47,8 @@ const TABS: BottomNavTab[] = [ { NormalIcon: IconStorage, label: "빌릴게", - selectedIcon: rentalSelected, - value: "rental", + selectedIcon: bililgeSelected, + value: "bililge", }, ]; diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index cc8fbf2..704c02e 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -7,10 +7,10 @@ import { ScreenHeaderContext } from "@/components/ui/screenHeaderContext"; // Bottom Nav 탭 ↔ 라우트 경로 매핑. 화면이 늘어나면 여기에 추가한다. const BOTTOM_NAV_PATHS: Record = { + bililge: "/bililge", board: "/board", event: "/event", home: "/", - rental: "/rental", }; function getBottomNavValueFromPath(pathname: string): BottomNavValue { diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index 98247b3..f28920a 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -3,12 +3,12 @@ import { useState } from "react"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; -import RentalCategoryFilter from "@/features/rental/components/RentalCategoryFilter"; -import RentalItemCard from "@/features/rental/components/RentalItemCard"; -import { RENTAL_ITEMS } from "@/features/rental/constants/rentalItems"; +import BililgeCategoryFilter from "@/features/bililge/components/BililgeCategoryFilter"; +import BililgeItemCard from "@/features/bililge/components/BililgeItemCard"; +import { BILILGE_ITEMS } from "@/features/bililge/constants/bililgeItems"; // Figma: 빌릴게 (nodeId 1243:73331) -function RentalListScreen() { +function BililgeListScreen() { const [tab, setTab] = useState("rent"); const [category, setCategory] = useState("전체"); @@ -26,14 +26,14 @@ function RentalListScreen() {
- +
{tab === "rent" ? (
- {RENTAL_ITEMS.map((item) => ( - ( + void; } // Figma: 빌릴게 Filter Row (nodeId 1243:73337) — WDS Chip이 아니라 Stream 로컬 커스텀 칩. // 카테고리·물품 매핑 API가 아직 없어 필터링 없이 선택 상태만 표시한다. -function RentalCategoryFilter({ value, onChange }: RentalCategoryFilterProps) { +function BililgeCategoryFilter({ + value, + onChange, +}: BililgeCategoryFilterProps) { return (
- {RENTAL_CATEGORIES.map((category) => { + {BILILGE_CATEGORIES.map((category) => { const active = category === value; return (
From f5c3691ccd6a51bdea0651a3f6bbabde619fcc27 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: Wed, 9 Sep 2026 17:07:20 +0900 Subject: [PATCH 07/23] =?UTF-8?q?chore:=20main=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=8A=A4=ED=82=AC=C2=B7=EC=BB=A8=EB=B2=A4=EC=85=98=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/component/SKILL.md | 55 +++++++++++ .claude/skills/figma-check/SKILL.md | 76 +++++++++++++++ docs/conventions/component-convention.md | 67 +++++++++++++ docs/conventions/wds-component-usage.md | 117 +++++++++++++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 .claude/skills/component/SKILL.md create mode 100644 .claude/skills/figma-check/SKILL.md create mode 100644 docs/conventions/component-convention.md create mode 100644 docs/conventions/wds-component-usage.md diff --git a/.claude/skills/component/SKILL.md b/.claude/skills/component/SKILL.md new file mode 100644 index 0000000..535655d --- /dev/null +++ b/.claude/skills/component/SKILL.md @@ -0,0 +1,55 @@ +--- +name: component +description: Figma Stream 파일의 노드를 코드 컴포넌트로 옮긴다. WDS(원티드 디자인 시스템) 컴포넌트로 확인되면 @wanteddev/wds를 import해서 재사용하고, Stream 고유 UI만 새로 만든다. /component 로 호출한다. +--- + +# /component — Figma → 코드 컴포넌트 변환 워크플로우 + +Stream Figma 파일의 노드를 받아서, WDS로 확인된 부분은 `@wanteddev/wds`를 import해 재사용하고 Stream 고유 UI만 새로 컴포넌트로 만드는 방식으로 코드를 생성한다. 모든 컴포넌트가 같은 구조를 따르게 하는 게 목적이다. + +> **발동 조건**: `/component`로 호출했을 때. 뒤에 Figma URL(node-id 포함) 또는 node-id가 없으면 임의로 노드를 고르지 말고 사용자에게 요청한다. +> **`figma:figma-design-to-code` 스킬과의 관계**: 이 스킬을 대체하지 않는다. 그 스킬의 "기존 컴포넌트·토큰 재사용" 단계를 WDS 우선 규칙으로 구체화한 래퍼다. `get_design_context`를 호출하기 전 `figma-design-to-code` 스킬(또는 `skill://figma/figma-design-to-code/SKILL.md`)도 함께 로드한다. + +## 상수 + +``` +Stream fileKey: 3QkxTuGLZkB17pZILTog9L +WDS libraryKey: lk-01f447137a741b37c25896e9a4e109dcb719fa4e54177be9a39d24b8da507c109279c2d1d0533b9943595d7d6a5ea398977f43b5d84e163797965d810ba79b69 +``` + +Figma URL이 주어지면 거기서 fileKey/nodeId를 추출한다. node-id만 주어지고 fileKey가 없으면 위 Stream fileKey를 기본값으로 쓴다(이 스킬은 Stream 파일 전용). + +## Step 1 — 대상 노드 확인 + +`get_metadata(fileKey, nodeId)` 또는 `get_screenshot`으로 무엇을 컴포넌트화할지 확인한다. 노드가 화면 전체처럼 너무 크면, 실제로 컴포넌트화할 하위 노드를 좁혀달라고 사용자에게 요청한다 — 임의로 쪼개지 않는다. + +## Step 2 — WDS 대조 + +1. `docs/conventions/wds-component-usage.md`를 먼저 읽는다. 이미 확정된 매핑이 있으면 재조사 없이 바로 쓴다 — **단, 대상 노드의 실제 스크린샷/스타일이 문서에 기록된 것과 눈에 띄게 다르면(색상·활성 상태 표현 등) 같은 이름이라도 재확인한다.** 파일 전체에서 한 번 확정된 컴포넌트라도 다른 화면에서는 Stream이 로컬로 새로 만든 동명의 요소일 수 있다(사례: `docs/conventions/wds-component-usage.md`의 "빌릴게 필터 Chip은 WDS Chip/Chip이 아니었다" 참고). +2. 대상 노드 안의 인스턴스 중 문서에 없는 이름이 있으면: + 1. `search_design_system`을 WDS libraryKey로 스코프 제한해서 정확한 이름으로 검색한다. + 2. 결과가 애매하면(이름만 비슷하거나 여러 개 매칭) `get_design_context`로 실제 인스턴스 노드를 열어, 응답의 "Component descriptions" 섹션에 나오는 메인 컴포넌트 Node ID·공식 문서 링크(`montage.wanted.co.kr`)로 확정한다. + 3. 새로 확정된 매핑은 `docs/conventions/wds-component-usage.md`의 표에 바로 추가한다 — 이 스킬을 쓸수록 문서가 쌓여서 다음 실행이 더 빨라진다. +3. 매칭도 안 되고 이름도 비슷한 게 없으면 Stream 고유 UI로 취급한다(Step 3-3). + +## Step 3 — 코드 생성 + +1. `get_design_context(fileKey, nodeId)`로 레퍼런스 코드를 받는다. +2. Step 2에서 WDS로 확정된 서브트리는 raw JSX 대신 실제 WDS export로 치환한다. + - export 이름은 반드시 `node_modules/@wanteddev/wds/dist/components/`에서 실존 여부를 확인한 후 쓴다 — 이름을 추측하지 않는다. + - 아이콘은 `@wanteddev/wds-icon`에서 가져온다. + - WDS 컴포넌트 내부를 오버라이드하지 않는다. 레이아웃 조정은 감싸는 wrapper에서 한다. +3. WDS로 확정되지 않은 나머지(Stream 고유 UI)는 `docs/conventions/component-convention.md`를 따라 새 컴포넌트로 작성한다 (파일 구조, variant→Props 유니온 타입 매핑, Figma 노드 추적 주석 등). +4. 이미지·아이콘 asset은 `download_assets`로 받아 `src/assets/`에 커밋한다 — Figma asset URL은 7일 후 만료되므로 그대로 참조하지 않는다. + +## Step 4 — 배치 & 검증 + +1. `docs/conventions/coding-style.md`·`component-convention.md`의 파일 위치 규칙(feature 전용 vs `components/ui/` 공용)에 따라 저장 위치를 정한다. 재사용 범위가 애매하면 사용자에게 확인한다. +2. `pnpm check`(Biome)로 lint/format을 통과시킨다. +3. `component-convention.md`의 "완료 기준 체크리스트"로 스스로 검증한다. + +## Step 5 — 결과 안내 + +- 생성·수정한 파일 경로 +- WDS로 대체한 컴포넌트 목록 / 새로 만든 Stream 고유 컴포넌트 목록 +- `wds-component-usage.md`에 새로 추가한 매핑이 있으면 그 사실을 짚어준다 diff --git a/.claude/skills/figma-check/SKILL.md b/.claude/skills/figma-check/SKILL.md new file mode 100644 index 0000000..4afeaf8 --- /dev/null +++ b/.claude/skills/figma-check/SKILL.md @@ -0,0 +1,76 @@ +--- +name: figma-check +description: 이미 구현된 코드가 Figma 디자인과 1:1로 일치하는지 교차검증한다. 색상 토큰, WDS 컴포넌트 사용 판단, 레이아웃/패딩, 실제 렌더링 스크린샷까지 대조해서 불일치를 번호 매겨 리포트한다. /figma-check [코드 파일 경로]로 호출한다. +--- + +# /figma-check — Figma ↔ 구현 교차검증 워크플로우 + +`/component`로 만든 코드가 시간이 지나거나 다른 세션에서 수정되면서 Figma 원본과 어긋나지 않았는지 확인한다. **이 스킬은 검증만 한다 — 발견한 불일치를 자동으로 고치지 않는다.** 수정은 사용자가 결과를 보고 별도로 요청할 때 진행한다. + +> **발동 조건**: `/figma-check`로 호출했을 때. 뒤에 Figma URL(node-id 포함) 또는 node-id가 없으면 임의로 노드를 고르지 말고 사용자에게 요청한다. +> **`/component`와의 관계**: `/component`가 "Figma → 코드"를 만드는 스킬이라면, 이 스킬은 그 결과물이 여전히 Figma와 맞는지 "코드 ↔ Figma"를 되짚어 확인하는 스킬이다. 둘 다 같은 컨벤션 문서(`docs/conventions/component-convention.md`, `wds-component-usage.md`)를 기준으로 삼는다. + +## 상수 + +``` +Stream fileKey: 3QkxTuGLZkB17pZILTog9L +WDS libraryKey: lk-01f447137a741b37c25896e9a4e109dcb719fa4e54177be9a39d24b8da507c109279c2d1d0533b9943595d7d6a5ea398977f43b5d84e163797965d810ba79b69 +``` + +Figma URL이 주어지면 거기서 fileKey/nodeId를 추출한다. node-id만 주어지고 fileKey가 없으면 위 Stream fileKey를 기본값으로 쓴다. + +## Step 1 — 대상 코드 파일 확정 + +- 사용자가 코드 경로를 같이 줬으면 그걸 쓴다. +- 안 줬으면 `component-convention.md`가 요구하는 `// Figma: ... (nodeId )` 파일 최상단 주석을 근거로 찾는다: + +```bash +grep -rln "nodeId <해당 id>\|nodeId \`<해당 id>\`" src/ --include="*.tsx" +``` + +- 매칭이 여러 개거나 하나도 없으면 임의로 고르지 말고 사용자에게 확인한다. 검증 대상이 화면 전체(여러 컴포넌트로 조립됨)면, `get_metadata`로 하위 노드 구조를 먼저 파악해 관련 컴포넌트 파일들을 전부 나열한다. + +## Step 2 — Figma 레퍼런스 수집 + +대상 노드에 대해 아래 세 가지를 받는다 (`figma:figma-design-to-code` 스킬도 함께 로드). + +1. `get_design_context(fileKey, nodeId)` — 레퍼런스 JSX/스타일과 "Component descriptions" 섹션(WDS 메인 컴포넌트 Node ID·문서 링크 확정용) +2. `get_screenshot(fileKey, nodeId)` — 비교용 스크린샷. 로컬에 저장해둔다 +3. `get_variable_defs(fileKey, nodeId)` — 이 노드에서 실제 쓰이는 Figma 변수명과 값(색상·타이포) + +## Step 3 — 실제 렌더링 캡처 + +1. dev 서버가 안 떠 있으면 `pnpm dev`로 띄운다(포트 충돌 시 기존 프로세스 정리 후 재시작). +2. `run` 스킬(또는 `chromium-cli`, 없으면 `npx playwright`)로 대상 화면/컴포넌트를 렌더링해서 스크린샷을 찍는다. 특정 variant(예: 선택 상태, 스테퍼 모드)를 봐야 하면 클릭 등으로 상태를 재현한 뒤 캡처한다. +3. 확인이 끝나면 띄운 dev 서버는 정리한다(사용자가 계속 쓰라고 하지 않는 한). + +## Step 4 — 교차검증 체크리스트 + +아래 다섯 가지를 순서대로 확인한다. 항목마다 통과/불일치를 기록해둔다. + +1. **비주얼 비교**: Step 2의 Figma 스크린샷과 Step 3의 실제 렌더링을 나란히 놓고 본다. 레이아웃, 정렬, 간격, 색감, 잘림 여부를 확인한다. +2. **색상 토큰**: 대상 코드에 `text-[#...]`, `bg-[#...]`, `border-[#...]` 같은 하드코딩 hex가 남아있으면 전부 지적한다. `docs/conventions/component-convention.md`의 "색상 토큰" 규칙대로 `src/index.css`의 `@theme` 토큰을 써야 한다. 이미 토큰이 있는데 못 찾고 hex를 새로 박은 경우도 있을 수 있으니, hex 값을 Step 2의 `get_variable_defs` 결과와 대조해서 맞는 토큰이 있는지 확인한다. 이름이 비슷해도 값이 다른 토큰들(예: `Line/Normal/Neutral` 반투명 vs `Line/Solid/Neutral` 불투명)을 혼동하지 않았는지 특히 주의한다. +3. **WDS 판단 재검증**: 코드가 `@wanteddev/wds`/`@wanteddev/wds-icon`을 import하는 자리마다 `docs/conventions/wds-component-usage.md`에 그 판단 근거가 있는지 확인한다. 없으면 Step 2의 Component descriptions로 확정하고 문서에 추가한다. 반대 방향도 확인한다 — Stream 로컬로 새로 만든 부분이 사실 WDS 컴포넌트인 경우, 또는 이름은 같지만 실제로는 다른 스타일(활성 상태 표현 등)이라 Stream 로컬이 맞는 경우. +4. **박스모델 실측**: 브라우저에서 대표 요소 하나를 골라 `getComputedStyle`로 padding/border-width/margin이 의도한 값과 맞는지 찍어본다. 특히 `0px`으로 죽어있으면 CSS 우선순위 문제(예: 레이어 밖 전역 reset이 Tailwind 유틸리티를 이기는 문제 — `docs/plans/rental-list-screen.md` 참고)를 의심한다. +5. **컴포넌트 기본값 재확인**: WDS 컴포넌트를 쓸 때 명시하지 않은 prop의 기본값이 Figma 디자인과 다른 걸 렌더링하고 있지 않은지 확인한다(예: `TopNavigation`의 기본 `variant="normal"`이 타이틀을 가운데 정렬시켜서 Figma의 좌측 정렬과 어긋났던 사례, `background` 기본값이 iOS 반투명 스타일이라 배경색이 미묘하게 달라 보였던 사례). 의심되면 해당 컴포넌트의 `node_modules/@wanteddev/wds/dist/components//style.js`를 직접 열어 실제 동작을 확인한다. + +## Step 5 — 결과 안내 + +`pr-check` 스킬과 같은 형식으로, 발견한 불일치를 중요도 순으로 번호 매겨 나열한다. + +``` +## Figma 교차검증 결과 — {대상} (nodeId {id}) + +1. 🔴 [파일:라인] 무엇이 다른지 + - Figma: {기대값} + - 코드: {실제값} + - 수정 방안: {한두 문장} + +2. 🟡 [파일:라인] ... +``` + +- 🔴 High: 화면에 실제로 다르게 보이는 것(레이아웃 깨짐, 색 틀림, WDS 컴포넌트 오판으로 동작이 다름) +- 🟡 Medium: 지금 당장 안 보이지만 잠재적으로 문제(하드코딩 hex가 토큰과 값은 같지만 토큰을 안 써서 나중에 디자인 바뀌면 안 따라가는 경우 등) +- 🟢 Low: 사소한 스타일 차이, 주석/문서 누락 +- 전부 통과했으면 "N개 항목 모두 Figma와 일치" 로 짧게 알린다. +- 새로 확정된 WDS 매핑이나 색상 토큰이 있으면 `docs/conventions/wds-component-usage.md` / `component-convention.md`에 반영했는지 짚어준다. diff --git a/docs/conventions/component-convention.md b/docs/conventions/component-convention.md new file mode 100644 index 0000000..b9eedfe --- /dev/null +++ b/docs/conventions/component-convention.md @@ -0,0 +1,67 @@ +# Component Convention + +> Figma 디자인을 코드 컴포넌트로 옮길 때 따르는 구조 규칙. +> `docs/conventions/coding-style.md`(네이밍·폴더·TS 규칙)를 보완하는 문서이며, `/component` 스킬이 만드는 모든 컴포넌트는 이 규칙을 따른다. + +## 0. 원칙 + +Figma에 있는 요소라고 전부 새로 코드를 짜지 않는다. + +- **WDS(원티드 디자인 시스템) 컴포넌트로 확인된 건 반드시 `@wanteddev/wds`/`@wanteddev/wds-icon`을 import해서 쓴다.** 직접 마크업을 새로 짜지 않는다. 판별 기준은 `docs/conventions/wds-component-usage.md`. +- **Stream 고유 UI만 새 컴포넌트로 만든다** — WDS에 없는, Stream 서비스에서만 쓰는 화면 조각(카드, 리스트 아이템 등). + +## 1. 파일 위치 + +`coding-style.md`의 기능 기반(feature-based) 구조를 따른다. + +- 지금 다루는 화면/기능 전용이면 `features/<기능>/components/.tsx` +- 이미 다른 화면에서도 쓰이는 게 Figma 상에서 확인되면 `components/ui/.tsx` +- **애매하면 먼저 `features/` 아래에 둔다.** 두 번째 화면에서 실제로 재사용될 때 `components/ui/`로 옮긴다 — 성급하게 공용 폴더부터 만들지 않는다(coding-style.md의 "빈 폴더 미리 만들지 않는다"와 같은 이유). + +## 2. 파일 구조 + +- **컴포넌트 하나 = 파일 하나** (`ComponentName.tsx`). 폴더로 쪼개지 않는다 — 실제로 서브컴포넌트가 분리될 필요가 생기면 그때 판단한다. +- Figma의 variant(예: `trailingControl: Button | Stepper`)는 **Props의 유니온 타입 하나로 매핑**한다. variant 조합마다 별도 컴포넌트를 만들지 않는다. +- Props는 `interface`로 선언한다(coding-style.md TypeScript 규칙). + +```tsx +interface RentalItemCardProps { + itemName: string + quantity: number + trailingControl?: 'button' | 'stepper' +} +``` + +## 3. WDS 컴포넌트 사용 + +- 매칭된 서브트리는 `get_design_context`가 준 raw JSX 대신 **실제 WDS export로 치환**한다. +- export 이름은 반드시 `node_modules/@wanteddev/wds/dist/components/`에서 확인 후 쓴다 — 이름을 추측하지 않는다. + - 예: Figma `Button/Button` → `import { Button } from '@wanteddev/wds'` + - 예: Figma `Chip/Chip` → `import { Chip } from '@wanteddev/wds'` +- 아이콘은 `@wanteddev/wds-icon`에서 가져온다. +- **WDS 컴포넌트 내부를 임의로 오버라이드하지 않는다.** 간격·배치 같은 레이아웃 조정은 감싸는 wrapper에서 한다. + +## 4. Stream 고유 UI (신규 컴포넌트) + +- `get_design_context`의 raw JSX/Tailwind는 **레퍼런스일 뿐, 그대로 커밋하지 않는다.** 프로젝트 Tailwind 클래스로 다시 짜되, 색상은 아래 "색상 토큰" 규칙을 따른다. + +### 색상 토큰 + +- 색은 `text-[#171719]`처럼 hex를 직접 박지 않는다. `src/index.css`의 `@theme` 블록에 있는 시맨틱 토큰(`text-label-normal`, `bg-background-alternative`, `border-line-solid-neutral`, `text-primary`, `bg-primary-subtle` 등)을 쓴다. 이 토큰들은 `@wanteddev/wds/global.css`가 심어둔 `--semantic-*`/`--atomic-*` CSS 변수를 그대로 별칭 연결한 것이라 다크 테마 전환도 자동으로 따라간다. +- 필요한 색이 아직 토큰으로 없으면, hex를 추측해서 쓰지 말고 `get_variable_defs(fileKey, nodeId)`로 해당 노드의 실제 Figma 변수명·값을 확인한 뒤 `index.css`의 `@theme`에 새 토큰을 추가한다. `Line/Normal/Neutral`(반투명 `#70737c29`)과 `Line/Solid/Neutral`(불투명 `#eaebec`)처럼 이름이 비슷해도 값이 다른 토큰이 있으니 이름만 보고 넘겨짚지 않는다. +- 컴포넌트 인스턴스가 없는 화면 배경/외곽선처럼 Figma 값이 실제로는 안 보이는 경우(예: Bottom Nav 상단 border가 바로 위 배경과 같은 색이라 안 보였던 사례)도 있다 — 이럴 땐 왜 다른 토큰으로 바꿨는지 주석으로 남긴다. +- Stream 자체 이미지·아이콘(일러스트, 물품 아이콘 등)은 `download_assets`로 받아 `src/assets/`에 커밋한다. Figma asset URL은 **7일 후 만료**되므로 절대 코드에 그대로 참조하지 않는다. +- `data-node-id` 같은 Figma 추적용 속성은 컴포넌트 마크업에 남기지 않는다. 대신 파일 최상단에 원본 Figma 노드를 알 수 있는 주석 한 줄만 남긴다 — 나중에 디자인이 바뀌었을 때 다시 대조할 수 있도록: + +```tsx +// Figma: Rental Item Card (nodeId 1041:61407) +``` + +## 5. 완료 기준 체크리스트 + +- [ ] WDS로 확인된 요소는 전부 import로 대체했다 (raw JSX 없음) +- [ ] Stream 고유 요소만 새 컴포넌트로 작성했다 +- [ ] Props가 Figma variant를 유니온 타입으로 반영한다 +- [ ] 이미지/아이콘 asset을 다운로드해 커밋했다 (만료되는 Figma URL 미참조) +- [ ] 파일 위치가 재사용 범위(feature 전용 vs 공용)에 맞는다 +- [ ] `pnpm check`(Biome) 통과 diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md new file mode 100644 index 0000000..fd28c06 --- /dev/null +++ b/docs/conventions/wds-component-usage.md @@ -0,0 +1,117 @@ +# Figma Stream 파일 — WDS(원티드 디자인 시스템) 사용 현황 + +> 작성일: 2026-09-07 +> 종류: 살아있는 참고 문서 — `/component` 스킬이 매번 이 문서부터 대조하고, 새로 확정되는 매핑을 여기에 추가한다 +> 대상 파일: [🌊 Stream](https://www.figma.com/design/3QkxTuGLZkB17pZILTog9L/%F0%9F%8C%8A--Stream) (`fileKey: 3QkxTuGLZkB17pZILTog9L`) + +## 배경 + +`4. 사용자 UI` 페이지(`nodeId: 971:29755`) 안에 있는 `component`라는 이름의 섹션(`nodeId: 985:36615`)은 Stream 팀이 자체적으로 만든 화면 조각(로컬 심볼) 모음이며, WDS 컴포넌트가 아니다. 실제 WDS 컴포넌트는 각 화면 인스턴스 안에 흩어져서 쓰이고 있어, 파일 전체 메타데이터에서 인스턴스 이름을 모아 [Wanted Design System (Community)](https://www.figma.com) 라이브러리(`libraryKey: lk-01f447137a741b37c25896e9a4e109dcb719fa4e54177be9a39d24b8da507c109279c2d1d0533b9943595d7d6a5ea398977f43b5d84e163797965d810ba79b69`)의 `search_design_system` 검색 결과와 이름을 대조해 정리했다. + +## 조사 방법 및 한계 + +1. `get_libraries`로 Stream 파일에 연결된 라이브러리 확인 → WDS(Community), iOS and iPadOS 26 2개가 연결됨. +2. `get_metadata(971:29755)`로 페이지 전체 노드 트리(약 43만자)를 받아 로컬 파일로 저장. +3. 저장된 트리에서 `` 태그만 추출해 이름별로 집계(총 178개 고유 이름, 인스턴스 총 개수 기준). +4. 각 이름을 `search_design_system`(WDS 라이브러리로 스코프 제한)에 검색해 **정확히 같은 이름의 컴포넌트가 WDS에 존재하는지** 대조. + +**한계**: `get_metadata`는 인스턴스의 `componentKey`(어느 라이브러리 컴포넌트를 참조하는지 나타내는 고유 키)를 주지 않는다. 그래서 아래 표는 "이름 일치"로 확인한 것이며, 100% 확정하려면 `get_design_context`로 각 인스턴스를 열어 componentKey를 대조해야 한다(아래 "완전 확정 방법" 참고). + +## WDS 컴포넌트로 확인됨 (이름 정확히 일치, componentKey까지 확보) + +| WDS 컴포넌트 | 파일 내 인스턴스 수 | WDS componentKey | +|---|---|---| +| `Top Navigation/Resource/Contents` | 81 | `fcafe72bb0d975a6a1e6486be86be899e7d93e4a` | +| `Content Badge/Content Badge` | 48 | `2118b972d3ea08f35fb551cc755e41b5adeeb312` | +| `Control/Checkbox` | 42 | `72378cce3669fd4c065f12d01c254c303209d700` | +| `Action Area/Action Area` | 34 | `56b315679e172ceeac7ad64851cb0059ec235ae7` | +| `Control/Radio` | 28 | `8267d231338418aa74450dcfbe1576791540fb2a` | +| `Textinput/Textarea` | 24 | 메인 컴포넌트 Node ID `567:14111` — [문서](https://montage.wanted.co.kr/docs/components/selection-and-input/text-area/design) | +| `Chip/Chip` | 24 | 메인 컴포넌트 Node ID `440:4251` — [문서](https://montage.wanted.co.kr/docs/components/actions/action-chip/design) | +| `Icon/Normal/Location` | 13 | 메인 컴포넌트 Node ID `567:16585` | +| `Icon/Normal/Clock` | 13 | `7b620e5b46b1a467c6f8662fabcde63ce4e73b01` | +| `Page Indicator/Counter` | 12 | `560362b1ebe2ebe05646e66f4eb54a1531f3073d` | +| `Toast/Toast` | 10 | `5e6b6b522ae500ca6cd893ee2c2ffaf378c5c808` | +| `Button/Button` | 9 | `d28f3e22ae96d34ce26fb02977f23fb8084b7f85` | +| `Menu/Resource/Action Area/Trailing Content/Button` | 7 | `b7088257913aa98cea946cc3dc7931ebd544b872` | +| `Divider/Divider` | 7 | `cdef3da5cdbdd1e6f5d5d9efe85280c509b9e614` | +| `Icon/Normal/Circle Info` | 6 | 메인 컴포넌트 Node ID `440:4076` | +| `Icon/Normal/Arrow Right` | 6 | `26812c6481c960486eebf2282d6e12f2d4a133cd` | +| `Tab/Tab` | 4 | `454aa29664579ce983621c8b1c078f3e2e7ddbb0` | +| `Pagination/Dots` | 4 | 메인 컴포넌트 Node ID `445:9563` — [문서](https://montage.wanted.co.kr/docs/components/navigations/pagination-dots/design) | +| `Avatar` (Avatar/Avatar 계열) | 4 | `5885add30e5c5f5048057425d06ee89f263e96dd` | +| `Icon/Normal/Circle Check` | 3 | `bd9f80c38233b8d368cbcb4b8b9dcfa4c14c10b5` | +| `Menu/Menu` | 3 | `b4044d0a6a54bc6f8b10a76b97bfdf46a3978098` | +| `Icon/Normal/Plus` | 2 | `c2b078d8027a89ac207d6f5a1fe4205f9e26242c` | +| `Icon/Normal/Pencil` | 2 | `ddc90ae1926c0277477f233629cd4c186e87426f` | +| `Category/Resource/Chip/Normal/Normal` | 2 | `7a1668f2266cd57a689731ded85b52aa58c39905` | +| `Category/Resource/Chip/Normal/XSmall` | 1 | `73e0f352cd9ac759377854340136cd1f1032b6cb` | +| `Category/Resource/Chip/Alternative/Small` | 1 | `72cee86994b3c3581887be5149c41a59c4b02d93` | +| `Icon/Normal/Calendar` | 1 | `e050124e28e6217eedf243295b6087728acd9edc` | + +WDS 컴포넌트만 합산하면 파일 안에서 **약 350회 이상**의 인스턴스가 확인된다(위 표 합계 기준). `Textinput/Textarea`, `Chip/Chip`, `Icon/Normal/Location`, `Icon/Normal/Circle Info`, `Pagination/Dots` 5개는 `get_design_context`로 실제 노드를 열어 WDS 메인 컴포넌트 Node ID(및 3개는 원티드 공식 디자인 시스템 문서 링크)까지 확인해 완전히 확정했다. + +## 이후 세션에서 개별 화면 작업 중 추가 확인된 매핑 + +파일 전체 스캔이 아니라 `/component`로 특정 화면(빌릴게, `1243:73331`)을 구현하면서 `get_design_context`로 열어본 김에 확정한 것들. 인스턴스 수는 파일 전체 기준이 아니라 "이 화면에서 확인됨"이다. + +| WDS 컴포넌트 | 확인 경로 | WDS 메인 컴포넌트 Node ID / 문서 | +|---|---|---| +| `Segmented Control/Segmented Control` | 빌릴게 화면 Top Navigation 안 "대여/반납" 토글 | `500:11592` — [문서](https://montage.wanted.co.kr/docs/components/selection-and-input/segmented-control/design) | +| `Icon/Normal/Search` | 빌릴게 화면 Top Navigation 트레일링 아이콘 | `445:5904` | +| `Icon/Normal/Bell` | 빌릴게 화면 Top Navigation 트레일링 아이콘 | `445:13236` | +| `Icon/Normal/Home` | Bottom Nav "홈" 탭(Normal 상태) | `980:35475` | +| `Icon/Normal/Ticket` | Bottom Nav "행사" 탭(Normal 상태) | `980:35529` | +| `Icon/Normal/List` | Bottom Nav "게시판" 탭(Normal 상태) | `980:35703` | + +코드에서는 `@wanteddev/wds-icon`의 `IconSearch`/`IconBell`/`IconHome`/`IconTicket`/`IconList`로 대응된다(각각 default export를 `index.d.ts`에서 named export로 재노출). `Segmented Control`은 `@wanteddev/wds`의 `SegmentedControl`/`SegmentedControlItem`으로 대응된다. + +### 반례 — 빌릴게 필터 Chip은 WDS `Chip/Chip`이 아니었다 + +위 "WDS 컴포넌트로 확인됨" 표에 `Chip/Chip`이 파일 전체 기준 24개 인스턴스로 확정돼 있다고 해서, **다른 화면의 비슷하게 생긴 칩도 자동으로 WDS라고 가정하면 안 된다.** 빌릴게 화면의 카테고리 필터(전체/전자기기/생활잡화/상비약/위생용품)를 처음 구현할 때 이 표만 보고 재조사 없이 WDS `Chip`을 그대로 썼는데, 실제 Figma 스타일(활성 = 연한 파랑 배경 + 파랑 outline, 비활성 = 회색 outline)이 WDS Chip의 기본 활성 스타일(검정 배경)과 달랐다 — Stream이 로컬로 새로 만든 칩이었다. **스타일이 눈에 띄게 다르면, 이름이 같아 보여도 그 인스턴스는 따로 `get_design_context`로 열어 확인한다.** 코드는 `src/features/rental/components/RentalCategoryFilter.tsx` 참고 (plain ` +``` + +hex를 하드코딩하지 않고 `index.css`의 색상 토큰을 그대로 참조했고, `Button` 자체(접근성 속성, `disabled`/`loading` 상태 처리 등)는 그대로 재사용한다. `src/features/rental/components/RentalItemCard.tsx` 참고. + +## 제외됨 — Stream 자체 로컬 컴포넌트 (WDS 아님) + +이름은 비슷해 보여도 WDS 검색 결과에 없거나, `component` 섹션(985:36615)에서 로컬 심볼로 직접 정의된 것들: + +- `Rental Item Card`, `RentalHistory-card`, `ApplicationHistory-card`, `Item-card` 계열, `Event-card`, `Q&A Card`, `Notice-card` — Stream 도메인 전용 카드 +- `Bottom Nav`, `BottomNav/Icon`, `Locker-button`, `SearchField`, `Floating Button`, `Empty State`, `Modal`, `Modal/ButtonGroup`, `Section-header`, `Top Navigation`(WDS의 `Top Navigation/Resource/Contents`와 다른 별개 로컬 프레임), `divider(new)`, `ProgressBar`, `Native / Home Indicator`, `Native / Bottom Sheet Indicator` +- `Icon/Feedback`, `Icon/Camera`, `Icon/Link`, `Icon/Answer`, `Icon/Activity`, `Icon/Arrow` 및 고데기·알약·후시딘 등 물품 아이콘 — Stream 전용 아이콘 세트 (WDS의 `Icon/Normal/*` 네이밍과 다름) +- `Status Bar - iPhone`, `Home Bar` — WDS가 아니라 별도로 연결된 **iOS and iPadOS 26 (Community)** 라이브러리 소속으로 추정 + +### 재검증: `Bottom Nav` / `Modal` / `Section-header` + +`@wanteddev/wds` 코드 패키지에는 `bottom-navigation`, `modal`, `section-header` 컴포넌트가 실제로 존재해서 이 셋이 WDS일 가능성을 재검토했으나, Figma 쪽 이름으로 다시 검색한 결과 **원래 분류(Stream 로컬)가 맞다**: + +- `Bottom Nav` — WDS에는 `Bottom Navigation/Bottom Navigation`이라는 이름으로 존재(componentKey `e0bf6586448b2b496500a76ee51838f44eb562d6`). Stream 파일의 `component` 섹션에는 이와 별개로 `Bottom Nav`라는 **로컬 컴포넌트 셋**이 직접 정의돼 있고(`Selected=Home/Event/Board/Rental` variant), 화면에서 쓰이는 인스턴스 이름도 `Bottom Navigation/Bottom Navigation`이 아니라 `Bottom Nav`다. 즉 디자이너가 WDS 컴포넌트를 안 쓰고 로컬로 새로 만든 것. +- `Modal` — WDS 라이브러리에서 "Modal"로 검색해도 이름이 일치하는 컴포넌트가 없음(가장 가까운 결과가 무관한 `Icon/Normal/Medal`). Stream 파일의 `Modal`/`Modal/ButtonGroup`은 `Circle Exclamation=on/off`, `Style=Default/Negative` 같은 Stream 전용 variant를 가진 로컬 컴포넌트 셋. +- `Section-header` — WDS 라이브러리에서 "Section Header"로 검색해도 결과 0건. Stream 파일 안의 로컬 컴포넌트. + +**주의**: 이건 "Figma 디자인 파일이 어떤 컴포넌트를 참조하고 있는가"에 대한 결론이지, "코드에서 무엇을 써야 하는가"와는 다른 질문이다. `wds`의 `Modal`/`BottomNavigation`/`SectionHeader` 코드 컴포넌트는 여전히 존재하므로, 실제 구현 시에는 (디자인이 로컬로 그려졌더라도) WDS 코드 컴포넌트를 기반으로 만드는 게 나을 수 있다 — 이건 구현 단계에서 별도로 판단할 문제. + +## 완전 확정 방법 (필요 시) + +이름 대조가 아니라 100% 확정하려면, 확인하고 싶은 인스턴스의 `nodeId`를 알아낸 뒤 `get_design_context(fileKey, nodeId)`를 호출해 응답에 포함된 componentKey를 위 표의 WDS componentKey와 직접 비교하면 된다. 또는 Figma 앱에서 인스턴스 선택 → 우측 패널 "Instance of" → 라이브러리 아이콘 클릭으로도 즉시 확인 가능하다. From 0047473ac4c4f6eec2bebcf39b31df34689ef28f 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: Wed, 9 Sep 2026 17:26:29 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20=EB=8C=80=EC=97=AC=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=ED=9C=A0=20=ED=94=BC=EC=BB=A4=EB=A1=9C=20=EB=8C=80?= =?UTF-8?q?=EC=97=AC=20=EC=8B=9C=EC=9E=91=20=EC=8B=9C=EA=B0=84=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/wds-component-usage.md | 13 +- package.json | 1 + pnpm-lock.yaml | 12 ++ src/components/ui/BottomSheet.tsx | 53 ++++++ src/components/ui/ScreenLayout.tsx | 36 ++-- src/components/ui/screenSheetPortalContext.ts | 8 + src/components/ui/useScreenSheetPortal.ts | 9 + src/features/bililge/BililgeListScreen.tsx | 18 +- .../bililge/components/BililgeRentalSheet.tsx | 156 ++++++++++++++++++ src/index.css | 4 + 10 files changed, 297 insertions(+), 13 deletions(-) create mode 100644 src/components/ui/BottomSheet.tsx create mode 100644 src/components/ui/screenSheetPortalContext.ts create mode 100644 src/components/ui/useScreenSheetPortal.ts create mode 100644 src/features/bililge/components/BililgeRentalSheet.tsx diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index fd28c06..96da1c1 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -91,7 +91,18 @@ WDS 컴포넌트만 합산하면 파일 안에서 **약 350회 이상**의 인 ``` -hex를 하드코딩하지 않고 `index.css`의 색상 토큰을 그대로 참조했고, `Button` 자체(접근성 속성, `disabled`/`loading` 상태 처리 등)는 그대로 재사용한다. `src/features/rental/components/RentalItemCard.tsx` 참고. +hex를 하드코딩하지 않고 `index.css`의 색상 토큰을 그대로 참조했고, `Button` 자체(접근성 속성, `disabled`/`loading` 상태 처리 등)는 그대로 재사용한다. `src/features/bililge/components/BililgeItemCard.tsx` 참고. + +### 빌릴게 대여 바텀시트 — `Action Area/Action Area`, `Icon/Normal/Circle Info`, 그리고 Time Picker는 의도적으로 WDS를 안 씀 + +`/component`로 빌릴게 대여 바텀시트(Figma nodeId `1422:57155`, 실제 시트 콘텐츠는 `1422:57176`)를 구현하며 확인된 내용. + +- **`Action Area/Action Area` + `ActionAreaButton`**: 버튼 하나(대여 신청하기)만 있는 액션 영역도 `@wanteddev/wds`의 `ActionArea`(기본 `variant="strong"`) + `ActionAreaButton`(기본 `variant="main"`)으로 그대로 재현된다. `ActionAreaButton`의 `main` variant가 내부적으로 `Button`을 `size="large"` `fullWidth` `variant="solid"` `color="primary"`로 렌더링해서 Figma의 파란 통 너비 버튼과 정확히 일치했다(`node_modules/@wanteddev/wds/dist/components/action-area/index.js` 확인). `src/features/bililge/components/BililgeRentalSheet.tsx` 참고. +- **`Icon/Normal/Circle Info`**: 안내 문구("대여 시작 시간은 최소 5분 뒤부터...") 앞 아이콘. 위 표에서 이미 확정된 매핑 재사용(`@wanteddev/wds-icon`의 `IconCircleInfo`). +- **Time Picker(휠 피커)는 예외적으로 WDS를 쓰지 않기로 결정했다.** `@wanteddev/wds`에 `time-picker` 컴포넌트가 실제로 존재하지만(`node_modules/@wanteddev/wds/dist/components/time-picker/`), 이건 `` 기반 텍스트 필드형 컴포넌트라 Figma가 그리는 iOS 스타일 휠 스크롤 피커(오전/오후·시·분 3열, 가운데 값만 진하게)와는 UI 패턴 자체가 다르다. 사용자가 명시적으로 지정한 [`@ncdai/react-wheel-picker`](https://react-wheel-picker.chanhdai.com)(unstyled core, `WheelPicker`/`WheelPickerWrapper`)로 구현했다: + - `optionItem`/`highlightItem`/`highlightWrapper` classNames로 텍스트 스타일만 입히고(선택 안 됨: `text-label-disable` 17px medium, 선택됨: `text-label-normal` 18px semibold), Figma의 "Selection Highlight"(3열을 가로지르는 pill 배경, `bg-background-alternative`)는 라이브러리 밖에서 별도 `absolute` div로 얹었다 — 각 컬럼마다 하이라이트 배경을 따로 안 그리기 위해서다. + - 이 라이브러리의 CSS(`@ncdai/react-wheel-picker/style.css`)도 `@wanteddev/wds/global.css`와 같은 이유로 **반드시 `layer(base)`로 import해야 한다**(`src/index.css`) — 안 그러면 Tailwind 유틸리티가 라이브러리의 unlayered CSS한테 밀려서 `justify-center` 같은 오버라이드가 안 먹는다. + - 새 토큰 `--color-label-disable`(`--semantic-label-disable` 별칭)을 이때 추가했다. `get_variable_defs`로 확인한 실제 값은 `rgba(55,56,60,0.16)`. ## 제외됨 — Stream 자체 로컬 컴포넌트 (WDS 아님) diff --git a/package.json b/package.json index 93b6b61..9feac36 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "check:fix": "biome check --write ." }, "dependencies": { + "@ncdai/react-wheel-picker": "^1.2.3", "@wanteddev/wds": "^3.12.0", "@wanteddev/wds-icon": "^3.12.0", "react": "^19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6eb849c..0e18b92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@ncdai/react-wheel-picker': + specifier: ^1.2.3 + version: 1.2.3(react@19.2.8) '@wanteddev/wds': specifier: ^3.12.0 version: 3.12.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/serialize@1.3.3)(@emotion/utils@1.4.2)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -234,6 +237,11 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@ncdai/react-wheel-picker@1.2.3': + resolution: {integrity: sha512-TQjDfuDH9Ss7sgacSRPnakqWfkR8E1w6g7Pkt7bTYX35SfdHbGSzC8g/lXKFeMyNIikOWD9wC3uFPLy+Ui/R0g==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + '@oxc-project/types@0.147.0': resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} @@ -1485,6 +1493,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@ncdai/react-wheel-picker@1.2.3(react@19.2.8)': + dependencies: + react: 19.2.8 + '@oxc-project/types@0.147.0': {} '@radix-ui/number@1.1.0': {} diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx new file mode 100644 index 0000000..da11072 --- /dev/null +++ b/src/components/ui/BottomSheet.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import { createPortal } from "react-dom"; + +import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; + +interface BottomSheetProps { + open: boolean; + onClose: () => void; + children: ReactNode; +} + +// Figma: Views / Bottom Sheets (nodeId 1422:57176) — WDS에는 대응하는 코드 컴포넌트가 없다 +// (component-convention.md 참고: Native / Bottom Sheet Indicator는 Stream/iOS 목업 전용 로컬 요소). +// 딤+시트를 ScreenLayout의 포털 슬롯(useScreenSheetPortal)에 그려서 375×812 프레임 전체를 덮는다. +function BottomSheet({ open, onClose, children }: BottomSheetProps) { + const portalEl = useScreenSheetPortal(); + + if (!portalEl) { + return null; + } + + return createPortal( +
+ - + {stepperValue}
-
+
setTime((prev) => ({ ...prev, hour })) } + optionItemHeight={24} options={HOUR_OPTIONS} value={time.hour} - visibleCount={8} + visibleCount={12} />
-
+
setTime((prev) => ({ ...prev, minute })) } + optionItemHeight={24} options={MINUTE_OPTIONS} value={time.minute} - visibleCount={8} + visibleCount={12} />
From b50014cb24f93b1f7d9caf5a2339ce21227530c9 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: Wed, 9 Sep 2026 17:42:18 +0900 Subject: [PATCH 10/23] =?UTF-8?q?fix:=20=ED=9C=A0=20=ED=94=BC=EC=BB=A4=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=A4=84=20=EB=92=A4=20=ED=9A=8C=EC=83=89?= =?UTF-8?q?=20=EA=B8=80=EC=9E=90=EA=B0=80=20=EB=B9=84=EC=B3=90=EC=84=9C=20?= =?UTF-8?q?=EB=91=90=EA=BA=BC=EC=9B=8C=20=EB=B3=B4=EC=9D=B4=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bililge/components/BililgeRentalSheet.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/bililge/components/BililgeRentalSheet.tsx b/src/features/bililge/components/BililgeRentalSheet.tsx index 83a4354..5a608ae 100644 --- a/src/features/bililge/components/BililgeRentalSheet.tsx +++ b/src/features/bililge/components/BililgeRentalSheet.tsx @@ -39,7 +39,10 @@ const MINUTE_OPTIONS: WheelPickerOption[] = Array.from( // 3개 컬럼 공통으로 하나 깔아주기 때문에 여기서는 텍스트 스타일만 다룬다. const WHEEL_CLASS_NAMES: WheelPickerClassNames = { highlightItem: "font-semibold text-label-normal text-lg tabular-nums", - highlightWrapper: "", + // 뒤쪽 옵션 리스트(회색)가 하이라이트 리스트(진한 글자)에 그대로 비쳐서 겹쳐 보이는 문제 — + // 배경색을 채워서 가운데 줄만큼은 회색 글자를 완전히 가려야 한다. 공유 pill과 같은 색이라 + // 컬럼 사이 gap에서 보이는 pill과 이어져서 하나의 막대처럼 보인다. + highlightWrapper: "bg-background-alternative", optionItem: "font-medium text-[17px] text-label-disable tabular-nums", }; @@ -140,7 +143,7 @@ function BililgeRentalSheet({ item, open, onClose }: BililgeRentalSheetProps) {
-
+

대여 시작 시간은 최소 5분 뒤부터 선택할 수 있어요 From cf90bc0aa3e6ccd9f198abdad11656e3802606af 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: Wed, 9 Sep 2026 17:55:36 +0900 Subject: [PATCH 11/23] =?UTF-8?q?fix:=20figma-check=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20=E2=80=94=20=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20=EC=97=AC=EB=B0=B1,=20=EC=95=88=EB=82=B4?= =?UTF-8?q?=20=EB=AC=B8=EA=B5=AC=20=EA=B5=B5=EA=B8=B0=20Figma=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 2 +- src/features/bililge/components/BililgeRentalSheet.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index da11072..ce1c16c 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -38,7 +38,7 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) { open ? "translate-y-0" : "translate-y-full" }`} > -

+
{children} diff --git a/src/features/bililge/components/BililgeRentalSheet.tsx b/src/features/bililge/components/BililgeRentalSheet.tsx index 5a608ae..9cb9126 100644 --- a/src/features/bililge/components/BililgeRentalSheet.tsx +++ b/src/features/bililge/components/BililgeRentalSheet.tsx @@ -145,7 +145,7 @@ function BililgeRentalSheet({ item, open, onClose }: BililgeRentalSheetProps) {
-

+

대여 시작 시간은 최소 5분 뒤부터 선택할 수 있어요

From 2e6144ed918ea7a8797af67a7dcdc671dcd4686c 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: Wed, 9 Sep 2026 18:00:56 +0900 Subject: [PATCH 12/23] =?UTF-8?q?fix:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EB=8B=A8=20=EC=84=B8=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EC=97=90=EC=96=B4=EB=A6=AC=EC=96=B4=EC=97=90=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=ED=99=88=20=EC=9D=B8=EB=94=94=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 8 ++++++-- src/index.css | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index ce1c16c..92b5b7e 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -42,8 +42,12 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) {
{children} - {/* iOS Home Indicator 안전 영역 — 모든 BottomSheet가 공통으로 필요해서 여기서 확보한다 */} -
+ {/* Figma: Native / Home Indicator (nodeId 1422:57206) — 이 시트가 열려있는 동안은 화면 + 맨 아래 chrome 역할을 하므로, BottomNav 대신 여기서 안전 영역 + 홈 인디케이터 바를 + 직접 그린다. */} +
+
+
, portalEl, diff --git a/src/index.css b/src/index.css index b959962..fec44b4 100644 --- a/src/index.css +++ b/src/index.css @@ -25,6 +25,8 @@ --color-icons-primary: #0f172a; /* WDS 시맨틱 세트에 없는 값(Figma "Sky/Base") — Native / Bottom Sheet Indicator(드래그 핸들) 전용 */ --color-sheet-indicator: #cdcfd0; + /* WDS 시맨틱 세트에 없는 값(Figma "Ink/Darkest") — Native / Home Indicator(하단 세이프에어리어 바) 전용 */ + --color-ink-darkest: #090a0a; } /* 무한 스크롤 목록 등 스크롤은 동작하되 스크롤바는 안 보이게 할 때 쓰는 재사용 유틸리티 */ From da0758b711259b8b9784902f1c11d47993aa346c 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: Wed, 9 Sep 2026 18:03:03 +0900 Subject: [PATCH 13/23] =?UTF-8?q?revert:=20=ED=99=88=20=EC=9D=B8=EB=94=94?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=B0=94=20=EC=A0=9C=EA=B1=B0,?= =?UTF-8?q?=20=EC=84=B8=EC=9D=B4=ED=94=84=EC=97=90=EC=96=B4=EB=A6=AC?= =?UTF-8?q?=EC=96=B4=20=EC=97=AC=EB=B0=B1=EB=A7=8C=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 8 ++------ src/index.css | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index 92b5b7e..ce1c16c 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -42,12 +42,8 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) {
{children} - {/* Figma: Native / Home Indicator (nodeId 1422:57206) — 이 시트가 열려있는 동안은 화면 - 맨 아래 chrome 역할을 하므로, BottomNav 대신 여기서 안전 영역 + 홈 인디케이터 바를 - 직접 그린다. */} -
-
-
+ {/* iOS Home Indicator 안전 영역 — 모든 BottomSheet가 공통으로 필요해서 여기서 확보한다 */} +
, portalEl, diff --git a/src/index.css b/src/index.css index fec44b4..b959962 100644 --- a/src/index.css +++ b/src/index.css @@ -25,8 +25,6 @@ --color-icons-primary: #0f172a; /* WDS 시맨틱 세트에 없는 값(Figma "Sky/Base") — Native / Bottom Sheet Indicator(드래그 핸들) 전용 */ --color-sheet-indicator: #cdcfd0; - /* WDS 시맨틱 세트에 없는 값(Figma "Ink/Darkest") — Native / Home Indicator(하단 세이프에어리어 바) 전용 */ - --color-ink-darkest: #090a0a; } /* 무한 스크롤 목록 등 스크롤은 동작하되 스크롤바는 안 보이게 할 때 쓰는 재사용 유틸리티 */ From e7887bfe14b73316b6f107f3df803c48b1cb4364 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: Wed, 9 Sep 2026 18:08:44 +0900 Subject: [PATCH 14/23] =?UTF-8?q?fix:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EB=8B=A8=20=EC=97=AC=EB=B0=B1=EC=9D=84=20?= =?UTF-8?q?34px=EB=A1=9C=20=EA=B3=BC=EB=8B=A4=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=ED=96=88=EB=8D=98=20=EA=B2=83=2014px=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index ce1c16c..df18e08 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -42,8 +42,11 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) {
{children} - {/* iOS Home Indicator 안전 영역 — 모든 BottomSheet가 공통으로 필요해서 여기서 확보한다 */} -
+ {/* Figma의 Action Area는 버튼을 감싸는 Container(p-5=20px, 이건 WDS ActionArea 자체 + padding으로 이미 확보됨) 다음에 iOS 홈 인디케이터용 "Gesture" 여백(pt-3.5=14px)이 + 하나 더 붙는데, 실제 @wanteddev/wds의 ActionArea 컴포넌트에는 이 14px이 없어서 + 여기서 더해준다. */} +
, portalEl, From fc643d4cc8dd2952af0370cd661b37249ea231b2 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: Wed, 9 Sep 2026 18:13:16 +0900 Subject: [PATCH 15/23] =?UTF-8?q?fix:=20Bottom=20Nav=20=ED=95=98=EB=8B=A8?= =?UTF-8?q?=20=EA=B2=80=EC=9D=80=EC=83=89=20=ED=99=88=20=EC=9D=B8=EB=94=94?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=B0=94=20=EC=A0=9C=EA=B1=B0,?= =?UTF-8?q?=20=EC=97=AC=EB=B0=B1=EB=A7=8C=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomNav.tsx | 4 +--- src/index.css | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/ui/BottomNav.tsx b/src/components/ui/BottomNav.tsx index e9f2669..12978cb 100644 --- a/src/components/ui/BottomNav.tsx +++ b/src/components/ui/BottomNav.tsx @@ -84,9 +84,7 @@ function BottomNav({ value, onValueChange }: BottomNavProps) { ); })}
-
-
-
+
); } diff --git a/src/index.css b/src/index.css index b959962..decd1c7 100644 --- a/src/index.css +++ b/src/index.css @@ -21,8 +21,6 @@ --color-primary: var(--semantic-primary-normal); --color-primary-subtle: var(--atomic-blue-95); - /* WDS 시맨틱 세트에 없는 값(Figma "Icons/Primary") — 홈 인디케이터 전용, 값 자체는 Figma에서 확인됨 */ - --color-icons-primary: #0f172a; /* WDS 시맨틱 세트에 없는 값(Figma "Sky/Base") — Native / Bottom Sheet Indicator(드래그 핸들) 전용 */ --color-sheet-indicator: #cdcfd0; } From d6d9f2b74bd0d35610bcee57a09a2b271ee363cf 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: Wed, 9 Sep 2026 18:15:21 +0900 Subject: [PATCH 16/23] =?UTF-8?q?fix:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=EC=9D=84=20iOS=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EA=B0=90=EC=86=8D=20=EA=B3=A1=EC=84=A0?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9E=90=EC=97=B0=EC=8A=A4=EB=9F=BD?= =?UTF-8?q?=EA=B2=8C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index df18e08..9b3f31c 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -21,7 +21,7 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) { return createPortal(
From 4142283b63c3e783da6626343c86a103a1efab3f 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 11:57:46 +0900 Subject: [PATCH 17/23] =?UTF-8?q?perf:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=B2=AB=20=EC=98=A4=ED=94=88=20=EB=95=8C=20?= =?UTF-8?q?=ED=9C=A0=20=ED=94=BC=EC=BB=A4=20=EB=A7=88=EC=9A=B4=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C=20=EC=B2=AB=20?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=9E=84=EC=9D=84=20=EB=A7=89=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bililge/BililgeListScreen.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index fe6f88a..17c716a 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -1,5 +1,5 @@ import { SegmentedControl, SegmentedControlItem } from "@wanteddev/wds"; -import { useState } from "react"; +import { startTransition, useState } from "react"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; @@ -44,8 +44,14 @@ function BililgeListScreen() { itemName={item.name} key={item.id} onRentRequest={() => { - setRentalItem(item); + // 바텀시트를 여는 것(슬라이드 애니메이션)은 즉시 반영하고, 그 안의 휠 + // 피커(특히 분 60개) 마운트처럼 무거운 작업은 startTransition으로 낮은 + // 우선순위로 미뤄서 첫 프레임이 버벅이지 않게 한다 — 처음 열 때만 해당하고, + // rentalItem은 닫아도 null로 안 돌아가서 두 번째부터는 이 마운트 비용 자체가 없다. setRentalSheetOpen(true); + startTransition(() => { + setRentalItem(item); + }); }} quantity={item.quantity} /> From acae08df50fbb65641071a246a6b0cc77069a654 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 14:14:52 +0900 Subject: [PATCH 18/23] =?UTF-8?q?refactor:=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=EC=9D=84=20WDS=20Typography=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98,=20=ED=83=80=EC=9D=B4=ED=8F=AC=EA=B7=B8=EB=9E=98?= =?UTF-8?q?=ED=94=BC=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지금까지 text-xs/text-[Npx] 등 Tailwind 값을 화면마다 손으로 맞춰왔는데, Bottom Nav 탭 라벨이 실제 Figma 값(11px)과 다른 12px로 굳어있던 걸 계기로 @wanteddev/wds의 Typography 컴포넌트(Figma 타입 스케일과 1:1 대응)로 교체했다. --- docs/conventions/component-convention.md | 21 +++++++++++++ docs/conventions/wds-component-usage.md | 6 ++++ src/components/ui/BottomNav.tsx | 13 ++++++-- .../components/BililgeCategoryFilter.tsx | 18 +++++++++-- .../bililge/components/BililgeItemCard.tsx | 31 +++++++++++++++---- .../bililge/components/BililgeRentalSheet.tsx | 31 ++++++++++++++----- 6 files changed, 101 insertions(+), 19 deletions(-) diff --git a/docs/conventions/component-convention.md b/docs/conventions/component-convention.md index b9eedfe..401208a 100644 --- a/docs/conventions/component-convention.md +++ b/docs/conventions/component-convention.md @@ -50,6 +50,26 @@ interface RentalItemCardProps { - 색은 `text-[#171719]`처럼 hex를 직접 박지 않는다. `src/index.css`의 `@theme` 블록에 있는 시맨틱 토큰(`text-label-normal`, `bg-background-alternative`, `border-line-solid-neutral`, `text-primary`, `bg-primary-subtle` 등)을 쓴다. 이 토큰들은 `@wanteddev/wds/global.css`가 심어둔 `--semantic-*`/`--atomic-*` CSS 변수를 그대로 별칭 연결한 것이라 다크 테마 전환도 자동으로 따라간다. - 필요한 색이 아직 토큰으로 없으면, hex를 추측해서 쓰지 말고 `get_variable_defs(fileKey, nodeId)`로 해당 노드의 실제 Figma 변수명·값을 확인한 뒤 `index.css`의 `@theme`에 새 토큰을 추가한다. `Line/Normal/Neutral`(반투명 `#70737c29`)과 `Line/Solid/Neutral`(불투명 `#eaebec`)처럼 이름이 비슷해도 값이 다른 토큰이 있으니 이름만 보고 넘겨짚지 않는다. - 컴포넌트 인스턴스가 없는 화면 배경/외곽선처럼 Figma 값이 실제로는 안 보이는 경우(예: Bottom Nav 상단 border가 바로 위 배경과 같은 색이라 안 보였던 사례)도 있다 — 이럴 땐 왜 다른 토큰으로 바꿨는지 주석으로 남긴다. +### 타이포그래피 + +- 글자 크기·굵기를 `text-xs`/`text-[17px]`/`font-semibold`처럼 Tailwind로 직접 짓지 않는다. `@wanteddev/wds`의 `Typography` 컴포넌트를 쓴다 — Figma의 이름 있는 타입 스타일(Headline 2/Bold 등)과 `variant`+`weight` 조합이 1:1로 대응한다(`node_modules/@wanteddev/wds/dist/components/typography/style.js`에서 실측 확인 가능). +- Tailwind에는 11px(Caption 2) 같은 값이 기본 스케일에 없어서, 대충 가까운 `text-xs`(12px)를 썼다가 실제로 다른 크기가 되는 사고가 실제로 있었다(Bottom Nav 탭 라벨). `Typography`를 쓰면 이 스케일 자체가 WDS 값이라 이런 어긋남이 구조적으로 없어진다. +- 색은 `className`의 Tailwind 색상 토큰이 아니라 `Typography`의 `color` prop(예: `color="semantic.label.normal"`)으로 준다. `Typography`가 `color` 미지정 시 `color: inherit`을 자체 CSS-in-JS로 주입하는데, 이 스타일이 Tailwind 유틸리티 레이어보다 우선순위가 높아서(`@wanteddev/wds/global.css`를 `layer(base)`로 감싸야 했던 것과 같은 이유) `className="text-label-normal"`을 같이 줘도 씹힐 수 있다. +- `variant`/`weight` → Figma 이름 대응표(자주 쓰는 것만): + + | `variant` | `weight` | 실제 크기 | Figma 이름 | + |---|---|---|---| + | `headline1` | `bold` | 18px / SemiBold | Headline 1/Bold | + | `headline2` | `bold` | 17px / SemiBold | Headline 2/Bold | + | `headline2` | `medium` | 17px / Medium | Headline 2/Medium | + | `label1` | `bold` | 14px / SemiBold | Label 1/Normal - Bold | + | `caption1` | `bold` | 12px / SemiBold | Caption 1/Bold | + | `caption1` | `medium` | 12px / Medium | Caption 1/Medium | + | `caption1` | `regular` | 12px / Regular | Caption 1/Regular | + | `caption2` | `medium` | 11px / Medium | Caption 2/Medium | + +- **예외**: 서드파티 라이브러리가 `className` 문자열만 받아서 자기 DOM에 그대로 꽂는 자리(예: `@ncdai/react-wheel-picker`의 `classNames` prop)는 `Typography`로 감쌀 수 없다 — 이럴 땐 Tailwind `text-[17px]` 같은 값을 그대로 쓰되, 어느 Figma 타입 스타일을 옮긴 값인지 주석을 남긴다(`BililgeRentalSheet.tsx`의 `WHEEL_CLASS_NAMES` 참고). + - Stream 자체 이미지·아이콘(일러스트, 물품 아이콘 등)은 `download_assets`로 받아 `src/assets/`에 커밋한다. Figma asset URL은 **7일 후 만료**되므로 절대 코드에 그대로 참조하지 않는다. - `data-node-id` 같은 Figma 추적용 속성은 컴포넌트 마크업에 남기지 않는다. 대신 파일 최상단에 원본 Figma 노드를 알 수 있는 주석 한 줄만 남긴다 — 나중에 디자인이 바뀌었을 때 다시 대조할 수 있도록: @@ -60,6 +80,7 @@ interface RentalItemCardProps { ## 5. 완료 기준 체크리스트 - [ ] WDS로 확인된 요소는 전부 import로 대체했다 (raw JSX 없음) +- [ ] 텍스트는 `Typography`(`variant`+`weight`)로 썼다 — 서드파티가 className만 받는 자리가 아닌 이상 `text-xs`/`text-[Npx]` 직접 사용 없음 - [ ] Stream 고유 요소만 새 컴포넌트로 작성했다 - [ ] Props가 Figma variant를 유니온 타입으로 반영한다 - [ ] 이미지/아이콘 asset을 다운로드해 커밋했다 (만료되는 Figma URL 미참조) diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 96da1c1..a76a9bd 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -66,6 +66,12 @@ WDS 컴포넌트만 합산하면 파일 안에서 **약 350회 이상**의 인 코드에서는 `@wanteddev/wds-icon`의 `IconSearch`/`IconBell`/`IconHome`/`IconTicket`/`IconList`로 대응된다(각각 default export를 `index.d.ts`에서 named export로 재노출). `Segmented Control`은 `@wanteddev/wds`의 `SegmentedControl`/`SegmentedControlItem`으로 대응된다. +### `Typography` — 텍스트 스타일은 Figma 인스턴스 스캔에 안 잡혀서 뒤늦게 확인됨 + +위 표들은 Figma의 인스턴스(컴포넌트) 이름을 스캔해서 만든 거라, Figma에서 텍스트 스타일(Text Style)로만 적용되고 별도 컴포넌트 인스턴스가 아닌 타이포그래피는 이 방식으로는 안 잡힌다. 그래서 지금까지 `text-xs`/`text-[17px]` 같은 Tailwind 값을 화면마다 손으로 맞춰왔는데, `@wanteddev/wds`에 Figma의 이름 있는 타입 스타일(Headline 2/Bold 등)과 정확히 대응하는 `Typography` 컴포넌트가 있다는 걸 뒤늦게 확인했다(`node_modules/@wanteddev/wds/dist/components/typography/style.js`). Bottom Nav 탭 라벨을 `text-xs`(12px)로 잘못 만들었던 것도 실제 Figma 값(Caption 2/Medium, 11px)과 어긋난 채로 남아있다가 이번에 확인됐다. + +`variant`+`weight` 조합과 색상 `color` prop 사용법은 `docs/conventions/component-convention.md`의 "타이포그래피" 절 참고. 이후 새 화면을 만들 때는 텍스트 크기를 짐작하지 말고 이 컴포넌트부터 확인한다. + ### 반례 — 빌릴게 필터 Chip은 WDS `Chip/Chip`이 아니었다 위 "WDS 컴포넌트로 확인됨" 표에 `Chip/Chip`이 파일 전체 기준 24개 인스턴스로 확정돼 있다고 해서, **다른 화면의 비슷하게 생긴 칩도 자동으로 WDS라고 가정하면 안 된다.** 빌릴게 화면의 카테고리 필터(전체/전자기기/생활잡화/상비약/위생용품)를 처음 구현할 때 이 표만 보고 재조사 없이 WDS `Chip`을 그대로 썼는데, 실제 Figma 스타일(활성 = 연한 파랑 배경 + 파랑 outline, 비활성 = 회색 outline)이 WDS Chip의 기본 활성 스타일(검정 배경)과 달랐다 — Stream이 로컬로 새로 만든 칩이었다. **스타일이 눈에 띄게 다르면, 이름이 같아 보여도 그 인스턴스는 따로 `get_design_context`로 열어 확인한다.** 코드는 `src/features/rental/components/RentalCategoryFilter.tsx` 참고 (plain ` ); })} diff --git a/src/features/bililge/components/BililgeCategoryFilter.tsx b/src/features/bililge/components/BililgeCategoryFilter.tsx index c4b8cf1..bb9825c 100644 --- a/src/features/bililge/components/BililgeCategoryFilter.tsx +++ b/src/features/bililge/components/BililgeCategoryFilter.tsx @@ -1,3 +1,5 @@ +import { Typography } from "@wanteddev/wds"; + const BILILGE_CATEGORIES = [ "전체", "전자기기", @@ -25,14 +27,24 @@ function BililgeCategoryFilter({ ); })} diff --git a/src/features/bililge/components/BililgeItemCard.tsx b/src/features/bililge/components/BililgeItemCard.tsx index 995599d..5462401 100644 --- a/src/features/bililge/components/BililgeItemCard.tsx +++ b/src/features/bililge/components/BililgeItemCard.tsx @@ -1,5 +1,5 @@ // Figma: Rental Item Card (nodeId 1041:61407) -import { Button } from "@wanteddev/wds"; +import { Button, Typography } from "@wanteddev/wds"; import circleMinusFill from "@/assets/icons/circle-minus-fill.svg"; import circlePlusFill from "@/assets/icons/circle-plus-fill.svg"; @@ -39,10 +39,22 @@ function BililgeItemCard({
-

+ {itemName} -

-

수량 {quantity}

+ + + 수량 {quantity} +
@@ -70,9 +82,16 @@ function BililgeItemCard({ > - + {stepperValue} - + + ); + })} +
+ ); +} + +type ScreenHeaderProps = + | { + variant?: "display"; + title?: ScreenHeaderTitle; + trailing?: ReactNode; + } + | { + variant: "normal"; + title?: ScreenHeaderTitle; + leading?: ReactNode; + trailing?: ReactNode; + }; + +// 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="normal"(모달형 닫기 버튼 등, leading 필요)은 아직 WDS `TopNavigation`을 그대로 쓴다 — +// Figma 쪽 해당 패턴은 안 바뀌었다(component-convention.md "WDS 컴포넌트 내부를 임의로 +// 오버라이드하지 않는다" 원칙 유지). // -// 이 Top Navigation은 WDS Top Navigation/Resource/Contents가 아니라 Stream 로컬 -// 컴포넌트다 — 세로 패딩 12px + Title 3/Bold(32px)로 총 56px인데, WDS display variant는 -// 세로 패딩이 16px 고정이라 64px이 된다. 그래서 레이아웃만 직접 구현하고, 아이콘 버튼 -// (TopNavigationButton)은 그대로 재사용한다. -function ScreenHeader({ title }: ScreenHeaderProps) { +// title이 문자열이면 그대로 렌더링하고, { options, activeIndex } 형태(활성 상태가 있는 경우)면 +// 게시판류의 토글형 2단 타이틀로 렌더링한다. +// +// search variant(타이틀 자리가 검색 필드로 바뀌는 패턴)는 이번 범위에서 뺐다 — +// docs/plans/unified-screen-header.md 참고. 화면이 실제로 생기면 그때 추가한다. +function ScreenHeader(props: ScreenHeaderProps) { + const { title, trailing } = props; + + if (props.variant === "normal") { + return ( + + {title !== undefined && + (isToggleTitle(title) ? ( + + ) : ( + title + ))} + + ); + } + return (
- - {title} - -
- - - - - - +
+ {title !== undefined && + (isToggleTitle(title) ? ( + + ) : ( + + {title} + + ))}
+ {trailing !== undefined && ( +
{trailing}
+ )}
); } 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 (