diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 16f817c..3f996e7 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -439,3 +439,38 @@ WDS에 이름이 비슷한 `ProgressIndicator`가 실제로 존재해서(`제외 ### `Icon/Feedback`·`Icon/Answer`는 게시판-열린피드백 목록과 같은 로컬 아이콘 이 화면의 "피드백 내용"/"학생회 답변" 아이콘은 목록 화면의 `Icon/Question`/`Icon/Answer`와 이름만 다를 뿐 같은 자리(질문/답변 구분 아이콘)라 새로 받지 않고 기존 `src/assets/icons/feedbacks/{question,answer}.svg`를 그대로 재사용했다. + +## 사물함 구역 선택 화면(`1737:218452`, `1737:218489`) 구현 중 확정된 매핑 + +| WDS 컴포넌트 | 확인 경로 | 비고 | +|---|---|---| +| `Content Badge/Content Badge` | 구역 카드의 혼잡도 뱃지 | `445:5656` — [문서](https://montage.wanted.co.kr/docs/components/contents/content-badge/design) | +| `Icon/Normal/Chevron Up` | 평면도 가운데 열 방향 표시 | `687:24638`, 코드 `IconChevronUp` | +| `Icon/Normal/Reset` | Top Navigation 트레일링 새로고침 | `790:38150` — "초기화를 표현합니다 / 반시계 화살표". **`IconRefresh`가 아니라 `IconReset`이다** — wds-icon에 Refresh·Reset·Rotate가 다 있어서 이름만 보고 고르면 틀린다 | + +평면도의 구역 카드·호실·화장실·계단은 전부 Stream 로컬이다. 뱃지만 WDS다. + +### `ContentBadge`의 `size`는 `xsmall`이다 — 빌릴게의 `small`을 베끼면 틀린다 + +`style.js` 실측 결과 세 사이즈가 이렇다. + +| size | padding | 타이포 | +|---|---|---| +| `xsmall` | `3px 6px` | Caption 2 (11px) | +| `small` | `4px 6px` | Caption 1 (12px) | +| `medium` | `5px 8px` | Label 2 (14px) | + +이 화면의 뱃지는 Figma가 `px-6 py-3` + 11px이라 **`xsmall`**이다. `BililgeRentalHistoryEntry.tsx:52`가 쓰는 `small`과 다르다 — "`Content Badge`의 `size`는 화면마다 실측해야 한다" 절의 사례가 하나 더 늘었다. + +혼잡도 3단계는 `color="accent"` + `accentColor`(green/orange/red)로, 배경이 accentColor의 8% 투명도로 깔린다. **마감은 `color="neutral"`**이고 이때 배경은 `semantic.fill.normal`, 글자는 `neutralColor`가 된다(`style.js`의 color 분기 확인) — Figma의 마감 뱃지(`Fill/Normal` 배경 + `Label/Alternative` 글자)와 정확히 맞는다. + +### `download_assets`는 노드 모양에 따라 `export`와 `svgAssets` 중 쓸 게 갈린다 + +- **단일 벡터 레이어**(화장실 `1737:218466`, 계단 `1737:218484`)는 `svgAssets` 항목이 그대로 깨끗한 아이콘이다. `export`를 쓰면 캔버스 배경(``)이 붙는다 +- **여러 레이어 조합**(사물함 유의사항 일러스트)은 `svgAssets`가 레이어별로 쪼개져 나와서 `export`를 써야 하고, 그때 붙는 캔버스 배경을 직접 걷어내야 한다 + +받은 SVG에 `fill="#EFEFEF"`인 전체 크기 ``가 맨 앞에 있으면 캔버스 배경이다. + +### 구역 카드 배경이 화면 배경과 같은 색이라 라우트에 `background: "normal"`이 필요하다 + +카드 배경이 `Background/Normal/Alternative`(`#f7f7f8`)인데 `ScreenLayout` 기본 배경도 같은 값이라, handle을 안 주면 카드가 배경에 완전히 묻힌다(실제로 처음 렌더에서 카드가 안 보였다). Figma 화면 배경이 흰 면이라 `handle: { background: "normal" }`을 준다. diff --git a/src/app/router.tsx b/src/app/router.tsx index db1d4cd..3c00ff7 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -17,6 +17,7 @@ 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 LockersSectionSelectScreen from "@/features/lockers/LockersSectionSelectScreen"; import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen"; import NoticesListScreen from "@/features/notices/NoticesListScreen"; @@ -108,7 +109,7 @@ const routes = [ }, { element: , - // 사물함 유의사항 시트 확인용 임시 라우트 — 실제 진입점이 붙으면 화면과 함께 지운다 + // 시트만 있는 화면이라 뒤에 Bottom Nav가 비쳐도 Figma(홈 위에 뜨는 시트)와 같다 path: "/lockers/apply", }, { @@ -121,6 +122,17 @@ const routes = [ } satisfies ScreenRouteHandle, path: "/chat", }, + { + element: , + // Bottom Nav 대신 하단 고정 버튼(Action Area)이 있는 화면. + // 구역 카드 배경이 Background/Normal/Alternative(#f7f7f8)라 화면까지 같은 색이면 + // 카드가 배경에 묻힌다 — Figma대로 흰 면을 깐다. + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/lockers/apply/sections", + }, // 라우트가 없는 경로 — 레이아웃 안에 둬서 하단 탭이 유지되고, 탭 경로(/event 등)면 그 탭이 활성으로 보인다 { element: , path: "*" }, ], diff --git a/src/assets/icons/lockers/restroom.svg b/src/assets/icons/lockers/restroom.svg new file mode 100644 index 0000000..05419db --- /dev/null +++ b/src/assets/icons/lockers/restroom.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/lockers/stairs.svg b/src/assets/icons/lockers/stairs.svg new file mode 100644 index 0000000..d2eb859 --- /dev/null +++ b/src/assets/icons/lockers/stairs.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/features/lockers/LockersApplyScreen.tsx b/src/features/lockers/LockersApplyScreen.tsx index 5147c13..99eb32e 100644 --- a/src/features/lockers/LockersApplyScreen.tsx +++ b/src/features/lockers/LockersApplyScreen.tsx @@ -1,44 +1,43 @@ -import { Button, Typography } from "@wanteddev/wds"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import LockersNoticeSheet from "@/features/lockers/components/LockersNoticeSheet"; +import { IS_LOCKERS_APPLY_PERIOD } from "@/features/lockers/constants/lockersNotice"; -// 임시 확인용 화면 — 시트를 띄울 진입점이 아직 없어서 둔 자리다. -// 실제 진입점(홈 "사물함 상태" 퀵링크 `1737:218284` 또는 공지 상세 CTA)이 붙으면 -// 이 화면과 /lockers/apply 라우트는 지운다. docs/plans/#68-lockers-notice-sheet.md 참고. +// BottomSheet의 시트 내려가는 시간(duration-[380ms])과 맞춘다 — 다 내려간 뒤에 화면을 떠난다 +const SHEET_CLOSE_MS = 380; + +// Figma: 사물함 신청 전 유의사항 (nodeId 1737:218213) +// 신청 흐름의 첫 화면이다 — 들어오면 유의사항 시트부터 띄우고, 확인하면 구역 선택으로 보낸다. +// 시트를 닫으면(딤 탭) 들어온 곳으로 돌아간다. +// +// 이 화면으로 오는 진입점(홈 "사물함 상태" 퀵링크 `1737:218284` 또는 공지 상세 CTA)은 +// 해당 화면을 만들 때 붙인다. function LockersApplyScreen() { + const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); - // 시트는 닫아도 380ms 동안 미끄러져 내려간다. 기간 여부를 open과 같은 state에서 파생시키면 - // 그 사이 버튼이 비활성 문구로 바뀌는 게 보이므로, 닫을 때는 open만 끄고 이 값은 유지한다. - const [isApplyPeriod, setIsApplyPeriod] = useState(true); - const openSheet = (applyPeriod: boolean) => { - setIsApplyPeriod(applyPeriod); - setIsOpen(true); + // 처음부터 open으로 그리면 시트가 올라오는 전환 없이 바로 떠 있다. 닫힌 상태가 한 번 그려진 뒤 + // 열어야 BottomSheet의 전환이 보여서 두 프레임 뒤에 연다. + useEffect(() => { + let frameId = requestAnimationFrame(() => { + frameId = requestAnimationFrame(() => setIsOpen(true)); + }); + return () => cancelAnimationFrame(frameId); + }, []); + + const handleClose = () => { + setIsOpen(false); + setTimeout(() => navigate(-1), SHEET_CLOSE_MS); }; return ( -
- - 사물함 유의사항 시트 확인용 임시 화면 - - - - - setIsOpen(false)} - // 확인 후 이동할 사물함 구역 선택 화면(1737:218452)은 다음 이슈라 지금은 닫기만 한다 - onConfirm={() => setIsOpen(false)} - open={isOpen} - /> -
+ navigate("/lockers/apply/sections")} + open={isOpen} + /> ); } diff --git a/src/features/lockers/LockersSectionSelectScreen.tsx b/src/features/lockers/LockersSectionSelectScreen.tsx new file mode 100644 index 0000000..4fc4175 --- /dev/null +++ b/src/features/lockers/LockersSectionSelectScreen.tsx @@ -0,0 +1,73 @@ +import { + ActionArea, + ActionAreaButton, + TopNavigationButton, +} from "@wanteddev/wds"; +import { IconChevronLeft, IconReset } from "@wanteddev/wds-icon"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import ScreenHeader from "@/components/ui/ScreenHeader"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; +import LockersFloorMap from "@/features/lockers/components/LockersFloorMap"; + +// Figma: 사물함 구역 선택 전 (nodeId 1737:218452), 선택완료 (1737:218489) +// 새로고침(Figma Icon/Normal/Reset)은 잔여 수량 API가 없어 지금은 페이지를 다시 읽는다. +// 선택 후 이동할 화면은 아직 디자인이 없어서 지금은 아무것도 하지 않는다. +function LockersSectionSelectScreen() { + const [selectedSectionId, setSelectedSectionId] = useState( + null, + ); + const navigate = useNavigate(); + + useScreenHeader( + navigate(-1)} + variant="icon" + > + + + } + title="사물함 구역 선택" + trailing={ + window.location.reload()} + variant="icon" + > + + + } + variant="normal" + />, + ); + + return ( +
+
+ +
+ + + {/* 높이 보정 이유는 wds-component-usage.md "Action Area 메인 버튼 높이" 참고 */} + + {selectedSectionId === null + ? "사물함 구역을 선택해 주세요" + : "선택 완료"} + + +
+ ); +} + +export default LockersSectionSelectScreen; diff --git a/src/features/lockers/components/LockersFloorMap.tsx b/src/features/lockers/components/LockersFloorMap.tsx new file mode 100644 index 0000000..2965360 --- /dev/null +++ b/src/features/lockers/components/LockersFloorMap.tsx @@ -0,0 +1,133 @@ +import { Typography } from "@wanteddev/wds"; +import { IconChevronUp } from "@wanteddev/wds-icon"; +import type { ReactNode } from "react"; + +import restroomIcon from "@/assets/icons/lockers/restroom.svg"; +import stairsIcon from "@/assets/icons/lockers/stairs.svg"; +import LockersSectionCard from "@/features/lockers/components/LockersSectionCard"; +import { + LOCKERS_LEFT_SECTIONS, + LOCKERS_RIGHT_SECTIONS, + LOCKERS_RIGHT_SECTIONS_BELOW_STAIRS, +} from "@/features/lockers/constants/lockersSections"; + +interface LockersFloorMapProps { + selectedSectionId: string | null; + onSelect: (sectionId: string) => void; +} + +// 호실·화장실처럼 고를 수 없는 자리. 점선 테두리에 라벨만 가운데 둔다. +function MapArea({ + children, + className, +}: { + children: ReactNode; + className: string; +}) { + return ( +
+ {children} +
+ ); +} + +function AreaLabel({ icon, label }: { icon?: string; label: string }) { + return ( +
+ {icon !== undefined && ( + + )} + + {label} + +
+ ); +} + +// Figma: 사물함 구역 선택 Floor Map (nodeId 1737:218457) +// +// 호실·화장실·계단·복도 위치는 실제 건물 구조라 데이터로 받지 않고 여기 레이아웃이 갖는다. +// 구역 카드와 잔여 수량만 데이터에서 온다. +// +// Figma는 375×812 한 벌만 그려뒀지만 ScreenLayout은 폰이든 데스크톱(480px 컬럼)이든 높이를 +// 뷰포트로 꽉 채운다. 그래서 치수를 고정하지 않고 **Figma 값을 flex 비율로** 옮겨서 +// 남는 공간을 비례 배분한다 — 열 124:67:124, 왼쪽 열 186:74:186:134, 오른쪽 열 74…34:74. +// 열 사이 간격(10px)과 카드 사이 간격(왼쪽 4px·오른쪽 6px)만 Figma 값 그대로 고정이다. +// 루트에 min-h-0을 주지 않아서 카드 내용 높이보다는 줄어들지 않는다 — 320×568처럼 낮은 화면에서는 +// 대신 부모(LockersSectionSelectScreen)가 세로로 스크롤한다. +function LockersFloorMap({ + selectedSectionId, + onSelect, +}: LockersFloorMapProps) { + const renderCard = (section: (typeof LOCKERS_RIGHT_SECTIONS)[number]) => ( + + ); + + return ( +
+
+ + + + {LOCKERS_LEFT_SECTIONS.map(renderCard)} + + + + + + +
+ + {/* 가운데 열은 복도다 — 카드가 없고 방향 표시만 있다. + 빈 칸도 Figma 비율(위 32 / 라벨 사이 176 / 아래 330)을 flex로 옮긴다. + "정릉시장 방향"은 열보다 넓어서 Figma처럼 한 줄로 넘치게 둔다. */} +
+
+
+ + + 정릉시장 방향 + +
+
+ + 복도 + +
+
+ +
+ {LOCKERS_RIGHT_SECTIONS.map(renderCard)} + + + + {LOCKERS_RIGHT_SECTIONS_BELOW_STAIRS.map(renderCard)} +
+
+ ); +} + +export default LockersFloorMap; diff --git a/src/features/lockers/components/LockersSectionCard.tsx b/src/features/lockers/components/LockersSectionCard.tsx new file mode 100644 index 0000000..2da47db --- /dev/null +++ b/src/features/lockers/components/LockersSectionCard.tsx @@ -0,0 +1,110 @@ +import { ContentBadge, Typography } from "@wanteddev/wds"; + +import type { + LockersSection, + LockersSectionStatus, +} from "@/features/lockers/constants/lockersSections"; + +interface LockersSectionCardProps { + section: LockersSection; + isSelected: boolean; + onSelect: (sectionId: string) => void; +} + +// 마감은 WDS `color="neutral"`(배경 Fill/Normal)이고 나머지 셋은 `color="accent"`로, +// 배경이 accentColor의 8% 투명도로 깔린다 — 둘 다 Figma와 그대로 맞는다. +// 뱃지 크기는 `xsmall`(padding 3/6 + Caption 2, 11px)이다. 빌릴게 반납 뱃지가 쓰는 `small`은 +// padding 4/6 + Caption 1(12px)이라 이 화면과 다르다. +const STATUS_BADGES = { + almostFull: { + color: "accent", + label: "마감 임박", + token: "semantic.accent.foreground.red", + }, + full: { + color: "neutral", + label: "마감", + token: "semantic.label.alternative", + }, + normal: { + color: "accent", + label: "보통", + token: "semantic.accent.foreground.orange", + }, + spacious: { + color: "accent", + label: "여유", + token: "semantic.accent.foreground.green", + }, +} as const satisfies Record; + +// 선택됐을 때만 테두리가 생기면 콘텐츠 박스가 1px씩 줄어서 가운데 정렬된 내용이 밀린다. +// 항상 1px 테두리를 두되 평소엔 투명으로 둬서 박스 크기를 고정한다 +// (배경이 테두리 자리까지 깔려서 테두리 없는 것과 똑같이 보인다). +const CONTAINER_CLASS_NAMES = { + full: "border-transparent bg-cool-neutral-97", + normal: "border-transparent bg-background-alternative", + selected: "border-primary bg-primary-subtle", +} as const; + +// Figma: 사물함 구역 선택 Zone Button (nodeId 1737:218476 외 — Figma 레이어 이름은 Zone이다) — WDS에 대응 컴포넌트가 없는 +// Stream 로컬 카드다. 안쪽 혼잡도 뱃지만 WDS `ContentBadge`를 쓴다. +// +// 마감 구역은 고를 수 없어서 버튼을 잠근다. Figma에 잠금 상태의 선택 모습이 따로 없는 것도 +// 고를 수 없다는 뜻으로 읽었다. +function LockersSectionCard({ + section, + isSelected, + onSelect, +}: LockersSectionCardProps) { + const badge = STATUS_BADGES[section.status]; + const isFull = section.status === "full"; + const containerClassName = isSelected + ? CONTAINER_CLASS_NAMES.selected + : CONTAINER_CLASS_NAMES[isFull ? "full" : "normal"]; + + return ( + + ); +} + +export default LockersSectionCard; diff --git a/src/features/lockers/constants/lockersNotice.ts b/src/features/lockers/constants/lockersNotice.ts index 3dd7046..acefbb1 100644 --- a/src/features/lockers/constants/lockersNotice.ts +++ b/src/features/lockers/constants/lockersNotice.ts @@ -2,6 +2,10 @@ 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"; +// 지금이 신청 기간인지. 실제 기간 API가 붙기 전까지 이 값을 바꿔서 두 상태를 확인한다 +// (행사 신청 플로우의 목업 제출 결과와 같은 방식). +export const IS_LOCKERS_APPLY_PERIOD = true; + export interface LockersNoticeItem { icon: string; label: string; diff --git a/src/features/lockers/constants/lockersSections.ts b/src/features/lockers/constants/lockersSections.ts new file mode 100644 index 0000000..a838253 --- /dev/null +++ b/src/features/lockers/constants/lockersSections.ts @@ -0,0 +1,36 @@ +// 구역 혼잡도. Figma 목업의 잔여 수량과 뱃지가 서로 맞물리지 않아서(6/6·12/12이 "여유"인데 +// 24/60은 "보통", 24/40은 "마감 임박") 잔여 비율에서 유도하지 않고 데이터로 받는다. +// 임계값 규칙이 정해지면 그때 유도로 바꾼다. +export type LockersSectionStatus = + | "spacious" + | "normal" + | "almostFull" + | "full"; + +export interface LockersSection { + id: string; + name: string; + status: LockersSectionStatus; + remaining: number; + total: number; +} + +// Figma: 사물함 구역 선택 전 Floor Map (nodeId 1737:218457)의 구역을 그대로 옮긴 목데이터. +// 평면도 배치(231/232호실·화장실·계단·복도)는 데이터가 아니라 LockersFloorMap의 레이아웃이 갖는다. +export const LOCKERS_LEFT_SECTIONS: LockersSection[] = [ + { id: "A-1", name: "A-1", remaining: 24, status: "spacious", total: 34 }, +]; + +export const LOCKERS_RIGHT_SECTIONS: LockersSection[] = [ + { id: "A-2", name: "A-2", remaining: 6, status: "spacious", total: 6 }, + { id: "B-1", name: "B-1", remaining: 24, status: "normal", total: 60 }, + { id: "B-2", name: "B-2", remaining: 24, status: "almostFull", total: 40 }, + { id: "C", name: "C", remaining: 0, status: "full", total: 100 }, + { id: "D", name: "D", remaining: 24, status: "spacious", total: 30 }, + { id: "A-3", name: "A-3", remaining: 12, status: "spacious", total: 12 }, +]; + +// 계단 아래에 따로 놓이는 구역 — 평면도상 위치가 달라서 배열을 나눠 둔다. +export const LOCKERS_RIGHT_SECTIONS_BELOW_STAIRS: LockersSection[] = [ + { id: "A-4", name: "A-4", remaining: 24, status: "spacious", total: 26 }, +]; diff --git a/src/index.css b/src/index.css index 9421301..6ef9815 100644 --- a/src/index.css +++ b/src/index.css @@ -20,6 +20,9 @@ ); --color-line-solid-neutral: var(--semantic-line-solid-neutral); + /* Figma "Line/Solid/Normal"(#e1e2e4) — 이름이 비슷한 Line/Solid/Neutral(#eaebec)과 값이 다르다. + 사물함 평면도의 호실·화장실·계단 점선 테두리가 이 값이다. */ + --color-line-solid-normal: var(--semantic-line-solid-normal); --color-line-normal-neutral: var(--semantic-line-normal-neutral); --color-line-solid-alternative: var(--semantic-line-solid-alternative); @@ -40,6 +43,10 @@ --color-primary: var(--semantic-primary-normal); --color-primary-subtle: var(--atomic-blue-95); + /* 사물함 구역 선택의 마감 구역 배경(Figma "Cool Neutral/97") — 회색 계열이지만 + Background/Normal/Alternative(#f7f7f8)보다 한 단계 진해서 잠긴 걸 드러낸다 */ + --color-cool-neutral-97: var(--atomic-coolNeutral-97); + --color-status-positive: var(--semantic-status-positive); --color-inverse-background: var(--semantic-inverse-background);