Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ad19585
refactor: Bottom Nav 활성 탭을 화면 state 대신 라우트에서 파생
xeoxxn Sep 9, 2026
6647381
refactor: ScreenLayout을 라우터 레벨로 이동, useScreenHeader 훅 도입
xeoxxn Sep 9, 2026
8730156
feat: 재사용 가능한 ScreenHeader 컴포넌트 분리
xeoxxn Sep 9, 2026
5655e51
refactor: 대여/반납 토글을 헤더에서 화면 요소로 분리, 필터 고정+목록만 스크롤
xeoxxn Sep 9, 2026
983e481
docs: 용어 사전 컨벤션 추가
xeoxxn Sep 9, 2026
bf4693b
refactor: rental → bililge 네이밍 통일
xeoxxn Sep 9, 2026
f5c3691
chore: main에서 컴포넌트 관련 스킬·컨벤션 문서 가져오기
xeoxxn Sep 9, 2026
0047473
feat: 대여 신청 바텀시트 추가(휠 피커로 대여 시작 시간 선택)
xeoxxn Sep 9, 2026
94b12de
fix: 휠 피커·스테퍼 숫자 폭 고정해서 자릿수 바뀌어도 안 밀리게 수정
xeoxxn Sep 9, 2026
b50014c
fix: 휠 피커 선택 줄 뒤 회색 글자가 비쳐서 두꺼워 보이던 문제 수정
xeoxxn Sep 9, 2026
cf90bc0
fix: figma-check 결과 반영 — 시트 상단 여백, 안내 문구 굵기 Figma에 맞춤
xeoxxn Sep 9, 2026
2e6144e
fix: 바텀시트 하단 세이프에어리어에 누락된 홈 인디케이터 바 추가
xeoxxn Sep 9, 2026
da0758b
revert: 홈 인디케이터 바 제거, 세이프에어리어 여백만 유지
xeoxxn Sep 9, 2026
e7887bf
fix: 바텀시트 하단 여백을 34px로 과다 계산했던 것 14px로 수정
xeoxxn Sep 9, 2026
fc643d4
fix: Bottom Nav 하단 검은색 홈 인디케이터 바 제거, 여백만 유지
xeoxxn Sep 9, 2026
d6d9f2b
fix: 바텀시트 슬라이드 애니메이션을 iOS 스타일 감속 곡선으로 자연스럽게 조정
xeoxxn Sep 9, 2026
4142283
perf: 바텀시트 첫 오픈 때 휠 피커 마운트가 슬라이드 첫 프레임을 막던 문제 수정
xeoxxn Sep 10, 2026
acae08d
refactor: 텍스트 스타일을 WDS Typography 컴포넌트로 전환, 타이포그래피 컨벤션 추가
xeoxxn Sep 10, 2026
a6d8310
fix: 빌릴게/홈 헤더를 Figma 로컬 Top Navigation 사양(56px)에 맞춤
xeoxxn Sep 14, 2026
a3d920f
fix: 카테고리 필터 칩 높이를 Figma 스펙(32px)에 맞춤
xeoxxn Sep 14, 2026
76bdae6
fix: 대여 신청 버튼 높이를 Figma 스펙(56px)에 맞춤
xeoxxn Sep 14, 2026
35bd916
refactor: 화면 스크롤을 위한 h-full을 ScreenLayout에서 기본 보장
xeoxxn Sep 14, 2026
aa71a94
[Refactor/#23] 화면 헤더를 ScreenHeader 하나로 통합 (#24)
xeoxxn Sep 14, 2026
d79b3a3
Merge remote-tracking branch 'origin/main' into feat/#13-router-renta…
xeoxxn Sep 14, 2026
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
22 changes: 22 additions & 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 All @@ -50,6 +51,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 노드를 알 수 있는 주석 한 줄만 남긴다 — 나중에 디자인이 바뀌었을 때 다시 대조할 수 있도록:

Expand All @@ -60,6 +81,7 @@ interface RentalItemCardProps {
## 5. 완료 기준 체크리스트

- [ ] WDS로 확인된 요소는 전부 import로 대체했다 (raw JSX 없음)
- [ ] 텍스트는 `Typography`(`variant`+`weight`)로 썼다 — 서드파티가 className만 받는 자리가 아닌 이상 `text-xs`/`text-[Npx]` 직접 사용 없음
- [ ] Stream 고유 요소만 새 컴포넌트로 작성했다
- [ ] Props가 Figma variant를 유니온 타입으로 반영한다
- [ ] 이미지/아이콘 asset을 다운로드해 커밋했다 (만료되는 Figma URL 미참조)
Expand Down
19 changes: 18 additions & 1 deletion docs/conventions/wds-component-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<button>` 기반, WDS import 없음).
Expand All @@ -91,7 +97,18 @@ WDS 컴포넌트만 합산하면 파일 안에서 **약 350회 이상**의 인
</Button>
```

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/`), 이건 `<input>` 기반 텍스트 필드형 컴포넌트라 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 아님)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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() {
return (
<div className="flex min-h-screen items-center justify-center bg-[#e5e5e5] py-6">
<Routes>
<Route element={<HomeScreen />} path="/" />
<Route element={<RentalListScreen />} path="/rental" />
<Route element={<ScreenLayout />}>
<Route element={<HomeScreen />} path="/" />
<Route element={<BililgeListScreen />} path="/bililge" />
</Route>
</Routes>
</div>
);
Expand Down
25 changes: 15 additions & 10 deletions src/components/ui/BottomNav.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Typography } from "@wanteddev/wds";
import {
IconHome,
IconList,
IconStorage,
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;
Expand Down Expand Up @@ -47,8 +48,8 @@ const TABS: BottomNavTab[] = [
{
NormalIcon: IconStorage,
label: "빌릴게",
selectedIcon: rentalSelected,
value: "rental",
selectedIcon: bililgeSelected,
value: "bililge",
},
];

Expand All @@ -75,18 +76,22 @@ function BottomNav({ value, onValueChange }: BottomNavProps) {
) : (
<NormalIcon className="size-6 text-label-assistive" />
)}
<span
className={`font-medium text-xs ${isSelected ? "text-primary" : "text-label-assistive"}`}
<Typography
color={
isSelected
? "semantic.primary.normal"
: "semantic.label.assistive"
}
variant="caption2"
weight="medium"
>
{label}
</span>
</Typography>
</button>
);
})}
</div>
<div className="relative h-[34px] w-full">
<div className="absolute bottom-2 left-1/2 h-[5px] w-[134px] -translate-x-1/2 rounded-full bg-icons-primary" />
</div>
<div className="h-[34px] w-full" />
</div>
);
}
Expand Down
56 changes: 56 additions & 0 deletions src/components/ui/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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(
<div
className={`absolute inset-0 transition-opacity duration-300 ease-out ${
open
? "pointer-events-auto opacity-100"
: "pointer-events-none opacity-0"
}`}
>
<button
aria-label="시트 닫기"
className="absolute inset-0 bg-black/40"
onClick={onClose}
type="button"
/>
<div
className={`absolute inset-x-0 bottom-0 flex flex-col rounded-t-2xl bg-background-normal transition-transform duration-[380ms] ease-[cubic-bezier(0.32,0.72,0,1)] ${
open ? "translate-y-0" : "translate-y-full"
}`}
>
<div className="mb-2 flex h-6 shrink-0 items-center justify-center">
<div className="h-[5px] w-12 rounded-full bg-sheet-indicator" />
</div>
{children}
{/* Figma의 Action Area는 버튼을 감싸는 Container(p-5=20px, 이건 WDS ActionArea 자체
padding으로 이미 확보됨) 다음에 iOS 홈 인디케이터용 "Gesture" 여백(pt-3.5=14px)이
하나 더 붙는데, 실제 @wanteddev/wds의 ActionArea 컴포넌트에는 이 14px이 없어서
여기서 더해준다. */}
<div className="h-[14px] shrink-0" />
</div>
</div>,
portalEl,
);
}

export default BottomSheet;
Loading