diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index 9408187..0546aed 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -63,7 +63,9 @@ function BililgeListScreen() {
{tab === "rent" ? (
- {BILILGE_ITEMS.map((item) => ( + {BILILGE_ITEMS.filter( + (item) => category === "전체" || item.category === category, + ).map((item) => ( [] = Array.from( (_, i) => ({ label: String(i).padStart(2, "0"), value: i }), ); +// 칸 사이 세로 간격(행 간 pitch)이 Figma보다 좁아 보인다는 QA 지적(디자인 QA 페이지 +// nodeId 2849:56598의 "빌릴게 대여 바텀시트" 비교 항목, 구현 스크린샷의 오전-오후 사이에 +// 빨간 화살표로 표시됨) — 기존 24는 선택 안 된 줄의 텍스트 블록 높이(17px 폰트 × line-height +// 1.412 ≈ 24)를 그대로 가져다 쓴 값이라, 실제 Figma의 줄 간 pitch(선택 줄 높이 26 + gap 6 + +// 비선택 줄 높이 24를 반씩 걸쳐 계산하면 ≈31px, Time Picker 스크린샷에서 실측해도 31px)보다 +// 작았다. WheelPicker 기본값(30)에 더 가까운 이 값으로 세 컬럼 모두 맞춘다. +const OPTION_ITEM_HEIGHT = 31; + // 선택되지 않은 칸은 옅게, 가운데 선택된 칸만 진하게 — 배경 하이라이트 바는 Time Picker 쪽에서 // 3개 컬럼 공통으로 하나 깔아주기 때문에 여기서는 텍스트 스타일만 다룬다. // 여기는 @ncdai/react-wheel-picker가 className 문자열만 받아서 자기 DOM에 그대로 꽂는 자리라 @@ -121,7 +129,7 @@ function BililgeRentalSheet({ item, open, onClose }: BililgeRentalSheetProps) { onValueChange={(period) => setTime((prev) => ({ ...prev, period })) } - optionItemHeight={24} + optionItemHeight={OPTION_ITEM_HEIGHT} options={PERIOD_OPTIONS} value={time.period} visibleCount={12} @@ -133,7 +141,7 @@ function BililgeRentalSheet({ item, open, onClose }: BililgeRentalSheetProps) { onValueChange={(hour) => setTime((prev) => ({ ...prev, hour })) } - optionItemHeight={24} + optionItemHeight={OPTION_ITEM_HEIGHT} options={HOUR_OPTIONS} value={time.hour} visibleCount={12} @@ -145,7 +153,7 @@ function BililgeRentalSheet({ item, open, onClose }: BililgeRentalSheetProps) { onValueChange={(minute) => setTime((prev) => ({ ...prev, minute })) } - optionItemHeight={24} + optionItemHeight={OPTION_ITEM_HEIGHT} options={MINUTE_OPTIONS} value={time.minute} visibleCount={12} diff --git a/src/features/bililge/components/BililgeReturnSection.tsx b/src/features/bililge/components/BililgeReturnSection.tsx index 178d0a1..13c8f58 100644 --- a/src/features/bililge/components/BililgeReturnSection.tsx +++ b/src/features/bililge/components/BililgeReturnSection.tsx @@ -1,22 +1,18 @@ import { Button, Divider, Typography } from "@wanteddev/wds"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { Fragment, useState } from "react"; import rentalHistoryEmptyIllustration from "@/assets/icons/bililge-empty/rental-history.svg"; import returnItemsEmptyIllustration from "@/assets/icons/bililge-empty/return-items.svg"; +import ScreenToast from "@/components/ui/ScreenToast"; import BililgeEmptyState from "@/features/bililge/components/BililgeEmptyState"; import BililgeItemCard from "@/features/bililge/components/BililgeItemCard"; import BililgeRentalHistoryEntry from "@/features/bililge/components/BililgeRentalHistoryEntry"; import BililgeReturnConfirmModal from "@/features/bililge/components/BililgeReturnConfirmModal"; -import BililgeReturnToast from "@/features/bililge/components/BililgeReturnToast"; import { BILILGE_RENTAL_HISTORY, BILILGE_RETURN_ITEMS, } from "@/features/bililge/constants/bililgeReturns"; -// 토스트 자동 닫힘 시간 — Figma엔 지속시간이 없어서 WDS Toast의 duration="short" 기본값과 -// 맞췄다(node_modules/@wanteddev/wds/dist/components/toast/index.mjs, short=3000ms). -const TOAST_DURATION_MS = 3000; - interface BililgeReturnSectionProps { onBrowseRentals?: () => void; } @@ -35,36 +31,18 @@ interface BililgeReturnSectionProps { function BililgeReturnSection({ onBrowseRentals }: BililgeReturnSectionProps) { const [confirmOpen, setConfirmOpen] = useState(false); const [toastOpen, setToastOpen] = useState(false); - // 토스트가 떠 있는 동안 다시 확인해도 스크린리더가 재안내하도록, 확인마다 값을 바꿔 메시지 - // 텍스트를 새로 마운트한다(BililgeReturnToast의 messageKey로 전달). + // 토스트가 떠 있는 동안 다시 확인해도 스크린리더가 재안내하도록, 확인마다 값을 바꿔 + // ScreenToast를 새로 마운트한다(EventsApplicationScreen의 실패 토스트와 같은 패턴). const [toastToken, setToastToken] = useState(0); - // 타이머를 이펙트가 아니라 ref로 직접 관리한다 — 토스트가 이미 떠 있는 채로 다시 확인하면 - // "새 확인 시점부터 3초"가 되도록, 기존 타이머를 지우고 새로 시작해야 하기 때문이다. - const toastTimerRef = useRef | null>(null); - - useEffect(() => { - return () => { - if (toastTimerRef.current) { - clearTimeout(toastTimerRef.current); - } - }; - }, []); const handleConfirm = () => { setConfirmOpen(false); setToastOpen(true); setToastToken((token) => token + 1); - if (toastTimerRef.current) { - clearTimeout(toastTimerRef.current); - } - toastTimerRef.current = setTimeout( - () => setToastOpen(false), - TOAST_DURATION_MS, - ); }; return ( -
+
{BILILGE_RETURN_ITEMS.length > 0 ? ( -
+
{BILILGE_RETURN_ITEMS.map((item) => ( - +
); } diff --git a/src/features/bililge/components/BililgeReturnToast.tsx b/src/features/bililge/components/BililgeReturnToast.tsx deleted file mode 100644 index b3f06dc..0000000 --- a/src/features/bililge/components/BililgeReturnToast.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Typography } from "@wanteddev/wds"; -import { IconCircleCheckFill } from "@wanteddev/wds-icon"; -import { createPortal } from "react-dom"; - -import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; - -interface BililgeReturnToastProps { - open: boolean; - // 토스트가 떠 있는 중에 다시 확인해도 스크린리더가 재안내하도록, 확인마다 바뀌는 값을 - // 메시지 텍스트의 key로 써서 강제로 다시 마운트한다. - messageKey: number; -} - -// Figma: 반납 신청 완료 토스트(nodeId 1133:50035, Toast/Toast) — WDS `useToast`/`Toast`는 -// 화면 전용 포털(#wds-region-manager-bottom)에 그려서 실제 뷰포트 기준으로 뜬다. 이 앱은 -// 데스크톱에서 폭이 고정된 컬럼을 화면 가운데에 세우는 구조라(App.tsx), WDS 토스트를 그대로 -// 쓰면 컬럼 밖 실제 뷰포트 하단에 떠버린다 — Bottom Nav를 WDS 대신 로컬로 다시 만든 것과 -// 같은 이유(wds-component-usage.md "Bottom Nav" 절 참고)로 BottomSheet와 같은 포털 슬롯에 -// 직접 그린다. 아이콘·타이포·배경(두 겹 반투명 레이어)은 Figma 그대로 옮겼다. -// -// role="status"/aria-live="polite"로 스크린리더에도 안내한다(코드리뷰 지적 반영). 메시지 -// 텍스트에 messageKey를 key로 줘서, 토스트가 떠 있는 중에 다시 확인해도(연속 확인) 텍스트가 -// 새로 마운트되며 재안내되게 한다. -function BililgeReturnToast({ open, messageKey }: BililgeReturnToastProps) { - const portalEl = useScreenSheetPortal(); - - if (!portalEl) { - return null; - } - - return createPortal( -
-
-
-
- - - 반납 신청이 완료됐어요. - -
-
, - portalEl, - ); -} - -export default BililgeReturnToast; diff --git a/src/features/bililge/constants/bililgeItems.ts b/src/features/bililge/constants/bililgeItems.ts index 4f2f477..d0cabd9 100644 --- a/src/features/bililge/constants/bililgeItems.ts +++ b/src/features/bililge/constants/bililgeItems.ts @@ -14,59 +14,149 @@ import sanitaryPad from "@/assets/icons/bililge-items/sanitary-pad.svg"; import umbrella from "@/assets/icons/bililge-items/umbrella.svg"; import usbCCharger from "@/assets/icons/bililge-items/usb-c-charger.svg"; +// BililgeCategoryFilter의 필터 칩과 값을 맞춘다("전체"는 필터 없음을 뜻하는 값이라 제외). +export type BililgeCategory = "전자기기" | "생활잡화" | "상비약" | "위생용품"; + export interface BililgeItem { id: string; name: string; quantity: number; icon: string; + category: BililgeCategory; } -// Figma: 빌릴게 Item Grid (nodeId 1243:73343) 순서·물품명·수량을 그대로 옮긴 목데이터 — 실 API 연동 전까지 사용 +// Figma: 빌릴게 Item Grid (nodeId 1243:73343) 순서·물품명·수량을 그대로 옮긴 목데이터 — 실 API 연동 전까지 사용. +// category는 Figma에 카테고리별 그룹핑이 따로 없어(칩 자체만 정의돼 있음) 물품명 기준으로 임의 +// 분류했다 — 실제 분류 기준이 정해지면 API 연동 시 교체한다. export const BILILGE_ITEMS: BililgeItem[] = [ - { icon: curlingIron, id: "curling-iron", name: "고데기", quantity: 28 }, { + category: "전자기기", + icon: curlingIron, + id: "curling-iron", + name: "고데기", + quantity: 28, + }, + { + category: "전자기기", 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 }, { + category: "전자기기", + icon: hairDryer, + id: "hair-dryer", + name: "드라이기", + quantity: 28, + }, + { + category: "위생용품", + icon: mask, + id: "mask", + name: "마스크", + quantity: 28, + }, + { + category: "상비약", + icon: bandAid, + id: "band-aid", + name: "밴드", + quantity: 28, + }, + { + category: "전자기기", + icon: powerBank, + id: "power-bank", + name: "보조배터리", + quantity: 28, + }, + { + category: "위생용품", icon: sanitaryPad, id: "sanitary-pad-large", name: "생리대(대형)", quantity: 28, }, { + category: "위생용품", 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 }, { + category: "상비약", + icon: alcoholSwab, + id: "alcohol-swab", + name: "알콜스왑", + quantity: 28, + }, + { + category: "생활잡화", + icon: umbrella, + id: "umbrella", + name: "우산", + quantity: 28, + }, + { + category: "상비약", + icon: pill, + id: "stomach-medicine", + name: "위장약", + quantity: 28, + }, + { + category: "상비약", + icon: pill, + id: "easyen-6", + name: "이지엔6", + quantity: 28, + }, + { + category: "상비약", + icon: eyeDrops, + id: "eye-drops", + name: "인공눈물", + quantity: 28, + }, + { + category: "상비약", + icon: pill, + id: "cold-medicine", + name: "종합감기약", + quantity: 28, + }, + { + category: "전자기기", icon: usbCCharger, id: "usb-c-cable", name: "케이블(C to C)", quantity: 28, }, - { icon: pill, id: "tylenol", name: "타이레놀", quantity: 28 }, { + category: "상비약", + icon: pill, + id: "tylenol", + name: "타이레놀", + quantity: 28, + }, + { + category: "상비약", icon: painReliefPatch, id: "pain-relief-patch", name: "파스", quantity: 28, }, - { icon: ointment, id: "ointment", name: "후시딘", quantity: 28 }, { + category: "상비약", + icon: ointment, + id: "ointment", + name: "후시딘", + quantity: 28, + }, + { + category: "전자기기", icon: eightPinCharger, id: "8pin-charger", name: "8핀 충전기",