diff --git a/package.json b/package.json index 24403c2..93b6b61 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "@wanteddev/wds": "^3.12.0", "@wanteddev/wds-icon": "^3.12.0", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.3" }, "devDependencies": { "@biomejs/biome": "^2.5.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 891564d..6eb849c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: react-dom: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) + react-router-dom: + specifier: ^7.18.3 + version: 7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@biomejs/biome': specifier: ^2.5.11 @@ -774,6 +777,10 @@ packages: convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -1101,6 +1108,23 @@ packages: '@types/react': optional: true + react-router-dom@7.18.3: + resolution: {integrity: sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.3: + resolution: {integrity: sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -1132,6 +1156,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1891,6 +1918,8 @@ snapshots: convert-source-map@1.9.0: {} + cookie@1.1.1: {} + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -2132,6 +2161,20 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 + react-router-dom@7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): dependencies: get-nonce: 1.0.1 @@ -2174,6 +2217,8 @@ snapshots: scheduler@0.27.0: {} + set-cookie-parser@2.7.2: {} + source-map-js@1.2.1: {} source-map@0.5.7: {} diff --git a/src/app/App.tsx b/src/app/App.tsx index 3b6b0c1..8dd6739 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,28 +1,16 @@ -import { useState } from "react"; +import { Route, Routes } from "react-router-dom"; -import type { BottomNavValue } from "@/components/ui/BottomNav"; -import ScreenLayout from "@/components/ui/ScreenLayout"; +import HomeScreen from "@/features/home/HomeScreen"; +import RentalListScreen from "@/features/rental/RentalListScreen"; -// 라우팅 도입 전까지 ScreenLayout 뼈대 확인용 임시 화면. 실제 화면 콘텐츠는 각 기능 작업에서 채운다. // 데스크톱에서 보기 좋게 아이폰 화면 크기로 가운데 정렬만 해준다. function App() { - const [bottomNavValue, setBottomNavValue] = useState("home"); - return (
- -

STREAM

-
- } - onBottomNavValueChange={setBottomNavValue} - > -
- 화면 콘텐츠는 각 기능에서 채운다 -
- + + } path="/" /> + } path="/rental" /> + ); } diff --git a/src/assets/icons/circle-minus-fill.svg b/src/assets/icons/circle-minus-fill.svg new file mode 100644 index 0000000..ad62883 --- /dev/null +++ b/src/assets/icons/circle-minus-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/circle-plus-fill.svg b/src/assets/icons/circle-plus-fill.svg new file mode 100644 index 0000000..b00fa3a --- /dev/null +++ b/src/assets/icons/circle-plus-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/rental-items/8pin-charger.svg b/src/assets/icons/rental-items/8pin-charger.svg new file mode 100644 index 0000000..481c548 --- /dev/null +++ b/src/assets/icons/rental-items/8pin-charger.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/alcohol-swab.svg b/src/assets/icons/rental-items/alcohol-swab.svg new file mode 100644 index 0000000..ec57d64 --- /dev/null +++ b/src/assets/icons/rental-items/alcohol-swab.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/band-aid.svg b/src/assets/icons/rental-items/band-aid.svg new file mode 100644 index 0000000..5456739 --- /dev/null +++ b/src/assets/icons/rental-items/band-aid.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/curling-iron.svg b/src/assets/icons/rental-items/curling-iron.svg new file mode 100644 index 0000000..a13d2e6 --- /dev/null +++ b/src/assets/icons/rental-items/curling-iron.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/default.svg b/src/assets/icons/rental-items/default.svg new file mode 100644 index 0000000..8cae4eb --- /dev/null +++ b/src/assets/icons/rental-items/default.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/eye-drops.svg b/src/assets/icons/rental-items/eye-drops.svg new file mode 100644 index 0000000..5a82cbd --- /dev/null +++ b/src/assets/icons/rental-items/eye-drops.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/hair-dryer.svg b/src/assets/icons/rental-items/hair-dryer.svg new file mode 100644 index 0000000..6578d26 --- /dev/null +++ b/src/assets/icons/rental-items/hair-dryer.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/laptop-charger.svg b/src/assets/icons/rental-items/laptop-charger.svg new file mode 100644 index 0000000..20df526 --- /dev/null +++ b/src/assets/icons/rental-items/laptop-charger.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/mask.svg b/src/assets/icons/rental-items/mask.svg new file mode 100644 index 0000000..b1c90a6 --- /dev/null +++ b/src/assets/icons/rental-items/mask.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/ointment.svg b/src/assets/icons/rental-items/ointment.svg new file mode 100644 index 0000000..2820271 --- /dev/null +++ b/src/assets/icons/rental-items/ointment.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/pain-relief-patch.svg b/src/assets/icons/rental-items/pain-relief-patch.svg new file mode 100644 index 0000000..decb03d --- /dev/null +++ b/src/assets/icons/rental-items/pain-relief-patch.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/pill.svg b/src/assets/icons/rental-items/pill.svg new file mode 100644 index 0000000..f399c53 --- /dev/null +++ b/src/assets/icons/rental-items/pill.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/power-bank.svg b/src/assets/icons/rental-items/power-bank.svg new file mode 100644 index 0000000..d2c4340 --- /dev/null +++ b/src/assets/icons/rental-items/power-bank.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/sanitary-pad.svg b/src/assets/icons/rental-items/sanitary-pad.svg new file mode 100644 index 0000000..69382ab --- /dev/null +++ b/src/assets/icons/rental-items/sanitary-pad.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/umbrella.svg b/src/assets/icons/rental-items/umbrella.svg new file mode 100644 index 0000000..fbd0e6a --- /dev/null +++ b/src/assets/icons/rental-items/umbrella.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/rental-items/usb-c-charger.svg b/src/assets/icons/rental-items/usb-c-charger.svg new file mode 100644 index 0000000..b8ec969 --- /dev/null +++ b/src/assets/icons/rental-items/usb-c-charger.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx new file mode 100644 index 0000000..7a89447 --- /dev/null +++ b/src/features/home/HomeScreen.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { Link, useNavigate } 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} + > +
+

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

