From 31716a2076b222b71ad2426fce45b83bd52ad6c0 Mon Sep 17 00:00:00 2001 From: sangrae Date: Sun, 11 Oct 2026 00:41:25 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EB=B9=8C=EB=A6=B4=EA=B2=8C=20?= =?UTF-8?q?=EB=8C=80=EC=97=AC/=EB=B0=98=EB=82=A9=20=ED=83=AD=EC=9D=84=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EB=82=98=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router.tsx | 5 +++++ src/components/ui/ScreenLayout.tsx | 4 ++++ src/features/bililge/BililgeListScreen.tsx | 21 +++++++++++++++++---- src/features/bililge/constants/bililge.ts | 8 ++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/features/bililge/constants/bililge.ts diff --git a/src/app/router.tsx b/src/app/router.tsx index 734f11d..819598b 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -47,6 +47,11 @@ const routes = [ element: , path: "/bililge", }, + { + element: , + // 빌릴게의 반납 탭 — 같은 화면이 주소로 탭을 고른다 + path: "/bililge/returns", + }, { element: , // 카드 없이 구분선으로만 나뉘는 목록이라 화면 전체가 흰 면이다 diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index c40ae99..62c2c1a 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -37,6 +37,10 @@ function getBottomNavValueFromPath(pathname: string): BottomNavValue { if (pathname === "/events/applications") { return "event"; } + // 빌릴게의 반납 탭(/bililge/returns)도 "빌릴게" 탭 소속이다(BILILGE_TAB_PATHS와 같은 경로) + if (pathname === "/bililge/returns") { + return "bililge"; + } return "home"; } diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index ef0f713..175f489 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -5,7 +5,7 @@ import { } from "@wanteddev/wds"; import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { Suspense, startTransition, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; @@ -15,12 +15,21 @@ import BililgeItemList from "@/features/bililge/components/BililgeItemList"; import BililgeListSkeleton from "@/features/bililge/components/BililgeListSkeleton"; import BililgeRentalSheet from "@/features/bililge/components/BililgeRentalSheet"; import BililgeReturnSection from "@/features/bililge/components/BililgeReturnSection"; +import { + BILILGE_TAB_PATHS, + type BililgeTab, +} from "@/features/bililge/constants/bililge"; import { getSearchPath } from "@/features/search/constants/search"; // Figma: 빌릴게 (nodeId 1243:73331) function BililgeListScreen() { const navigate = useNavigate(); - const [tab, setTab] = useState("rent"); + const { pathname } = useLocation(); + const tab: BililgeTab = + pathname === BILILGE_TAB_PATHS.return ? "return" : "rent"; + // 탭을 바꿀 때마다 히스토리가 쌓이지 않게 replace로 바꾼다 + const changeTab = (next: BililgeTab) => + navigate(BILILGE_TAB_PATHS[next], { replace: true }); const [category, setCategory] = useState("전체"); const [rentalItem, setRentalItem] = useState(null); const [rentalSheetOpen, setRentalSheetOpen] = useState(false); @@ -52,7 +61,11 @@ function BililgeListScreen() { Figma Tool 프레임(56~88)은 높이 32에 위아래 여백이 없다 — 세로 패딩을 주면 토글과 그 아래 필터 행이 함께 밀린다(행사·게시판 화면과 같은 규칙). */}
- + changeTab(value as BililgeTab)} + size="small" + value={tab} + > 대여 반납 @@ -84,7 +97,7 @@ function BililgeListScreen() { /> ) : ( - setTab("rent")} /> + changeTab("rent")} /> )}
diff --git a/src/features/bililge/constants/bililge.ts b/src/features/bililge/constants/bililge.ts new file mode 100644 index 0000000..45c77e7 --- /dev/null +++ b/src/features/bililge/constants/bililge.ts @@ -0,0 +1,8 @@ +// 빌릴게의 대여/반납 탭은 경로로 나눈다(행사 목록 EVENTS_TAB_PATHS와 같은 방식) — 새로고침·뒤로가기에도 +// 탭이 유지되고, 홈 대여 현황 "더보기"처럼 다른 화면에서도 반납 탭으로 바로 보낼 수 있다. +export const BILILGE_TAB_PATHS = { + rent: "/bililge", + return: "/bililge/returns", +} as const; + +export type BililgeTab = keyof typeof BILILGE_TAB_PATHS; From 9e1be5ffc030af1f247600e975c7cc59d0c7348f Mon Sep 17 00:00:00 2001 From: sangrae Date: Sun, 11 Oct 2026 00:49:08 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=ED=99=88=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=8C=80=EC=83=81=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=AA=A9?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/events/eventsMock.ts | 22 ++++++++--- src/features/home/constants/homeMock.ts | 51 ++++++++++++++++++++----- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/entities/events/eventsMock.ts b/src/entities/events/eventsMock.ts index d240c3f..1901932 100644 --- a/src/entities/events/eventsMock.ts +++ b/src/entities/events/eventsMock.ts @@ -38,14 +38,24 @@ const MOCK_DESCRIPTION = `안녕하십니까, 제10대 소프트웨어융합대 // Figma: 행사 Event List (nodeId 1243:70866) 문구를 그대로 옮긴 목데이터 — 실 API 연동 전까지 사용. // // Figma 목업은 같은 행사명 4개(모집중 1 / 모집예정 1 / 모집종료 2)를 상태만 바꿔 보여주는데, -// 여기서는 모집중을 비우고 모집종료 중복도 하나로 줄였다(모집예정 1 / 모집종료 1). +// 여기서는 모집종료 중복을 하나로 줄였다(모집중 1 / 모집예정 1 / 모집종료 1). // -// Empty State(1165:62713)는 Figma가 "모집중" 필터 버전으로만 그려져 있고, 일러스트·문구와 -// "아카이빙 둘러보기" 버튼이 그 조합의 스펙이다. 목데이터에 모집중 항목이 있으면 이 화면을 -// 아예 볼 수 없어서 모집중을 비웠다. 대신 모집중 상세(1133:42433)는 카드로 진입할 수 없다 — -// 실 API가 붙으면 사라질 제약이고, 지금 확인이 필요하면 아래 항목 하나의 status를 "open"으로 -// 되돌리면 된다. 모집예정은 상세 디자인(1156:53992)이 있어 진입 가능하게 남겼다. +// 모집중은 홈 신청 카드가 가리키는 행사라 남겨 둔다(#114). 그래서 모집중 필터의 Empty State(1165:62713, +// "아카이빙 둘러보기" 버튼)는 아래 모집중 항목을 빼야 볼 수 있다. export const EVENTS: EventItem[] = [ + { + actionLabel: "신청하기", + audience: MOCK_AUDIENCE, + description: MOCK_DESCRIPTION, + eventDate: "행사일 2026.06.04", + id: "sw-sports-day-open", + imageCount: 7, + location: MOCK_LOCATION, + schedule: MOCK_SCHEDULE, + status: "open", + statusLabel: "신청마감 D-2", + title: "소프트웨어융합대학 체육대회", + }, { actionLabel: "모집종료", audience: MOCK_AUDIENCE, diff --git a/src/features/home/constants/homeMock.ts b/src/features/home/constants/homeMock.ts index 8c44e09..38716c7 100644 --- a/src/features/home/constants/homeMock.ts +++ b/src/features/home/constants/homeMock.ts @@ -16,17 +16,19 @@ export type HomeNoticeTemplate = export interface HomeNotice { id: string; + // 배너를 누르면 여는 공지 상세. 공지가 없을 때의 기본 배너는 열 공지가 없어 비운다 + noticeId?: string; template: HomeNoticeTemplate; title: string; subtitle: string; } -// 모집 중인 행사·사물함 신청 안내 카드 -export interface HomeApplyCard { +// 모집 중인 행사·사물함 신청 안내 카드. 행사는 상세·신청 화면으로, 사물함은 사물함 신청으로 간다. +export type HomeApplyCard = { id: string; title: string; daysUntilDeadline: number; -} +} & ({ kind: "event"; eventId: string } | { kind: "locker" }); export interface HomeRental { id: string; @@ -36,8 +38,12 @@ export interface HomeRental { hoursUntilDue: number; } +// applicationId는 신청내역 목데이터(entities/events)의 신청완료 건으로 맞춘다. 홈 행 이름(Figma 홈 문구)과 +// 신청내역의 행사명은 목데이터라 서로 다르다. export interface HomeAppliedEvent { id: string; + // 행을 누르면 여는 신청내역 상세 + applicationId: string; name: string; daysUntilEvent: number; } @@ -61,51 +67,69 @@ export interface HomeData { myInfo: HomeMyInfo; } +// noticeId는 공지 목데이터(entities/notices, 1~5번)에 있는 공지로 맞춘다. 배너 문구와 공지 내용은 +// 목데이터라 서로 다르다. 배너가 6개라 마지막 배너는 1번 공지를 연다. const NOTICES: HomeNotice[] = [ { id: "notice-1", + noticeId: "1", subtitle: "여긴 부제목이 들어가요", template: "general", title: "일반공지 제목이 들어가는 자리입니다", }, { id: "notice-2", + noticeId: "2", subtitle: "여긴 부제목이 들어가요", template: "event", title: "2026-02 슬랑제 신청 안내", }, { id: "notice-3", + noticeId: "3", subtitle: "여긴 부제목이 들어가요", template: "partnership", title: "ECHO X 해커스토익 제휴 안내", }, { id: "notice-4", + noticeId: "4", subtitle: "여긴 부제목이 들어가요", template: "locker", title: "2026년도 사물함 신청 안내", }, { id: "notice-5", + noticeId: "5", subtitle: "여긴 부제목이 들어가요", template: "snack", title: "2026-02 기말고사 간식행사", }, { id: "notice-6", + noticeId: "1", subtitle: "여러분의 의견을 들려 주세요", template: "feedback", title: "stream에 바라는 점이 있다면?", }, ]; +// 행사 목데이터(entities/events)의 모집중 행사 const SPORTS_DAY: HomeApplyCard = { daysUntilDeadline: 2, + eventId: "sw-sports-day-open", id: "sports-day", + kind: "event", title: "소프트웨어융합대학 체육대회", }; +const LOCKER_2026: HomeApplyCard = { + daysUntilDeadline: 2, + id: "locker-2026", + kind: "locker", + title: "2026 사물함 신청", +}; + // 상태를 바꿔 홈 3종 화면을 확인한다. // few: 대여·신청 행사 2건 이하 (Figma 3147:146240), many: 3건 이상 (3147:146297), empty: 없음 (3147:146366) type HomeMockState = "few" | "many" | "empty"; @@ -127,15 +151,13 @@ const HOME_DATA: Record = { few: { appliedEvents: [ { + applicationId: "application-1", daysUntilEvent: 7, id: "opening-party", name: "26-2 개강 파티 신청 안내", }, ], - applyCards: [ - SPORTS_DAY, - { daysUntilDeadline: 2, id: "locker-2026", title: "2026 사물함 신청" }, - ], + applyCards: [SPORTS_DAY, LOCKER_2026], myInfo: { feedbackCount: 0, feeStatus: "unpaid", @@ -150,13 +172,24 @@ const HOME_DATA: Record = { }, many: { appliedEvents: [ - { daysUntilEvent: 7, id: "opening-assembly", name: "26-2학기 개강총회" }, { + applicationId: "application-1", + daysUntilEvent: 7, + id: "opening-assembly", + name: "26-2학기 개강총회", + }, + { + applicationId: "application-2", daysUntilEvent: 12, id: "sports-day", name: "소프트웨어융합대학 체육대회", }, - { daysUntilEvent: 20, id: "snack-event", name: "기말고사 간식행사" }, + { + applicationId: "application-1", + daysUntilEvent: 20, + id: "snack-event", + name: "기말고사 간식행사", + }, ], applyCards: [SPORTS_DAY], myInfo: { From 86b90fc986e0618b949d6a53b9d9a66b74e2f341 Mon Sep 17 00:00:00 2001 From: sangrae Date: Sun, 11 Oct 2026 01:03:23 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=ED=99=88=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EA=B3=BC=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20=EA=B0=81=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/HomeScreen.tsx | 52 +++++++++++++++++-- .../components/HomeAppliedEventSection.tsx | 28 ++++++++-- .../home/components/HomeApplyCard.tsx | 21 ++++++-- src/features/home/components/HomeInfoList.tsx | 47 +++++++++++++++-- .../home/components/HomeNoticeBanner.tsx | 28 ++++++++-- .../home/components/HomeRentalSection.tsx | 23 ++++++-- .../lockers/LockersApplyCompleteScreen.tsx | 4 +- 7 files changed, 176 insertions(+), 27 deletions(-) diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index c1c6a47..03446c4 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -5,6 +5,8 @@ import { useNavigate } from "react-router-dom"; import chatbotIcon from "@/assets/icons/chat/bot.svg"; import logo from "@/assets/icons/home/logo.svg"; import ScreenHeader from "@/components/ui/ScreenHeader"; +import { BILILGE_TAB_PATHS } from "@/features/bililge/constants/bililge"; +import { EVENTS_TAB_PATHS } from "@/features/events/constants/events"; import HomeAppliedEventSection from "@/features/home/components/HomeAppliedEventSection"; import HomeApplyCard from "@/features/home/components/HomeApplyCard"; import HomeArchiveBanner from "@/features/home/components/HomeArchiveBanner"; @@ -26,6 +28,9 @@ function HomeScreen() { const [isScrolled, setIsScrolled] = useState(false); + // 상세·신청·내 내역처럼 화면을 쌓는 이동은 슬라이드로, 하단 탭 화면(빌릴게·행사와 그 탭)으로는 바로 바꾼다 + const push = (to: string) => navigate(to, { viewTransition: true }); + return (
{/* 스크롤 영역 자체를 flex로 두면 내용이 길 때 섹션이 줄어들어서(배너 가로 스크롤 영역은 최소 높이가 0) 안쪽에서 쌓는다 */} @@ -72,24 +77,61 @@ function HomeScreen() { />
- + push(`/notices/${noticeId}`)} + /> {/* 배너 카드(relative)가 뒤 섹션보다 나중에 그려져 그림자가 덮지 않게 뒤 섹션도 relative로 둔다 */}
{HOME_MOCK_DATA.applyCards.length > 0 && (
{HOME_MOCK_DATA.applyCards.map((card) => ( - + + push( + card.kind === "event" + ? `/events/${card.eventId}/apply` + : "/lockers/apply", + ) + } + onSelect={() => + push( + card.kind === "event" + ? `/events/${card.eventId}` + : "/lockers/apply", + ) + } + /> ))}
)}
- + navigate(BILILGE_TAB_PATHS.rent)} + onMore={() => navigate(BILILGE_TAB_PATHS.return)} + rentals={HOME_MOCK_DATA.rentals} + />
- + navigate(EVENTS_TAB_PATHS.event)} + onMore={() => navigate(EVENTS_TAB_PATHS.application)} + onSelect={(event) => + push(`${EVENTS_TAB_PATHS.application}/${event.applicationId}`) + } + />
- + push("/my/fee")} + onSelectFeedbacks={() => push("/my/feedbacks")} + onSelectLocker={() => push("/my/locker")} + />
diff --git a/src/features/home/components/HomeAppliedEventSection.tsx b/src/features/home/components/HomeAppliedEventSection.tsx index 1e001da..40200b0 100644 --- a/src/features/home/components/HomeAppliedEventSection.tsx +++ b/src/features/home/components/HomeAppliedEventSection.tsx @@ -9,12 +9,27 @@ const VISIBLE_COUNT = 2; // Figma: Event Applications 1건 (nodeId 3147:146275), 3건 이상 (3147:146335), empty (3562:163940) // 제목의 건수는 Figma대로 3건 이상일 때만 붙는다. -function HomeAppliedEventSection({ events }: { events: HomeAppliedEvent[] }) { +interface HomeAppliedEventSectionProps { + events: HomeAppliedEvent[]; + onSelect: (event: HomeAppliedEvent) => void; + // 더보기 → 행사 신청내역 탭 + onMore: () => void; + // 빈 상태 → 행사 목록 + onBrowse: () => void; +} + +function HomeAppliedEventSection({ + events, + onSelect, + onMore, + onBrowse, +}: HomeAppliedEventSectionProps) { if (events.length === 0) { return ( 0 ? `신청한 행사 · ${events.length}건` : "신청한 행사"} {events.slice(0, VISIBLE_COUNT).map((event) => ( -
+
+ ))} {hiddenCount > 0 && ( <> -
- + +
); } diff --git a/src/features/home/components/HomeInfoList.tsx b/src/features/home/components/HomeInfoList.tsx index da78be3..e8b9f9b 100644 --- a/src/features/home/components/HomeInfoList.tsx +++ b/src/features/home/components/HomeInfoList.tsx @@ -57,9 +57,18 @@ interface InfoRowProps { badge?: InfoBadge; // 학생회비 납부하기는 앱 밖(송금)으로 나가서 ↗ 화살표다 isExternal?: boolean; + // 없으면 누를 수 없는 줄로 그린다(아직 화면이 없는 학생회비 납부하기) + onSelect?: () => void; } -function InfoRow({ icon, title, subtitle, badge, isExternal }: InfoRowProps) { +function InfoRow({ + icon, + title, + subtitle, + badge, + isExternal, + onSelect, +}: InfoRowProps) { let trailing: ReactNode = ( + const content = ( + <>
@@ -102,17 +111,42 @@ function InfoRow({ icon, title, subtitle, badge, isExternal }: InfoRowProps) {
{trailing}
-
+ + ); + + return onSelect ? ( + + ) : ( +
{content}
); } // Figma: Home Info List (nodeId 3147:146284). 순서는 학생회비 → 열린피드백 → 사물함으로 고정한다. -function HomeInfoList({ myInfo }: { myInfo: HomeMyInfo }) { +interface HomeInfoListProps { + myInfo: HomeMyInfo; + onSelectFee: () => void; + onSelectFeedbacks: () => void; + onSelectLocker: () => void; +} + +function HomeInfoList({ + myInfo, + onSelectFee, + onSelectFeedbacks, + onSelectLocker, +}: HomeInfoListProps) { const { feeStatus, feedbackCount, lockerStatus, lockerLabel } = myInfo; return (
{feeStatus === "unpaid" ? ( + // TODO: 학생회비 계좌 송금 화면(#105)이 머지되면 onSelect로 연결한다 )} 0 ? `${feedbackCount}건` : "-"} title="열린피드백 작성내역" /> diff --git a/src/features/home/components/HomeNoticeBanner.tsx b/src/features/home/components/HomeNoticeBanner.tsx index f9bc562..acc9886 100644 --- a/src/features/home/components/HomeNoticeBanner.tsx +++ b/src/features/home/components/HomeNoticeBanner.tsx @@ -56,6 +56,8 @@ interface HomeNoticeCardProps { total: number; // 마지막 카드 뒤에 붙는 첫 카드 복제본 — 스크린리더가 두 번 읽지 않게 숨긴다 isClone?: boolean; + // 없으면 누를 수 없는 카드(공지가 없을 때의 기본 배너) + onSelect?: () => void; } function HomeNoticeCard({ @@ -63,14 +65,21 @@ function HomeNoticeCard({ index, total, isClone, + onSelect, }: HomeNoticeCardProps) { const template = NOTICE_TEMPLATES[notice.template]; + // 공지 상세로 가는 카드는 버튼으로 그린다 + const Root = onSelect ? "button" : "div"; return ( -
{/* 카드가 넓어져도 그림은 Figma 카드 폭(335 = w-83.75) 그대로 가운데에 둔다 */} )}
-
+ ); } @@ -124,11 +133,20 @@ function getStep(scroller: HTMLElement) { // 카드 폭은 화면 폭 - 40(Figma 375 기준 335)이고, 카드 사이(gap-10)도 좌우 여백 합 40이라 이전·다음 카드는 화면 밖에 있다. // 가로 스크롤 영역이 카드 그림자(Shadow/Spread/Small, 60px)를 자르지 않게 위아래로 60px(15단계) 넓힌다. // 넓힌 자리는 아래 섹션 밑으로 깔려서 아래 섹션 터치를 막지 않는다. -function HomeNoticeBanner({ notices }: { notices: HomeNotice[] }) { +interface HomeNoticeBannerProps { + notices: HomeNotice[]; + onSelect: (noticeId: string) => void; +} + +function HomeNoticeBanner({ notices, onSelect }: HomeNoticeBannerProps) { const scrollerRef = useRef(null); const prefersReducedMotion = usePrefersReducedMotion(); const items = notices.length > 0 ? notices : [DEFAULT_NOTICE]; const canSlide = items.length > 1; + const selectHandler = (notice: HomeNotice) => { + const { noticeId } = notice; + return noticeId ? () => onSelect(noticeId) : undefined; + }; useEffect(() => { const scroller = scrollerRef.current; @@ -227,6 +245,7 @@ function HomeNoticeBanner({ notices }: { notices: HomeNotice[] }) { index={index} key={notice.id} notice={notice} + onSelect={selectHandler(notice)} total={items.length} /> ))} @@ -235,6 +254,7 @@ function HomeNoticeBanner({ notices }: { notices: HomeNotice[] }) { index={0} isClone notice={items[0]} + onSelect={selectHandler(items[0])} total={items.length} /> )} diff --git a/src/features/home/components/HomeRentalSection.tsx b/src/features/home/components/HomeRentalSection.tsx index 86abc56..867335b 100644 --- a/src/features/home/components/HomeRentalSection.tsx +++ b/src/features/home/components/HomeRentalSection.tsx @@ -7,13 +7,30 @@ import type { HomeRental } from "@/features/home/constants/homeMock"; const VISIBLE_COUNT = 2; +interface HomeRentalSectionProps { + rentals: HomeRental[]; + // 더보기 → 빌릴게 반납 탭 + onMore: () => void; + // 빈 상태 → 빌릴게 대여 탭 + onBrowse: () => void; +} + // Figma: Rental Summary 2건 (nodeId 3147:146269), 3건 이상 (3147:146326), empty (3562:163925) -function HomeRentalSection({ rentals }: { rentals: HomeRental[] }) { +function HomeRentalSection({ + rentals, + onMore, + onBrowse, +}: HomeRentalSectionProps) { if (rentals.length === 0) { return ( + } @@ -55,7 +72,7 @@ function HomeRentalSection({ rentals }: { rentals: HomeRental[] }) { {hiddenCount > 0 && ( <> - + {returnRequestDialogs} ); } diff --git a/src/features/home/components/HomeRentalSection.tsx b/src/features/home/components/HomeRentalSection.tsx index 867335b..b0d4982 100644 --- a/src/features/home/components/HomeRentalSection.tsx +++ b/src/features/home/components/HomeRentalSection.tsx @@ -9,6 +9,8 @@ const VISIBLE_COUNT = 2; interface HomeRentalSectionProps { rentals: HomeRental[]; + // 반납 신청 — 홈에서 바로 확인 모달을 띄운다 + onReturnRequest: (rental: HomeRental) => void; // 더보기 → 빌릴게 반납 탭 onMore: () => void; // 빈 상태 → 빌릴게 대여 탭 @@ -18,6 +20,7 @@ interface HomeRentalSectionProps { // Figma: Rental Summary 2건 (nodeId 3147:146269), 3건 이상 (3147:146326), empty (3562:163925) function HomeRentalSection({ rentals, + onReturnRequest, onMore, onBrowse, }: HomeRentalSectionProps) { @@ -59,6 +62,7 @@ function HomeRentalSection({ icon={rental.icon} itemName={rental.name} key={rental.id} + onRentRequest={() => onReturnRequest(rental)} subtitle={ rental.hoursUntilDue < 0 ? `반납 기한이 ${-rental.hoursUntilDue}시간 지났어요` From f51baa8a75c14230125ea2f5e1b507965c102d7b Mon Sep 17 00:00:00 2001 From: sangrae Date: Sun, 11 Oct 2026 01:04:34 +0900 Subject: [PATCH 5/9] =?UTF-8?q?docs:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=97=B0=EA=B2=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/wds-component-usage.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index dc34925..92e06c4 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -611,6 +611,24 @@ Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px 일러스트 44px + 문구 한 줄 + 작은 버튼이라 행사 목록 Empty State 때와 같은 이유(`FallbackView`의 일러스트 폭·패딩 고정)로 맞지 않는다. 그림은 빌릴게 반납 빈 상태(`icons/bililge-empty/return-items.svg`)·행사 목록 빈 상태(`icons/events/empty-events.svg`)와 같은 도형이라 크기만 줄여 그대로 쓴다. +### 홈에서 다른 화면으로 가는 연결(#114) + +| 홈에서 누르는 곳 | 이동 | +|---|---| +| 공지 배너 | 공지 상세 `/notices/:noticeId` (공지가 없을 때의 기본 배너는 누를 수 없다) | +| 신청 카드(행사) | 카드(배지·제목 영역) → 행사 상세 `/events/:eventId`, `신청하기` → 행사 신청 `/events/:eventId/apply` | +| 신청 카드(사물함) | 카드·`신청하기` 둘 다 → 사물함 신청 `/lockers/apply`(사물함은 상세 화면이 없다) | +| 대여 현황 `반납 신청` | 이동하지 않고 홈에서 반납 신청 확인 모달 → 완료 토스트 | +| 대여 현황 `N건 더보기` / 빈 상태 `대여하러 가기` | 빌릴게 반납 탭 `/bililge/returns` / 대여 탭 `/bililge` | +| 신청한 행사 행 / `N건 더보기` / 빈 상태 `행사 둘러보기` | 신청내역 상세 `/events/applications/:applicationId` / 신청내역 탭 `/events/applications` / 행사 탭 `/events` | +| 내 정보 학생회비 납부 여부·열린피드백 작성내역·사물함 배정 상태 | `/my/fee`·`/my/feedbacks`·`/my/locker` | + +- **탭은 경로로 연다**: 빌릴게(`/bililge`·`/bililge/returns`)와 행사(`/events`·`/events/applications`) 목록은 같은 화면이 주소로 탭을 고른다. 탭을 바꿀 때는 replace라 히스토리가 쌓이지 않고, 하단 탭은 탭 경로에서도 그 화면이 선택된다. +- **이동 방식**: 상세·신청·내 내역처럼 화면을 쌓는 이동은 `viewTransition`(슬라이드), 하단 탭 화면과 그 탭으로는 바로 바꾼다. +- **반납 신청은 빌릴게 반납 탭과 같은 흐름**: 확인 모달·토스트를 `useBililgeReturnRequest` 훅으로 꺼내 두 화면이 같이 쓴다. 모달 왼쪽 버튼은 Figma(`1133:50014`)대로 `닫기`다 — 처음엔 `수정`으로 잘못 들어가 있었다(사물함 칸 선택 확인 모달의 `수정`은 칸을 다시 고른다는 뜻이라 그대로다). +- **누를 수 있는 줄·카드는 버튼으로 그린다**: 신청 카드는 버튼 안에 버튼을 둘 수 없어서 카드 선택을 배지·제목 영역에만 건다(행사 목록 카드와 같은 방식). 공지 배너의 맨 끝 복제 카드는 키보드 포커스를 받지 않는다. 화면 모양은 버튼으로 바꾸기 전과 픽셀 단위로 같다. +- **아직 연결하지 않은 곳**: 헤더 설정(#96)·알림(#106), 학생회비 미납 줄의 납부하기(#105), 아카이빙 배너(#31)는 각 화면 PR이 머지된 뒤 연결한다. + ## 내 내역 화면(열린피드백 작성내역 `3147:147748`·`3147:147818`·`3147:147756`·`3147:147779` / 학생회비 납부내역 `3147:147860`·`3147:147879`·`3147:147841` / 사물함 배정 상태 `3147:148357`·`3147:148331`) 구현 중 확정된 매핑 | Figma | 코드 | 확인 내용 | From c9973006fcd421bed17333a8d5f9e264181fe7af Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 15 Oct 2026 14:58:46 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=EB=B9=8C=EB=A6=B4=EA=B2=8C=20?= =?UTF-8?q?=ED=83=AD=EC=9D=80=20=EB=9D=BC=EC=9A=B0=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=EB=84=98=EA=B8=B0=EA=B3=A0=20=ED=95=98=EB=8B=A8=20=ED=83=AD=20?= =?UTF-8?q?=EC=86=8C=EC=86=8D=EC=9D=80=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20han?= =?UTF-8?q?dle=EB=A1=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router.tsx | 7 ++++--- src/components/ui/ScreenLayout.tsx | 4 ---- src/features/bililge/BililgeListScreen.tsx | 12 +++++++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/router.tsx b/src/app/router.tsx index 8c97b48..d917e5e 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -50,12 +50,13 @@ const routes = [ path: "/", }, { - element: , + element: , path: "/bililge", }, { - element: , - // 빌릴게의 반납 탭 — 같은 화면이 주소로 탭을 고른다 + element: , + // 빌릴게의 반납 탭 — 같은 화면을 탭만 바꿔 그리고, 하단 탭은 "빌릴게"가 선택된다 + handle: { bottomNavValue: "bililge" } satisfies ScreenRouteHandle, path: "/bililge/returns", }, { diff --git a/src/components/ui/ScreenLayout.tsx b/src/components/ui/ScreenLayout.tsx index c0d98a9..cf1d3b6 100644 --- a/src/components/ui/ScreenLayout.tsx +++ b/src/components/ui/ScreenLayout.tsx @@ -32,10 +32,6 @@ function getBottomNavValueFromPath(pathname: string): BottomNavValue { if (pathname.startsWith("/feedbacks")) { return "board"; } - // 빌릴게의 반납 탭(/bililge/returns)도 "빌릴게" 탭 소속이다(BILILGE_TAB_PATHS와 같은 경로) - if (pathname === "/bililge/returns") { - return "bililge"; - } return "home"; } diff --git a/src/features/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index f7abb9a..b0b09cc 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -5,7 +5,7 @@ import { } from "@wanteddev/wds"; import { IconBell, IconSearch } from "@wanteddev/wds-icon"; import { Suspense, startTransition, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; @@ -22,12 +22,14 @@ import { import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications"; import { getSearchPath } from "@/features/search/constants/search"; +interface BililgeListScreenProps { + /** 대여/반납 탭 — 탭마다 경로가 달라서 라우터가 정해 넘긴다 */ + tab: BililgeTab; +} + // Figma: 빌릴게 (nodeId 1243:73331) -function BililgeListScreen() { +function BililgeListScreen({ tab }: BililgeListScreenProps) { const navigate = useNavigate(); - const { pathname } = useLocation(); - const tab: BililgeTab = - pathname === BILILGE_TAB_PATHS.return ? "return" : "rent"; // 탭을 바꿀 때마다 히스토리가 쌓이지 않게 replace로 바꾼다 const changeTab = (next: BililgeTab) => navigate(BILILGE_TAB_PATHS[next], { replace: true }); From d2ae095e0348abd370d39b1ca10df74b2820f191 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 15 Oct 2026 15:00:00 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20=ED=99=88=C2=B7=EC=82=AC?= =?UTF-8?q?=EB=AC=BC=ED=95=A8=20=EC=8B=A0=EC=B2=AD=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EC=9D=98=20=EB=82=B4=20=EB=82=B4=EC=97=AD=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=EC=9D=84=20=EA=B2=BD=EB=A1=9C=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/HomeScreen.tsx | 9 ++++++--- src/features/lockers/LockersApplyCompleteScreen.tsx | 7 +++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 701b0c7..102559a 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -8,6 +8,8 @@ import ScreenHeader from "@/components/ui/ScreenHeader"; import { BILILGE_TAB_PATHS } from "@/features/bililge/constants/bililge"; import { useBililgeReturnRequest } from "@/features/bililge/hooks/useBililgeReturnRequest"; import { EVENTS_TAB_PATHS } from "@/features/events/constants/events"; +import { FEE_PAYMENTS_PATH } from "@/features/fee/constants/fee"; +import { MY_FEEDBACKS_PATH } from "@/features/feedbacks/constants/feedbacks"; import HomeAppliedEventSection from "@/features/home/components/HomeAppliedEventSection"; import HomeApplyCard from "@/features/home/components/HomeApplyCard"; import HomeArchiveBanner from "@/features/home/components/HomeArchiveBanner"; @@ -18,6 +20,7 @@ import { HOME_HAS_UNREAD_NOTIFICATION, HOME_MOCK_DATA, } from "@/features/home/constants/homeMock"; +import { LOCKER_ASSIGNMENT_PATH } from "@/features/lockers/constants/lockers"; import { NOTIFICATIONS_PATH } from "@/features/notifications/constants/notifications"; import { getSearchPath } from "@/features/search/constants/search"; @@ -139,9 +142,9 @@ function HomeScreen() {
push("/my/fee")} - onSelectFeedbacks={() => push("/my/feedbacks")} - onSelectLocker={() => push("/my/locker")} + onSelectFee={() => push(FEE_PAYMENTS_PATH)} + onSelectFeedbacks={() => push(MY_FEEDBACKS_PATH)} + onSelectLocker={() => push(LOCKER_ASSIGNMENT_PATH)} />
diff --git a/src/features/lockers/LockersApplyCompleteScreen.tsx b/src/features/lockers/LockersApplyCompleteScreen.tsx index fb02e7d..eaa75f1 100644 --- a/src/features/lockers/LockersApplyCompleteScreen.tsx +++ b/src/features/lockers/LockersApplyCompleteScreen.tsx @@ -3,12 +3,10 @@ import { Navigate, useLocation, useNavigate } from "react-router-dom"; import CompleteCheck from "@/components/ui/CompleteCheck"; import ResultScreen from "@/components/ui/ResultScreen"; +import { LOCKER_ASSIGNMENT_PATH } from "@/features/lockers/constants/lockers"; import type { LockersApplyCompleteState } from "@/features/lockers/hooks/useLockersApplySubmit"; import { formatDate } from "@/lib/date"; -// 사물함 신청내역은 사물함 배정 상태 화면이다 -const APPLICATION_HISTORY_PATH = "/my/locker"; - // Figma: 사물함 신청 완료 페이지 (nodeId 1737:218058) function LockersApplyCompleteScreen() { const navigate = useNavigate(); @@ -29,7 +27,8 @@ function LockersApplyCompleteScreen() { primaryAction={{ label: "홈으로 가기", onClick: () => navigate("/") }} secondaryAction={{ label: "신청내역 보기", - onClick: () => navigate(APPLICATION_HISTORY_PATH), + // 사물함 신청내역은 사물함 배정 상태 화면이다 + onClick: () => navigate(LOCKER_ASSIGNMENT_PATH), }} title="신청이 완료됐어요" > From 53d30389fad6e6daa2dd0f23a9841730cd83e9c0 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 15 Oct 2026 15:01:44 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=ED=99=88=20=EC=95=84=EC=B9=B4?= =?UTF-8?q?=EC=9D=B4=EB=B9=99=20=EB=B0=B0=EB=84=88=EB=A5=BC=20=EC=95=84?= =?UTF-8?q?=EC=B9=B4=EC=9D=B4=EB=B9=99=20=EB=AA=A9=EB=A1=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/HomeScreen.tsx | 3 ++- .../home/components/HomeArchiveBanner.tsx | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/features/home/HomeScreen.tsx b/src/features/home/HomeScreen.tsx index 102559a..b87a01a 100644 --- a/src/features/home/HomeScreen.tsx +++ b/src/features/home/HomeScreen.tsx @@ -5,6 +5,7 @@ import { useNavigate } from "react-router-dom"; import chatbotIcon from "@/assets/icons/chat/bot.svg"; import logo from "@/assets/icons/home/logo.svg"; import ScreenHeader from "@/components/ui/ScreenHeader"; +import { ARCHIVES_PATH } from "@/features/archives/constants/archives"; import { BILILGE_TAB_PATHS } from "@/features/bililge/constants/bililge"; import { useBililgeReturnRequest } from "@/features/bililge/hooks/useBililgeReturnRequest"; import { EVENTS_TAB_PATHS } from "@/features/events/constants/events"; @@ -148,7 +149,7 @@ function HomeScreen() { />
- + push(ARCHIVES_PATH)} />
diff --git a/src/features/home/components/HomeArchiveBanner.tsx b/src/features/home/components/HomeArchiveBanner.tsx index 9254834..6fb95c7 100644 --- a/src/features/home/components/HomeArchiveBanner.tsx +++ b/src/features/home/components/HomeArchiveBanner.tsx @@ -2,10 +2,19 @@ import { Typography } from "@wanteddev/wds"; import archiveFolder from "@/assets/icons/home/archive-folder.svg"; +interface HomeArchiveBannerProps { + onSelect: () => void; +} + // Figma: Archive CTA (nodeId 3147:146288) -function HomeArchiveBanner() { +// 배너 전체가 아카이빙 목록으로 가는 버튼이다 +function HomeArchiveBanner({ onSelect }: HomeArchiveBannerProps) { return ( -
+
+ ); } From c178d8e0c6fb776d1e75cc73cec321ad9d7281f7 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 15 Oct 2026 15:02:25 +0900 Subject: [PATCH 9/9] =?UTF-8?q?docs:=20=ED=99=88=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=EC=97=90=20=EC=95=8C=EB=A6=BC=C2=B7=EC=95=84?= =?UTF-8?q?=EC=B9=B4=EC=9D=B4=EB=B9=99=EA=B3=BC=20=ED=83=AD=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20=EB=B0=A9=EC=8B=9D=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/wds-component-usage.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 4644eb7..cd97a6b 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -633,13 +633,15 @@ Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px | 대여 현황 `반납 신청` | 이동하지 않고 홈에서 반납 신청 확인 모달 → 완료 토스트 | | 대여 현황 `N건 더보기` / 빈 상태 `대여하러 가기` | 빌릴게 반납 탭 `/bililge/returns` / 대여 탭 `/bililge` | | 신청한 행사 행 / `N건 더보기` / 빈 상태 `행사 둘러보기` | 신청내역 상세 `/events/applications/:applicationId` / 신청내역 탭 `/events/applications` / 행사 탭 `/events` | -| 내 정보 학생회비 납부 여부·열린피드백 작성내역·사물함 배정 상태 | `/my/fee`·`/my/feedbacks`·`/my/locker` | +| 내 정보 학생회비 납부 여부·열린피드백 작성내역·사물함 배정 상태 | `FEE_PAYMENTS_PATH`·`MY_FEEDBACKS_PATH`·`LOCKER_ASSIGNMENT_PATH`(`/my/fee`·`/my/feedbacks`·`/my/locker`) | +| 헤더 알림 | 알림 목록 `NOTIFICATIONS_PATH`(#108에서 연결) | +| 아카이빙 배너 | 아카이빙 목록 `ARCHIVES_PATH`(`/archives`) — 배너 전체가 버튼이다 | -- **탭은 경로로 연다**: 빌릴게(`/bililge`·`/bililge/returns`)와 행사(`/events`·`/events/applications`) 목록은 같은 화면이 주소로 탭을 고른다. 탭을 바꿀 때는 replace라 히스토리가 쌓이지 않고, 하단 탭은 탭 경로에서도 그 화면이 선택된다. +- **탭은 경로로 연다**: 빌릴게(`/bililge`·`/bililge/returns`)와 행사(`/events`·`/events/applications`) 목록은 같은 화면을 라우터가 `tab` prop으로 골라 넘긴다(화면이 주소를 비교하지 않아 끝에 `/`가 붙어도 맞다). 탭을 바꿀 때는 replace라 히스토리가 쌓이지 않고, 하단 탭은 두 번째 탭 라우트의 `handle.bottomNavValue`로 그 화면이 선택된다. - **이동 방식**: 상세·신청·내 내역처럼 화면을 쌓는 이동은 `viewTransition`(슬라이드), 하단 탭 화면과 그 탭으로는 바로 바꾼다. - **반납 신청은 빌릴게 반납 탭과 같은 흐름**: 확인 모달·토스트를 `useBililgeReturnRequest` 훅으로 꺼내 두 화면이 같이 쓴다. 모달 왼쪽 버튼은 Figma(`1133:50014`)대로 `닫기`다 — 처음엔 `수정`으로 잘못 들어가 있었다(사물함 칸 선택 확인 모달의 `수정`은 칸을 다시 고른다는 뜻이라 그대로다). - **누를 수 있는 줄·카드는 버튼으로 그린다**: 신청 카드는 버튼 안에 버튼을 둘 수 없어서 카드 선택을 배지·제목 영역에만 건다(행사 목록 카드와 같은 방식). 공지 배너의 맨 끝 복제 카드는 키보드 포커스를 받지 않는다. 화면 모양은 버튼으로 바꾸기 전과 픽셀 단위로 같다. -- **아직 연결하지 않은 곳**: 헤더 설정(#96)·알림(#106), 학생회비 미납 줄의 납부하기(#105), 아카이빙 배너(#31)는 각 화면 PR이 머지된 뒤 연결한다. +- **아직 연결하지 않은 곳**: 학생회비 미납 줄의 납부하기(#105)는 송금 화면 PR(#107)이 머지된 뒤 연결한다. 헤더 설정은 설정 화면 PR(#97)이 직접 연결한다. ## 내 내역 화면(열린피드백 작성내역 `3147:147748`·`3147:147818`·`3147:147756`·`3147:147779` / 학생회비 납부내역 `3147:147860`·`3147:147879`·`3147:147841` / 사물함 배정 상태 `3147:148357`·`3147:148331`) 구현 중 확정된 매핑