diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index a27dd77..16f817c 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -378,6 +378,39 @@ WDS는 `useToast` 훅 + `Toast` 컴포넌트로 토스트 시스템을 완비하 - **`PaginationDots`는 부모 flex 컨테이너에 `items-center`가 없으면 왼쪽으로 붙는다**: 이 컴포넌트의 실제 루트(`tabindex` wrapper div)는 `className`/`sx` prop이 그 div까지 전달되지 않아 직접 센터링을 줄 수 없다(내부 tablist는 `width: fit-content`). `flex-col` 부모에 `items-center`를 주고, 형제 요소(캐러셀 스크롤 행)에는 `w-full`을 명시해서 폭을 유지해야 정확히 중앙에 온다 — `/figma-check`로 실측하다 발견된 버그. - **`bg-background-alternative`(`#f7f7f8`)는 흰 배경과 3/255밖에 차이가 안 나서 화면에 따라 거의 안 보일 수 있다**: Q&A 카드 배경·8px 섹션 구분선 둘 다 이 값인데, 개별 레이어 단위로 `get_variable_defs`를 다시 떼어봐도 이 값 하나만 바인딩돼 있고 다른 색·테두리는 없었다 — 코드가 Figma 값을 정확히 따르고 있는 게 확인됐다. 그럼에도 시각적 구분이 약하다고 느껴지면, Figma 스펙을 벗어나 더 진한 톤(예: `Line/Normal/Neutral` `#70737c29`)으로 의도적으로 조정할지는 별도 논의 필요 — 이번 PR에서는 Figma 값 그대로 두었다. +## 사물함 신청 전 유의사항 바텀시트(`1737:218213`) 구현 중 확정된 매핑 + +이 시트에서 WDS는 `ActionArea`/`ActionAreaButton`/`Typography`뿐이다. 제목·유의사항 3줄은 전부 Stream 로컬이다. + +- **`Notice Item`(`1417:56435` 계열)은 Stream 로컬**: `get_design_context`의 Component descriptions에 아예 잡히지 않았다. 42px 일러스트 + `[라벨 Label 1/Normal - Regular / 값 Body 2/Normal - Medium]` 두 줄 조합이며, 사물함 전용이 아닐 수 있어 보이지만 재사용처가 아직 없어서 `component-convention.md` §1대로 `features/lockers/`에 뒀다. 두 번째 화면에서 실제로 쓰이면 `components/ui/`로 옮긴다. +- **유의사항 일러스트 3개(시계 `1417:56488`, 사람 `1417:56599`, 달력 `1417:56600`)도 WDS 아이콘이 아니다**: `download_assets`의 `export`로 받으면 셋 다 42×42 단일 SVG로 떨어진다. 특히 달력은 `get_design_context` 응답에서 배경 SVG + 도형 6개(`#e7f1ff`/`#2c88fe`/`#b3d0f7` 하드코딩) 조합으로 나와서 손으로 그려야 하나 싶지만, **노드 단위 `export`를 쓰면 한 장으로 받아진다** — 도형을 다시 그리지 말 것. `src/assets/icons/lockers/`에 커밋했다. +- **`download_assets`의 `export`는 캔버스 배경까지 같이 굽는다**: 받은 SVG 맨 앞에 ``(Figma 캔버스 회색)와 뷰박스 밖으로 뻗는 페이지·섹션 배경 path(``, ``)가 들어 있었다. 그대로 쓰면 동그란 아이콘 뒤에 **회색 네모**가 깔린다. 실제 아이콘은 `` 하위(배경 원 `Ellipse 11` + 글리프)뿐이라 그 그룹만 남기고 걷어냈다(2.7KB → 1.0KB). **다시 export하면 같은 정리가 필요하다.** +- **타이포 실측**: 제목은 `heading2`+`bold`(20px/28), 항목 라벨은 `label1`+`regular`(14px/20), 항목 값은 `body2`+`medium`(15px/22)로 Figma와 정확히 일치한다(`typography/style.js` 확인). `heading2`(20px)와 `headline2`(17px)를 혼동하지 않는다. + +### 시트 상단 간격은 Figma가 시트마다 다르다 — `BottomSheet`의 `contentGap` + +드래그 핸들(24px) 다음 내용이 시작하는 위치가 시트마다 다르다. + +| Figma | 내용 시작 y | 간격 | +|---|---|---| +| 빌릴게 대여 `1422:57178` | 32 | 8px | +| 빌릴게 대여(시간 외) `1422:57209` | 32 | 8px | +| 사물함 유의사항 `1737:218308` | 36 | **12px** | + +`BottomSheet`가 빌릴게 기준 8px을 하드코딩하고 있어서 사물함 시트가 4px 짧게 나왔다(시트 상단→제목 실측 37px, Figma 41px). 공용 컴포넌트를 한쪽 값으로 고정할 수 없어 `contentGap?: 8 | 12` prop으로 빼고 기본값을 8(빌릴게 기존 동작)로 뒀다. 사물함 시트만 `contentGap={12}`를 넘긴다 — 수정 후 실측 41px로 일치. + +**새 시트를 만들 때 이 값을 Figma에서 먼저 확인한다.** 8/12 외 값이 나오면 `BottomSheetContentGap` 유니온에 추가한다. + +### Action Area 메인 버튼 높이 보정은 `paddingBlock`으로 통일한다 + +같은 56px 보정이 코드에 두 형태로 섞여 있었다 — 행사 화면 3곳은 `sx={{ paddingBlock: "16px" }}`, `BililgeRentalSheet.tsx:176`은 `sx={{ height: "56px" }}`다. + +**`paddingBlock` 쪽이 맞다.** Figma 스펙이 `padding: 16px 28px`이라 그대로 옮기는 형태이고, 높이를 고정하면 버튼 문구가 길어져 줄바꿈될 때 잘린다. 이 시트는 신청 기간이 아닐 때 문구가 `신청 기간이 아니에요`로 길어져서 실제로 걸릴 수 있는 자리다. `BililgeRentalSheet`도 정리 대상이다(이번 범위 밖). + +### 신청 기간이 아닐 때의 상태는 Figma에 없다 + +하단 `확인했어요` 버튼을 `disabled`로 두고 문구를 `신청 기간이 아니에요`로 바꾸기로 코드에서 정했다. 유의사항 3줄은 그대로 둔다. 디자인에 없는 상태를 만든 것이라 디자이너 확인이 필요하다 — `docs/plans/#68-lockers-notice-sheet.md` 참고. + ## 열린피드백 상세페이지 · 모아보기(`1410:49988`) 구현 중 확정된 매핑 같은 회차(round)의 답변된 피드백들을 가로로 스와이프해서 넘겨보는 화면. Figma는 정적 예시 한 장만 있어서(신청 폼·행사 상세처럼 스크롤 스냅 캐러셀은 화면에 안 잡힌다), 구조는 공지·행사 상세의 가로 스크롤 스냅 패턴을 그대로 가져오고, "스와이프하는 동안 진행 바가 같이 움직인다"는 요구사항은 스크롤 위치를 0~1 연속값으로 추적해서 반영했다. diff --git a/src/app/router.tsx b/src/app/router.tsx index ecd7f58..db1d4cd 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -16,6 +16,7 @@ import FeedbacksDetailScreen from "@/features/feedbacks/FeedbacksDetailScreen"; import FeedbacksListScreen from "@/features/feedbacks/FeedbacksListScreen"; import FeedbacksNewScreen from "@/features/feedbacks/FeedbacksNewScreen"; import HomeScreen from "@/features/home/HomeScreen"; +import LockersApplyScreen from "@/features/lockers/LockersApplyScreen"; import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen"; import NoticesListScreen from "@/features/notices/NoticesListScreen"; @@ -105,6 +106,11 @@ const routes = [ } satisfies ScreenRouteHandle, path: "/notices/:noticeId", }, + { + element: , + // 사물함 유의사항 시트 확인용 임시 라우트 — 실제 진입점이 붙으면 화면과 함께 지운다 + path: "/lockers/apply", + }, { element: , // Figma 챗봇 진입 화면에는 Bottom Nav가 없다(뒤로가기로 홈에 복귀). 흰 배경 위에 diff --git a/src/assets/icons/lockers/apply-period.svg b/src/assets/icons/lockers/apply-period.svg new file mode 100644 index 0000000..c8a787a --- /dev/null +++ b/src/assets/icons/lockers/apply-period.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/lockers/one-per-person.svg b/src/assets/icons/lockers/one-per-person.svg new file mode 100644 index 0000000..be0297d --- /dev/null +++ b/src/assets/icons/lockers/one-per-person.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/lockers/usage-period.svg b/src/assets/icons/lockers/usage-period.svg new file mode 100644 index 0000000..ceb0355 --- /dev/null +++ b/src/assets/icons/lockers/usage-period.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index bff3859..f976512 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -3,16 +3,31 @@ import { createPortal } from "react-dom"; import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; +// 드래그 핸들(24px)과 내용 사이 간격. Figma가 시트마다 다르게 잡아둬서 화면이 고른다 — +// 빌릴게 대여(`1422:57178`, `1422:57209`)는 8px, 사물함 유의사항(`1737:218308`)은 12px이다. +type BottomSheetContentGap = 8 | 12; + +const CONTENT_GAP_CLASS_NAMES: Record = { + 8: "mb-2", + 12: "mb-3", +}; + interface BottomSheetProps { open: boolean; onClose: () => void; + contentGap?: BottomSheetContentGap; children: ReactNode; } // Figma: Views / Bottom Sheets (nodeId 1422:57176) — WDS에는 대응하는 코드 컴포넌트가 없다 // (component-convention.md 참고: Native / Bottom Sheet Indicator는 Stream/iOS 목업 전용 로컬 요소). // 딤+시트를 ScreenLayout의 포털 슬롯(useScreenSheetPortal)에 그려서 화면 컬럼 전체를 덮는다. -function BottomSheet({ open, onClose, children }: BottomSheetProps) { +function BottomSheet({ + open, + onClose, + contentGap = 8, + children, +}: BottomSheetProps) { const portalEl = useScreenSheetPortal(); if (!portalEl) { @@ -38,7 +53,9 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) { open ? "translate-y-0" : "translate-y-full" }`} > -
+
{children} diff --git a/src/features/lockers/LockersApplyScreen.tsx b/src/features/lockers/LockersApplyScreen.tsx new file mode 100644 index 0000000..5147c13 --- /dev/null +++ b/src/features/lockers/LockersApplyScreen.tsx @@ -0,0 +1,45 @@ +import { Button, Typography } from "@wanteddev/wds"; +import { useState } from "react"; + +import LockersNoticeSheet from "@/features/lockers/components/LockersNoticeSheet"; + +// 임시 확인용 화면 — 시트를 띄울 진입점이 아직 없어서 둔 자리다. +// 실제 진입점(홈 "사물함 상태" 퀵링크 `1737:218284` 또는 공지 상세 CTA)이 붙으면 +// 이 화면과 /lockers/apply 라우트는 지운다. docs/plans/#68-lockers-notice-sheet.md 참고. +function LockersApplyScreen() { + const [isOpen, setIsOpen] = useState(false); + // 시트는 닫아도 380ms 동안 미끄러져 내려간다. 기간 여부를 open과 같은 state에서 파생시키면 + // 그 사이 버튼이 비활성 문구로 바뀌는 게 보이므로, 닫을 때는 open만 끄고 이 값은 유지한다. + const [isApplyPeriod, setIsApplyPeriod] = useState(true); + + const openSheet = (applyPeriod: boolean) => { + setIsApplyPeriod(applyPeriod); + setIsOpen(true); + }; + + return ( +
+ + 사물함 유의사항 시트 확인용 임시 화면 + + + + + setIsOpen(false)} + // 확인 후 이동할 사물함 구역 선택 화면(1737:218452)은 다음 이슈라 지금은 닫기만 한다 + onConfirm={() => setIsOpen(false)} + open={isOpen} + /> +
+ ); +} + +export default LockersApplyScreen; diff --git a/src/features/lockers/components/LockersNoticeSheet.tsx b/src/features/lockers/components/LockersNoticeSheet.tsx new file mode 100644 index 0000000..ba1e3ba --- /dev/null +++ b/src/features/lockers/components/LockersNoticeSheet.tsx @@ -0,0 +1,83 @@ +import { ActionArea, ActionAreaButton, Typography } from "@wanteddev/wds"; + +import BottomSheet from "@/components/ui/BottomSheet"; +import { LOCKERS_NOTICE_ITEMS } from "@/features/lockers/constants/lockersNotice"; + +interface LockersNoticeSheetProps { + open: boolean; + /** 지금이 사물함 신청 기간인지. 아니면 하단 버튼이 잠기고 문구가 바뀐다. */ + isApplyPeriod: boolean; + onClose: () => void; + onConfirm: () => void; +} + +// Figma: 사물함 신청 전 유의사항 Sheet Body (nodeId 1737:218310) +// +// 신청 기간이 아닐 때의 모습은 Figma에 없다 — 유의사항 3줄은 그대로 두고 하단 버튼만 잠가 +// "신청 기간이 아니에요"를 보여주기로 정했다(docs/plans/#68-lockers-notice-sheet.md 참고). +function LockersNoticeSheet({ + open, + isApplyPeriod, + onClose, + onConfirm, +}: LockersNoticeSheetProps) { + return ( + + {/* pb-7: Figma Sheet Body의 Notice Section ↔ Action Area 간격(28px). + BottomSheet는 자식 사이에 간격을 두지 않아서 여기서 낸다. */} +
+ + {/* Figma가 두 줄로 끊어둔 자리라 줄바꿈을 폭에 맡기지 않고 그대로 옮긴다 */} + 신청 전 아래 내용을 +
+ 확인해 주세요 +
+ +
+ {LOCKERS_NOTICE_ITEMS.map((item) => ( +
+ +
+ + {item.label} + + + {item.value} + +
+
+ ))} +
+
+ + + {/* Figma Main Action은 56px인데 ActionAreaButton은 48px이다. 높이를 고정하면 비활성 + 문구가 길어져 줄바꿈될 때 잘려서 padding으로 맞춘다(wds-component-usage.md 참고). */} + + {isApplyPeriod ? "확인했어요" : "신청 기간이 아니에요"} + + +
+ ); +} + +export default LockersNoticeSheet; diff --git a/src/features/lockers/constants/lockersNotice.ts b/src/features/lockers/constants/lockersNotice.ts new file mode 100644 index 0000000..3dd7046 --- /dev/null +++ b/src/features/lockers/constants/lockersNotice.ts @@ -0,0 +1,31 @@ +import applyPeriodIcon from "@/assets/icons/lockers/apply-period.svg"; +import onePerPersonIcon from "@/assets/icons/lockers/one-per-person.svg"; +import usagePeriodIcon from "@/assets/icons/lockers/usage-period.svg"; + +export interface LockersNoticeItem { + icon: string; + label: string; + value: string; +} + +// Figma: 사물함 신청 전 유의사항 Notice Items (nodeId 1737:218314)의 내용을 그대로 옮긴 목데이터. +// 이용 기간·신청 기간 API가 붙으면 그 값으로 바뀔 자리다. +// 위 일러스트 3개는 Figma export가 캔버스·섹션 배경까지 같이 구워 넣어서(42px 회색 네모) +// 아이콘 그룹만 남기고 걷어낸 SVG다 — 다시 받으면 같은 정리가 필요하다. +export const LOCKERS_NOTICE_ITEMS: LockersNoticeItem[] = [ + { + icon: usagePeriodIcon, + label: "사물함 이용 기간", + value: "2026.09.01(화) - 2026.12.15(금)", + }, + { + icon: onePerPersonIcon, + label: "중복 신청 불가", + value: "1인당 1개의 사물함만 신청 가능해요.", + }, + { + icon: applyPeriodIcon, + label: "신청기간", + value: "2026.09.01(화) - 2026.12.15(금)", + }, +];