+ + 빌릴게 화면 보기 + +
+
+ ); +} + +export default HomeScreen; diff --git a/src/features/rental/RentalListScreen.tsx b/src/features/rental/RentalListScreen.tsx new file mode 100644 index 0000000..25e9086 --- /dev/null +++ b/src/features/rental/RentalListScreen.tsx @@ -0,0 +1,88 @@ +import { + SegmentedControl, + SegmentedControlItem, + TopNavigation, + TopNavigationButton, +} 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"; +import { RENTAL_ITEMS } from "@/features/rental/constants/rentalItems"; + +// Figma: 빌릴게 (nodeId 1243:73331) +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 ( + + + 대여 + 반납 + + + } + trailingContent={ + <> + + + + + + + + } + variant="display" + > + 빌릴게 + + } + onBottomNavValueChange={handleBottomNavValueChange} + > +
+ +
+ + {tab === "rent" ? ( +
+ {RENTAL_ITEMS.map((item) => ( + + ))} +
+ ) : ( +
+ 반납 화면은 아직 준비 중이에요 +
+ )} +
+ ); +} + +export default RentalListScreen; diff --git a/src/features/rental/components/RentalCategoryFilter.tsx b/src/features/rental/components/RentalCategoryFilter.tsx new file mode 100644 index 0000000..d34d04b --- /dev/null +++ b/src/features/rental/components/RentalCategoryFilter.tsx @@ -0,0 +1,40 @@ +const RENTAL_CATEGORIES = [ + "전체", + "전자기기", + "생활잡화", + "상비약", + "위생용품", +]; + +interface RentalCategoryFilterProps { + value: string; + onChange: (category: string) => void; +} + +// Figma: 빌릴게 Filter Row (nodeId 1243:73337) — WDS Chip이 아니라 Stream 로컬 커스텀 칩. +// 카테고리·물품 매핑 API가 아직 없어 필터링 없이 선택 상태만 표시한다. +function RentalCategoryFilter({ value, onChange }: RentalCategoryFilterProps) { + return ( +
+ {RENTAL_CATEGORIES.map((category) => { + const active = category === value; + return ( + + ); + })} +
+ ); +} + +export default RentalCategoryFilter; diff --git a/src/features/rental/components/RentalItemCard.tsx b/src/features/rental/components/RentalItemCard.tsx new file mode 100644 index 0000000..3e8a65b --- /dev/null +++ b/src/features/rental/components/RentalItemCard.tsx @@ -0,0 +1,90 @@ +// Figma: Rental Item Card (nodeId 1041:61407) +import { Button } from "@wanteddev/wds"; + +import circleMinusFill from "@/assets/icons/circle-minus-fill.svg"; +import circlePlusFill from "@/assets/icons/circle-plus-fill.svg"; + +interface RentalItemCardProps { + icon: string; + itemName: string; + quantity: number; + trailingControl?: "button" | "stepper"; + onRentRequest?: () => void; + stepperValue?: number; + onStepperDecrease?: () => void; + onStepperIncrease?: () => void; +} + +// "대여 신청" 버튼은 Figma상 `Menu/Resource/Action Area/Trailing Content/Button`(WDS) 인스턴스지만, +// 그 이름과 1:1 대응하는 코드 export는 없다 — 대신 WDS `Button`(size="small")이 padding(7px/14px)· +// radius(8px)·타이포(label2)까지 정확히 일치해서 그걸 쓰고, Button 공개 variant엔 없는 "연한 파랑 배경 +// + 파랑 텍스트" 조합만 sx로 보정했다. 스테퍼 +/- 아이콘은 여전히 WDS 미확정이라 하드코딩 — docs/plans 참고. +function RentalItemCard({ + icon, + itemName, + quantity, + trailingControl = "button", + onRentRequest, + stepperValue = 1, + onStepperDecrease, + onStepperIncrease, +}: RentalItemCardProps) { + const isStepper = trailingControl === "stepper"; + + return ( +
+
+
+ +
+

