diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 3c0e6c5..cd97a6b 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -623,6 +623,26 @@ Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px 일러스트 44px + 문구 한 줄 + 작은 버튼이라 행사 목록 Empty State 때와 같은 이유(`FallbackView`의 일러스트 폭·패딩 고정)로 맞지 않는다. 그림은 빌릴게 반납 빈 상태(`icons/bililge-empty/return-items.svg`)·행사 목록 빈 상태(`icons/common/empty-events.svg`)와 같은 도형이라 크기만 줄여 그대로 쓴다. +### 홈에서 다른 화면으로 가는 연결(#114) + +| 홈에서 누르는 곳 | 이동 | +|---|---| +| 공지 배너 | 공지 상세 `/notices/:noticeId` (공지가 없을 때의 기본 배너는 누를 수 없다) | +| 신청 카드(행사) | 카드(배지·제목 영역) → 행사 상세 `/events/:eventId`, `신청하기` → 행사 신청 `/events/:eventId/apply` | +| 신청 카드(사물함) | 카드·`신청하기` 둘 다 → 사물함 신청 `/lockers/apply`(사물함은 상세 화면이 없다) | +| 대여 현황 `반납 신청` | 이동하지 않고 홈에서 반납 신청 확인 모달 → 완료 토스트 | +| 대여 현황 `N건 더보기` / 빈 상태 `대여하러 가기` | 빌릴게 반납 탭 `/bililge/returns` / 대여 탭 `/bililge` | +| 신청한 행사 행 / `N건 더보기` / 빈 상태 `행사 둘러보기` | 신청내역 상세 `/events/applications/:applicationId` / 신청내역 탭 `/events/applications` / 행사 탭 `/events` | +| 내 정보 학생회비 납부 여부·열린피드백 작성내역·사물함 배정 상태 | `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`) 목록은 같은 화면을 라우터가 `tab` prop으로 골라 넘긴다(화면이 주소를 비교하지 않아 끝에 `/`가 붙어도 맞다). 탭을 바꿀 때는 replace라 히스토리가 쌓이지 않고, 하단 탭은 두 번째 탭 라우트의 `handle.bottomNavValue`로 그 화면이 선택된다. +- **이동 방식**: 상세·신청·내 내역처럼 화면을 쌓는 이동은 `viewTransition`(슬라이드), 하단 탭 화면과 그 탭으로는 바로 바꾼다. +- **반납 신청은 빌릴게 반납 탭과 같은 흐름**: 확인 모달·토스트를 `useBililgeReturnRequest` 훅으로 꺼내 두 화면이 같이 쓴다. 모달 왼쪽 버튼은 Figma(`1133:50014`)대로 `닫기`다 — 처음엔 `수정`으로 잘못 들어가 있었다(사물함 칸 선택 확인 모달의 `수정`은 칸을 다시 고른다는 뜻이라 그대로다). +- **누를 수 있는 줄·카드는 버튼으로 그린다**: 신청 카드는 버튼 안에 버튼을 둘 수 없어서 카드 선택을 배지·제목 영역에만 건다(행사 목록 카드와 같은 방식). 공지 배너의 맨 끝 복제 카드는 키보드 포커스를 받지 않는다. 화면 모양은 버튼으로 바꾸기 전과 픽셀 단위로 같다. +- **아직 연결하지 않은 곳**: 학생회비 미납 줄의 납부하기(#105)는 송금 화면 PR(#107)이 머지된 뒤 연결한다. 헤더 설정은 설정 화면 PR(#97)이 직접 연결한다. + ## 내 내역 화면(열린피드백 작성내역 `3147:147748`·`3147:147818`·`3147:147756`·`3147:147779` / 학생회비 납부내역 `3147:147860`·`3147:147879`·`3147:147841` / 사물함 배정 상태 `3147:148357`·`3147:148331`) 구현 중 확정된 매핑 | Figma | 코드 | 확인 내용 | diff --git a/src/app/router.tsx b/src/app/router.tsx index 2955a99..d917e5e 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -50,9 +50,15 @@ const routes = [ path: "/", }, { - element: , + element: , path: "/bililge", }, + { + element: , + // 빌릴게의 반납 탭 — 같은 화면을 탭만 바꿔 그리고, 하단 탭은 "빌릴게"가 선택된다 + handle: { bottomNavValue: "bililge" } satisfies ScreenRouteHandle, + path: "/bililge/returns", + }, { element: , // 카드 없이 구분선으로만 나뉘는 목록이라 화면 전체가 흰 면이다 diff --git a/src/entities/events/eventsMock.ts b/src/entities/events/eventsMock.ts index ff2391d..fd2115a 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/bililge/BililgeListScreen.tsx b/src/features/bililge/BililgeListScreen.tsx index 944a9c5..b0b09cc 100644 --- a/src/features/bililge/BililgeListScreen.tsx +++ b/src/features/bililge/BililgeListScreen.tsx @@ -15,13 +15,24 @@ 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 { 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 [tab, setTab] = useState("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); @@ -59,7 +70,11 @@ function BililgeListScreen() { Figma Tool 프레임(56~88)은 높이 32에 위아래 여백이 없다 — 세로 패딩을 주면 토글과 그 아래 필터 행이 함께 밀린다(행사·게시판 화면과 같은 규칙). */}
- + changeTab(value as BililgeTab)} + size="small" + value={tab} + > 대여 반납 @@ -91,7 +106,7 @@ function BililgeListScreen() { /> ) : ( - setTab("rent")} /> + changeTab("rent")} /> )}
diff --git a/src/features/bililge/components/BililgeReturnConfirmModal.tsx b/src/features/bililge/components/BililgeReturnConfirmModal.tsx index 36c8d1c..fbd4b34 100644 --- a/src/features/bililge/components/BililgeReturnConfirmModal.tsx +++ b/src/features/bililge/components/BililgeReturnConfirmModal.tsx @@ -139,7 +139,7 @@ function BililgeReturnConfirmModal({ sx={{ flex: 1, padding: "12px 20px" }} variant="solid" > - 수정 + 닫기 + {returnRequestDialogs} ); } diff --git a/src/features/home/components/HomeAppliedEventSection.tsx b/src/features/home/components/HomeAppliedEventSection.tsx index 1a65ce7..21fcbd7 100644 --- a/src/features/home/components/HomeAppliedEventSection.tsx +++ b/src/features/home/components/HomeAppliedEventSection.tsx @@ -8,12 +8,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/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 ( -
+
+ ); } diff --git a/src/features/home/components/HomeInfoList.tsx b/src/features/home/components/HomeInfoList.tsx index 81d04ec..061b37b 100644 --- a/src/features/home/components/HomeInfoList.tsx +++ b/src/features/home/components/HomeInfoList.tsx @@ -41,9 +41,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 = ( + <>
@@ -86,17 +95,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..b0d4982 100644 --- a/src/features/home/components/HomeRentalSection.tsx +++ b/src/features/home/components/HomeRentalSection.tsx @@ -7,13 +7,33 @@ import type { HomeRental } from "@/features/home/constants/homeMock"; const VISIBLE_COUNT = 2; +interface HomeRentalSectionProps { + rentals: HomeRental[]; + // 반납 신청 — 홈에서 바로 확인 모달을 띄운다 + onReturnRequest: (rental: HomeRental) => void; + // 더보기 → 빌릴게 반납 탭 + 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, + onReturnRequest, + onMore, + onBrowse, +}: HomeRentalSectionProps) { if (rentals.length === 0) { return ( + } @@ -42,6 +62,7 @@ function HomeRentalSection({ rentals }: { rentals: HomeRental[] }) { icon={rental.icon} itemName={rental.name} key={rental.id} + onRentRequest={() => onReturnRequest(rental)} subtitle={ rental.hoursUntilDue < 0 ? `반납 기한이 ${-rental.hoursUntilDue}시간 지났어요` @@ -55,7 +76,7 @@ function HomeRentalSection({ rentals }: { rentals: HomeRental[] }) { {hiddenCount > 0 && ( <> -