Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion src/features/bililge/BililgeListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ function BililgeListScreen() {
<div className="scrollbar-hidden flex-1 overflow-y-auto">
{tab === "rent" ? (
<div className="flex flex-col gap-2 px-5 pb-4">
{BILILGE_ITEMS.map((item) => (
{BILILGE_ITEMS.filter(
(item) => category === "전체" || item.category === category,
).map((item) => (
<BililgeItemCard
icon={item.icon}
itemName={item.name}
Expand Down
3 changes: 2 additions & 1 deletion src/features/bililge/components/BililgeCategoryFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ interface BililgeCategoryFilterProps {
}

// Figma: 빌릴게 Filter Row (nodeId 1243:73337) — 칩 자체는 행사 화면과 공유하는 FilterChipGroup을 쓴다.
// 카테고리·물품 매핑 API가 아직 없어 필터링 없이 선택 상태만 표시한다.
// 실제 목록 필터링은 BililgeListScreen이 BILILGE_ITEMS의 category 필드를 기준으로 한다
// (bililgeItems.ts 참고) — 이 컴포넌트는 선택 상태 표시·onChange 전달만 맡는다.
function BililgeCategoryFilter({
value,
onChange,
Expand Down
14 changes: 11 additions & 3 deletions src/features/bililge/components/BililgeRentalSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ const MINUTE_OPTIONS: WheelPickerOption<number>[] = 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에 그대로 꽂는 자리라
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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}
Expand Down
42 changes: 13 additions & 29 deletions src/features/bililge/components/BililgeReturnSection.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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<ReturnType<typeof setTimeout> | 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 (
<div className="flex flex-col gap-8 px-5 pt-6">
<div className="flex flex-col gap-8 px-5 pt-7">
<div className="flex flex-col gap-3">
<Typography
as="p"
Expand All @@ -75,7 +53,7 @@ function BililgeReturnSection({ onBrowseRentals }: BililgeReturnSectionProps) {
반납이 필요한 물품
</Typography>
{BILILGE_RETURN_ITEMS.length > 0 ? (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
{BILILGE_RETURN_ITEMS.map((item) => (
<BililgeItemCard
actionLabel="반납 신청"
Expand Down Expand Up @@ -145,7 +123,13 @@ function BililgeReturnSection({ onBrowseRentals }: BililgeReturnSectionProps) {
onConfirm={handleConfirm}
open={confirmOpen}
/>
<BililgeReturnToast messageKey={toastToken} open={toastOpen} />
<ScreenToast
key={toastToken}
message="반납 신청이 완료됐어요."
onOpenChange={setToastOpen}
open={toastOpen}
variant="positive"
/>
</div>
);
}
Expand Down
60 changes: 0 additions & 60 deletions src/features/bililge/components/BililgeReturnToast.tsx

This file was deleted.

118 changes: 104 additions & 14 deletions src/features/bililge/constants/bililgeItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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핀 충전기",
Expand Down