+ {itemName} +

+

수량 {quantity}

+
+
+ + {!isStepper && ( + + )} + + {isStepper && ( +
+ + + {stepperValue} + + +
+ )} +
+
+ ); +} + +export default RentalItemCard; diff --git a/src/features/rental/constants/rentalItems.ts b/src/features/rental/constants/rentalItems.ts new file mode 100644 index 0000000..c3680f9 --- /dev/null +++ b/src/features/rental/constants/rentalItems.ts @@ -0,0 +1,75 @@ +import eightPinCharger from "@/assets/icons/rental-items/8pin-charger.svg"; +import alcoholSwab from "@/assets/icons/rental-items/alcohol-swab.svg"; +import bandAid from "@/assets/icons/rental-items/band-aid.svg"; +import curlingIron from "@/assets/icons/rental-items/curling-iron.svg"; +import eyeDrops from "@/assets/icons/rental-items/eye-drops.svg"; +import hairDryer from "@/assets/icons/rental-items/hair-dryer.svg"; +import laptopCharger from "@/assets/icons/rental-items/laptop-charger.svg"; +import mask from "@/assets/icons/rental-items/mask.svg"; +import ointment from "@/assets/icons/rental-items/ointment.svg"; +import painReliefPatch from "@/assets/icons/rental-items/pain-relief-patch.svg"; +import pill from "@/assets/icons/rental-items/pill.svg"; +import powerBank from "@/assets/icons/rental-items/power-bank.svg"; +import sanitaryPad from "@/assets/icons/rental-items/sanitary-pad.svg"; +import umbrella from "@/assets/icons/rental-items/umbrella.svg"; +import usbCCharger from "@/assets/icons/rental-items/usb-c-charger.svg"; + +export interface RentalItem { + id: string; + name: string; + quantity: number; + icon: string; +} + +// Figma: 빌릴게 Item Grid (nodeId 1243:73343) 순서·물품명·수량을 그대로 옮긴 목데이터 — 실 API 연동 전까지 사용 +export const RENTAL_ITEMS: RentalItem[] = [ + { icon: curlingIron, id: "curling-iron", name: "고데기", quantity: 28 }, + { + icon: laptopCharger, + id: "laptop-charger", + name: "노트북 충전기(C타입)", + quantity: 28, + }, + { icon: hairDryer, id: "hair-dryer", name: "드라이기", quantity: 28 }, + { icon: mask, id: "mask", name: "마스크", quantity: 28 }, + { icon: bandAid, id: "band-aid", name: "밴드", quantity: 28 }, + { icon: powerBank, id: "power-bank", name: "보조배터리", quantity: 28 }, + { + icon: sanitaryPad, + id: "sanitary-pad-large", + name: "생리대(대형)", + quantity: 28, + }, + { + icon: sanitaryPad, + id: "sanitary-pad-small", + name: "생리대(소형)", + quantity: 28, + }, + { icon: alcoholSwab, id: "alcohol-swab", name: "알콜스왑", quantity: 28 }, + { icon: umbrella, id: "umbrella", name: "우산", quantity: 28 }, + { icon: pill, id: "stomach-medicine", name: "위장약", quantity: 28 }, + { icon: pill, id: "easyen-6", name: "이지엔6", quantity: 28 }, + { icon: eyeDrops, id: "eye-drops", name: "인공눈물", quantity: 28 }, + { icon: pill, id: "cold-medicine", name: "종합감기약", quantity: 28 }, + { + icon: usbCCharger, + id: "usb-c-cable", + name: "케이블(C to C)", + quantity: 28, + }, + { icon: pill, id: "tylenol", name: "타이레놀", quantity: 28 }, + { + icon: painReliefPatch, + id: "pain-relief-patch", + name: "파스", + quantity: 28, + }, + { icon: ointment, id: "ointment", name: "후시딘", quantity: 28 }, + { + icon: eightPinCharger, + id: "8pin-charger", + name: "8핀 충전기", + quantity: 28, + }, +]; diff --git a/src/main.tsx b/src/main.tsx index cd7e812..3a3dd4d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,16 @@ import { ThemeProvider } from "@wanteddev/wds"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import App from "./app/App.tsx"; import "./index.css"; createRoot(document.getElementById("root")!).render( - + + + , );