From aacba6f74a3219753c09f180e7547177d2ee85d3 Mon Sep 17 00:00:00 2001 From: gain <163835628+leegain1@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:23:26 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EC=84=A0=ED=83=9D=EC=97=90=20=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EC=83=89=20=ED=86=A0=ED=81=B0=EA=B3=BC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icons/lockers/locker-unavailable.svg | 6 ++++++ src/assets/icons/lockers/shelf-divider.svg | 10 ++++++++++ .../images/lockers/section-a-1-photo.jpg | Bin 0 -> 158534 bytes src/index.css | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 src/assets/icons/lockers/locker-unavailable.svg create mode 100644 src/assets/icons/lockers/shelf-divider.svg create mode 100644 src/assets/images/lockers/section-a-1-photo.jpg diff --git a/src/assets/icons/lockers/locker-unavailable.svg b/src/assets/icons/lockers/locker-unavailable.svg new file mode 100644 index 0000000..a87d4c0 --- /dev/null +++ b/src/assets/icons/lockers/locker-unavailable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/lockers/shelf-divider.svg b/src/assets/icons/lockers/shelf-divider.svg new file mode 100644 index 0000000..8794b45 --- /dev/null +++ b/src/assets/icons/lockers/shelf-divider.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/lockers/section-a-1-photo.jpg b/src/assets/images/lockers/section-a-1-photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c00a01c481f81a964cbb604850e539c21a943000 GIT binary patch literal 158534 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

5XcX-Xv7zeH$dX8-kigRYWZHumP9S+a! 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⁣{$5RqcQAhulaeJZcU87*~v55ju& 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 SlyW|_n9o{?EliVpAOG3=>{R3c literal 0 HcmV?d00001 diff --git a/src/index.css b/src/index.css index 6ef9815..4b122fa 100644 --- a/src/index.css +++ b/src/index.css @@ -47,6 +47,14 @@ Background/Normal/Alternative(#f7f7f8)보다 한 단계 진해서 잠긴 걸 드러낸다 */ --color-cool-neutral-97: var(--atomic-coolNeutral-97); + /* 사물함 칸 선택 — 선택 가능한 칸 배경(Figma "Orange/95")과 칸 묶음 테두리(Figma "Line/Solid/Alternative") */ + --color-orange-95: var(--atomic-orange-95); + --color-line-solid-alternative: var(--semantic-line-solid-alternative); + /* 구역 실제 사진 위 칸 번호 핀 — 위쪽 칸부터 Figma "Orange/50" → "Orange/70" → "Orange/90" */ + --color-orange-50: var(--atomic-orange-50); + --color-orange-70: var(--atomic-orange-70); + --color-orange-90: var(--atomic-orange-90); + --color-status-positive: var(--semantic-status-positive); --color-inverse-background: var(--semantic-inverse-background); From 2997c9103dab391383322f8148c408961ca5a476 Mon Sep 17 00:00:00 2001 From: gain <163835628+leegain1@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:23:26 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20A-1=C2=B7A-2=EA=B5=AC=EC=97=AD=20?= =?UTF-8?q?=EC=82=AC=EB=AC=BC=ED=95=A8=20=EC=B9=B8=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/components/LockersA1LockerMap.tsx | 104 ++++++++++++++++++ .../lockers/components/LockersA2LockerMap.tsx | 62 +++++++++++ .../lockers/components/LockersLockerCell.tsx | 59 ++++++++++ .../lockers/components/LockersLockerGrid.tsx | 33 ++++++ .../components/LockersLockerLegend.tsx | 41 +++++++ .../lockers/components/LockersShelfLabel.tsx | 46 ++++++++ .../lockers/constants/lockersLockers.ts | 91 +++++++++++++++ 7 files changed, 436 insertions(+) create mode 100644 src/features/lockers/components/LockersA1LockerMap.tsx create mode 100644 src/features/lockers/components/LockersA2LockerMap.tsx create mode 100644 src/features/lockers/components/LockersLockerCell.tsx create mode 100644 src/features/lockers/components/LockersLockerGrid.tsx create mode 100644 src/features/lockers/components/LockersLockerLegend.tsx create mode 100644 src/features/lockers/components/LockersShelfLabel.tsx create mode 100644 src/features/lockers/constants/lockersLockers.ts diff --git a/src/features/lockers/components/LockersA1LockerMap.tsx b/src/features/lockers/components/LockersA1LockerMap.tsx new file mode 100644 index 0000000..a685a3f --- /dev/null +++ b/src/features/lockers/components/LockersA1LockerMap.tsx @@ -0,0 +1,104 @@ +import { Typography } from "@wanteddev/wds"; +import type { ReactNode } from "react"; + +import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; +import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; +import { + LOCKERS_A1_GROUPS, + type LockersLockerGroup, +} from "@/features/lockers/constants/lockersLockers"; + +interface LockersA1LockerMapProps { + selectedLockerNumber: number | null; + onSelect: (lockerNumber: number) => void; +} + +// 창문·벽면처럼 고를 수 없는 자리 표시. Figma "Direction Label"·"Aisle" +function MapLabel({ + children, + className, +}: { + children: ReactNode; + className: string; +}) { + return ( +

+ ); +} + +// Figma: Zone Area (nodeId 2159:110831 외) — 칸 묶음을 감싸는 테두리 +function ZoneArea({ + group, + selectedLockerNumber, + onSelect, +}: { group: LockersLockerGroup } & LockersA1LockerMapProps) { + return ( +
+ +
+ ); +} + +// Figma: A-1구역 Locker Grid Container (nodeId 2159:110821) — 창문을 사이에 두고 벽면 세 곳에 +// 칸 묶음이 붙어 있다. 실제 공간 배치라 데이터가 아니라 이 레이아웃이 갖는다(구역 평면도와 같은 방침). +// 폭이 화면보다 넓어서(약 620px) 부모가 좌우 스크롤로 보여준다. +// +// Figma의 Shelf Label은 높이가 1px로 깨져 있어서, 미니맵(2159:110761)처럼 칸 묶음 줄(122px) +// 높이에 맞춘다. +function LockersA1LockerMap({ + selectedLockerNumber, + onSelect, +}: LockersA1LockerMapProps) { + const zoneProps = { onSelect, selectedLockerNumber }; + + return ( +
+
+ +
+ 창문 +
+ {"왼\n쪽\n\n벽\n면"} + + + + + {"오\n른\n쪽\n\n벽\n면"} + +
+
+
+ {/* Figma에서 복도 라벨은 560px 폭 가운데 정렬이라 칸 묶음 줄 아래 가운데에 온다 */} +
+ + 복도 + +
+
+ ); +} + +export default LockersA1LockerMap; diff --git a/src/features/lockers/components/LockersA2LockerMap.tsx b/src/features/lockers/components/LockersA2LockerMap.tsx new file mode 100644 index 0000000..b325a49 --- /dev/null +++ b/src/features/lockers/components/LockersA2LockerMap.tsx @@ -0,0 +1,62 @@ +import { Typography } from "@wanteddev/wds"; + +import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; +import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; +import { LOCKERS_A2_GROUP } from "@/features/lockers/constants/lockersLockers"; + +interface LockersA2LockerMapProps { + selectedLockerNumber: number | null; + onSelect: (lockerNumber: number) => void; +} + +// 옆 구역·호실처럼 고를 수 없는 자리. 구역 평면도(LockersFloorMap)의 MapArea와 같은 점선 상자다. +function MapArea({ label, className }: { label: string; className: string }) { + return ( +
+ + {label} + +
+ ); +} + +// Figma: A-2구역 Locker Grid Section (nodeId 2159:109622) — 칸 묶음 옆이 B-1구역, 복도 건너편이 +// 231호실이다. 실제 공간 배치라 레이아웃이 갖는다. 화면 폭(335px) 안에 들어가서 스크롤이 생기지 않는다. +function LockersA2LockerMap({ + selectedLockerNumber, + onSelect, +}: LockersA2LockerMapProps) { + return ( +
+ +
+
+ + +
+ + 복도 + + +
+
+ ); +} + +export default LockersA2LockerMap; diff --git a/src/features/lockers/components/LockersLockerCell.tsx b/src/features/lockers/components/LockersLockerCell.tsx new file mode 100644 index 0000000..8847870 --- /dev/null +++ b/src/features/lockers/components/LockersLockerCell.tsx @@ -0,0 +1,59 @@ +import { Typography } from "@wanteddev/wds"; + +import lockerUnavailableIcon from "@/assets/icons/lockers/locker-unavailable.svg"; +import type { LockersLocker } from "@/features/lockers/constants/lockersLockers"; + +interface LockersLockerCellProps { + locker: LockersLocker; + isSelected: boolean; + onSelect: (lockerNumber: number) => void; +} + +// Figma: Locker Cell (nodeId 2159:110833 외) — Stream 로컬. 28px 정사각형, Orange/95 배경에 +// 번호만 Caption 2/Regular로 가운데 둔다. +// +// 선택 불가 칸은 Figma 범례(2159:110819)와 같은 그림이다 — 회색 바탕에 대각선. 칸 번호는 Figma +// 미니맵의 선택 불가 칸(2159:109565)처럼 지운다. 범례 SVG는 `preserveAspectRatio="none"`이라 +// 28px로 늘려도 모서리가 4px로 맞는다. +// +// 선택된 칸은 Figma에 없다. 구역 카드(LockersSectionCard)의 선택 표현(Blue/95 배경 + +// Primary/Normal 1px 테두리)을 그대로 따른다 — 테두리는 평소에도 투명으로 둬서 칸 크기를 고정한다. +function LockersLockerCell({ + locker, + isSelected, + onSelect, +}: LockersLockerCellProps) { + if (!locker.isAvailable) { + return ( + {`${locker.number}번 + ); + } + + return ( + + ); +} + +export default LockersLockerCell; diff --git a/src/features/lockers/components/LockersLockerGrid.tsx b/src/features/lockers/components/LockersLockerGrid.tsx new file mode 100644 index 0000000..9101eee --- /dev/null +++ b/src/features/lockers/components/LockersLockerGrid.tsx @@ -0,0 +1,33 @@ +import LockersLockerCell from "@/features/lockers/components/LockersLockerCell"; +import type { LockersLockerGroup } from "@/features/lockers/constants/lockersLockers"; + +interface LockersLockerGridProps { + group: LockersLockerGroup; + selectedLockerNumber: number | null; + onSelect: (lockerNumber: number) => void; +} + +// Figma: Locker Grid (nodeId 2159:110832 외) — 칸 사이 6px. 열 수는 묶음의 첫 행 길이를 따른다. +function LockersLockerGrid({ + group, + selectedLockerNumber, + onSelect, +}: LockersLockerGridProps) { + return ( +
+ {group.flat().map((locker) => ( + + ))} +
+ ); +} + +export default LockersLockerGrid; diff --git a/src/features/lockers/components/LockersLockerLegend.tsx b/src/features/lockers/components/LockersLockerLegend.tsx new file mode 100644 index 0000000..9824c9b --- /dev/null +++ b/src/features/lockers/components/LockersLockerLegend.tsx @@ -0,0 +1,41 @@ +import { Typography } from "@wanteddev/wds"; +import type { ReactNode } from "react"; + +import lockerUnavailableIcon from "@/assets/icons/lockers/locker-unavailable.svg"; + +function LegendItem({ icon, label }: { icon: ReactNode; label: string }) { + return ( +
+ {icon} + + {label} + +
+ ); +} + +// Figma: Legend (nodeId 2159:110814) — Stream 로컬. 칸 색이 무엇을 뜻하는지 알려준다. +function LockersLockerLegend() { + return ( +
+ } + label="선택 가능" + /> + + } + label="선택 불가" + /> +
+ ); +} + +export default LockersLockerLegend; diff --git a/src/features/lockers/components/LockersShelfLabel.tsx b/src/features/lockers/components/LockersShelfLabel.tsx new file mode 100644 index 0000000..4e012a5 --- /dev/null +++ b/src/features/lockers/components/LockersShelfLabel.tsx @@ -0,0 +1,46 @@ +import { Typography } from "@wanteddev/wds"; + +import shelfDividerIcon from "@/assets/icons/lockers/shelf-divider.svg"; + +interface LockersShelfLabelProps { + /** 칸 묶음 높이에 맞춘다 — A-1은 테두리 있는 묶음(122px), A-2는 칸만(96px) */ + className: string; +} + +function ShelfText({ children }: { children: string }) { + return ( + + {children} + + ); +} + +// Figma: Shelf Label (nodeId 1628:174373) — Stream 로컬. 칸 묶음 왼쪽에서 위아래 방향을 알려준다. +// 가운데 점선은 Figma가 가로 Divider를 90° 돌려 그린 것이라 같은 방식(컨테이너 단위로 길이를 +// 맞춘 뒤 회전)으로 옮긴다. SVG 원본 크기(120×1.2)는 건드리지 않는다. +function LockersShelfLabel({ className }: LockersShelfLabelProps) { + return ( +
+ 위쪽 칸 +
+ +
+ 아래쪽 칸 +
+ ); +} + +export default LockersShelfLabel; diff --git a/src/features/lockers/constants/lockersLockers.ts b/src/features/lockers/constants/lockersLockers.ts new file mode 100644 index 0000000..8615bbd --- /dev/null +++ b/src/features/lockers/constants/lockersLockers.ts @@ -0,0 +1,91 @@ +export interface LockersLocker { + number: number; + isAvailable: boolean; +} + +// 칸 묶음 하나(Figma "Zone Area" 안의 "Locker Grid"). 행 단위로 적는다 — 번호가 행마다 +// 이어지지 않는 묶음이 있어서(A-1 가운데 묶음은 25~30 / 22~24·31~33 / 19~21·34~36) 규칙으로 만들지 않는다. +export type LockersLockerGroup = LockersLocker[][]; + +function toRows(rows: number[][]): LockersLockerGroup { + return rows.map((row) => + row.map((number) => ({ isAvailable: true, number })), + ); +} + +// Figma: A-1구역 Locker Grid Area (nodeId 2159:110825) — 왼쪽 벽면 · 창문 · 오른쪽 벽면 순서. +// Figma 목업은 전부 선택 가능이라 목데이터도 그렇게 둔다. API가 붙으면 isAvailable만 채우면 된다. +export const LOCKERS_A1_GROUPS = { + left: toRows([ + [10, 11, 12], + [13, 14, 15], + [16, 17, 18], + ]), + middle: toRows([ + [25, 26, 27, 28, 29, 30], + [22, 23, 24, 31, 32, 33], + [19, 20, 21, 34, 35, 36], + ]), + right: toRows([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]), +} as const satisfies Record; + +// Figma: A-2구역 Locker Grid (nodeId 2159:109626) +export const LOCKERS_A2_GROUP: LockersLockerGroup = toRows([ + [82, 83, 84], + [85, 86, 87], + [88, 89, 90], +]); + +// 사진 속 칸 번호 핀 색. 위쪽 칸일수록 진하다(Figma Orange/50 → 70 → 90). +export type LockersPhotoPinTone = "upper" | "middle" | "lower"; + +export interface LockersPhotoPin { + number: number; + tone: LockersPhotoPinTone; + /** 사진 프레임(300×224) 기준 좌표(px) */ + left: number; + top: number; +} + +// Figma 가운데 묶음 핀은 "Photo Annotation"(left 94.14 · top 89.83) 안에서 flex로 놓여 있다. +// 핀 12.414px + 가로 간격 7px · 세로 간격 12px을 풀어서 좌표로 적는다. +const MIDDLE_PIN_LEFTS = [94.14, 113.55, 132.97, 152.38, 171.8, 191.21]; +const MIDDLE_PIN_ROWS: [LockersPhotoPinTone, number, number[]][] = [ + ["upper", 89.83, [25, 26, 27, 28, 29, 30]], + ["middle", 114.24, [22, 23, 24, 31, 32, 33]], + ["lower", 138.66, [19, 20, 21, 34, 35, 36]], +]; + +// Figma: A-1구역 실제사진 Modal (nodeId 2159:110299) 안의 핀 좌표 +export const LOCKERS_A1_PHOTO_PINS: LockersPhotoPin[] = [ + { left: 9.5, number: 10, tone: "upper", top: 94.14 }, + { left: 39.5, number: 11, tone: "upper", top: 88.45 }, + { left: 61.5, number: 12, tone: "upper", top: 84.31 }, + { left: 9.5, number: 13, tone: "middle", top: 141.72 }, + { left: 39.5, number: 14, tone: "middle", top: 129.31 }, + { left: 61.5, number: 15, tone: "middle", top: 118.97 }, + { left: 9.5, number: 16, tone: "lower", top: 192.93 }, + { left: 39.5, number: 17, tone: "lower", top: 169.65 }, + { left: 61.5, number: 18, tone: "lower", top: 152.07 }, + ...MIDDLE_PIN_ROWS.flatMap(([tone, top, numbers]) => + numbers.map((number, index) => ({ + left: MIDDLE_PIN_LEFTS[index], + number, + tone, + top, + })), + ), + { left: 226.5, number: 1, tone: "upper", top: 83.28 }, + { left: 251.5, number: 2, tone: "upper", top: 85.86 }, + { left: 283.5, number: 3, tone: "upper", top: 89.48 }, + { left: 226.5, number: 4, tone: "middle", top: 118.97 }, + { left: 251.5, number: 5, tone: "middle", top: 131.9 }, + { left: 283.5, number: 6, tone: "middle", top: 149.48 }, + { left: 226.5, number: 7, tone: "lower", top: 149.48 }, + { left: 251.5, number: 8, tone: "lower", top: 171.21 }, + { left: 283.5, number: 9, tone: "lower", top: 193.97 }, +]; From e465abdd4d6e7736b4f5ab9bf3e01263da6ade27 Mon Sep 17 00:00:00 2001 From: gain <163835628+leegain1@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:23:26 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=EA=B5=AC=EC=97=AD=EB=B3=84=20?= =?UTF-8?q?=EC=82=AC=EB=AC=BC=ED=95=A8=20=EC=84=A0=ED=83=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B3=BC=20=EC=8B=A4=EC=A0=9C=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router.tsx | 10 + .../lockers/LockersLockerSelectScreen.tsx | 196 ++++++++++++++++++ .../lockers/LockersSectionSelectScreen.tsx | 4 +- .../components/LockersLockerMinimap.tsx | 50 +++++ .../components/LockersSectionPhotoModal.tsx | 127 ++++++++++++ .../components/LockersSelectedLockerBar.tsx | 63 ++++++ 6 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/features/lockers/LockersLockerSelectScreen.tsx create mode 100644 src/features/lockers/components/LockersLockerMinimap.tsx create mode 100644 src/features/lockers/components/LockersSectionPhotoModal.tsx create mode 100644 src/features/lockers/components/LockersSelectedLockerBar.tsx diff --git a/src/app/router.tsx b/src/app/router.tsx index 3c00ff7..b5d2e76 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 LockersLockerSelectScreen from "@/features/lockers/LockersLockerSelectScreen"; import LockersSectionSelectScreen from "@/features/lockers/LockersSectionSelectScreen"; import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen"; import NoticesListScreen from "@/features/notices/NoticesListScreen"; @@ -133,6 +134,15 @@ const routes = [ } satisfies ScreenRouteHandle, path: "/lockers/apply/sections", }, + { + element: , + // 구역 선택 화면과 같다 — 하단 고정 선택 영역이 있고, 칸 배경이 흰 면 위에 놓인다 + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/lockers/apply/sections/:sectionId", + }, // 라우트가 없는 경로 — 레이아웃 안에 둬서 하단 탭이 유지되고, 탭 경로(/event 등)면 그 탭이 활성으로 보인다 { element: , path: "*" }, ], diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx new file mode 100644 index 0000000..7e3ef04 --- /dev/null +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -0,0 +1,196 @@ +import { TopNavigationButton } from "@wanteddev/wds"; +import { IconChevronLeft, IconImage, IconReset } from "@wanteddev/wds-icon"; +import type { ComponentType } from "react"; +import { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; + +import ComingSoonScreen from "@/app/ComingSoonScreen"; +import sectionA1Photo from "@/assets/images/lockers/section-a-1-photo.jpg"; +import ScreenHeader from "@/components/ui/ScreenHeader"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; +import LockersA1LockerMap from "@/features/lockers/components/LockersA1LockerMap"; +import LockersA2LockerMap from "@/features/lockers/components/LockersA2LockerMap"; +import LockersLockerLegend from "@/features/lockers/components/LockersLockerLegend"; +import LockersLockerMinimap, { + type LockersLockerMinimapViewport, +} from "@/features/lockers/components/LockersLockerMinimap"; +import LockersSectionPhotoModal from "@/features/lockers/components/LockersSectionPhotoModal"; +import LockersSelectedLockerBar from "@/features/lockers/components/LockersSelectedLockerBar"; +import { + LOCKERS_A1_PHOTO_PINS, + type LockersPhotoPin, +} from "@/features/lockers/constants/lockersLockers"; + +interface LockerMapProps { + selectedLockerNumber: number | null; + onSelect: (lockerNumber: number) => void; +} + +interface SectionLayout { + LockerMap: ComponentType; + photo?: { src: string; pins: LockersPhotoPin[] }; +} + +// 구역마다 칸 배치가 실제 공간 구조라 모양이 전부 달라서, 구역별 배치 컴포넌트를 둔다. +// 아직 Figma에 A-1·A-2만 있다. +const SECTION_LAYOUTS: Record = { + "A-1": { + LockerMap: LockersA1LockerMap, + photo: { pins: LOCKERS_A1_PHOTO_PINS, src: sectionA1Photo }, + }, + "A-2": { LockerMap: LockersA2LockerMap }, +}; + +// Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) +function LockersLockerSelectScreen() { + const { sectionId = "" } = useParams(); + const layout = SECTION_LAYOUTS[sectionId]; + + if (layout === undefined) { + return ; + } + + // 구역이 바뀌면 고른 칸·스크롤을 처음부터 다시 잡는다 + return ( + + ); +} + +function SectionLockerSelect({ + sectionId, + layout, +}: { + sectionId: string; + layout: SectionLayout; +}) { + const navigate = useNavigate(); + const [selectedLockerNumber, setSelectedLockerNumber] = useState< + number | null + >(null); + const [isPhotoOpen, setIsPhotoOpen] = useState(false); + const [viewport, setViewport] = useState( + null, + ); + const scrollRef = useRef(null); + const { LockerMap, photo } = layout; + + // 칸 배치가 화면보다 넓을 때만 미니맵을 띄운다 — Figma도 화면에 다 들어오는 A-2는 + // 미니맵을 투명하게 숨겨 자리만 남겨뒀다. + const updateViewport = useCallback(() => { + const el = scrollRef.current; + if (!el) { + return; + } + const { clientWidth, scrollLeft, scrollWidth } = el; + setViewport( + scrollWidth > clientWidth + ? { size: clientWidth / scrollWidth, start: scrollLeft / scrollWidth } + : null, + ); + }, []); + + useLayoutEffect(() => { + const el = scrollRef.current; + if (!el) { + return; + } + updateViewport(); + const observer = new ResizeObserver(updateViewport); + observer.observe(el); + + return () => observer.disconnect(); + }, [updateViewport]); + + useScreenHeader( + navigate(-1)} + variant="icon" + > + + + } + title={`${sectionId}구역 사물함 선택`} + trailing={ + <> + {photo && ( + setIsPhotoOpen(true)} + variant="icon" + > + + + )} + window.location.reload()} + variant="icon" + > + + + + } + variant="normal" + />, + ); + + const map = ( + + ); + + return ( +
+
+ {/* Figma Minimap Section — 미니맵이 없어도 128px 자리를 지켜서 칸 배치 위치가 구역마다 같다 */} +
+ {viewport ? ( + + {map} + + ) : ( +
+ )} + +
+ {/* 화면 끝까지 스크롤되도록 좌우 여백을 스크롤 영역 안쪽에 둔다 */} +
+
{map}
+
+
+ + {}} + sectionName={sectionId} + selectedLockerNumber={selectedLockerNumber} + /> + + {photo && ( + setIsPhotoOpen(false)} + open={isPhotoOpen} + photo={photo.src} + pins={photo.pins} + sectionName={sectionId} + /> + )} +
+ ); +} + +export default LockersLockerSelectScreen; diff --git a/src/features/lockers/LockersSectionSelectScreen.tsx b/src/features/lockers/LockersSectionSelectScreen.tsx index 4fc4175..96c1262 100644 --- a/src/features/lockers/LockersSectionSelectScreen.tsx +++ b/src/features/lockers/LockersSectionSelectScreen.tsx @@ -13,7 +13,6 @@ 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, @@ -59,6 +58,9 @@ function LockersSectionSelectScreen() { {/* 높이 보정 이유는 wds-component-usage.md "Action Area 메인 버튼 높이" 참고 */} + navigate(`/lockers/apply/sections/${selectedSectionId}`) + } sx={{ paddingBlock: "16px" }} > {selectedSectionId === null diff --git a/src/features/lockers/components/LockersLockerMinimap.tsx b/src/features/lockers/components/LockersLockerMinimap.tsx new file mode 100644 index 0000000..ff04943 --- /dev/null +++ b/src/features/lockers/components/LockersLockerMinimap.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from "react"; + +export interface LockersLockerMinimapViewport { + /** 스크롤 영역 전체 폭 대비 보이는 영역의 시작 위치(0~1) */ + start: number; + /** 스크롤 영역 전체 폭 대비 보이는 영역의 폭(0~1) */ + size: number; +} + +interface LockersLockerMinimapProps { + /** 본문과 같은 칸 배치를 그대로 넘긴다 — 축소해서 그린다 */ + children: ReactNode; + viewport: LockersLockerMinimapViewport; +} + +// Figma 미니맵(2159:110757)은 본문 칸 배치(약 620px 폭)를 약 192px로 줄여 그렸다. +const MINIMAP_ZOOM = 0.31; + +// Figma: Minimap (nodeId 2159:110757) — Stream 로컬. 좌우로 스크롤되는 칸 배치의 전체 모습과 +// 지금 보이는 자리(검은 테두리, Figma "Highlight Overlay")를 보여준다. +// +// Figma는 칸·글자 크기를 하나하나 줄여서(칸 8px·글자 4px) 따로 그렸지만, 여기서는 본문 배치를 +// `zoom`으로 줄여서 그린다. 칸 배치가 바뀌어도 미니맵을 따로 고칠 필요가 없고, 테두리 위치를 +// 스크롤 비율에 그대로 맞출 수 있다. 4px 글자를 직접 쓰면 브라우저 최소 글자 크기에 걸리는 것도 피한다. +// +// 테두리는 Figma처럼 미니맵보다 위아래로 크다(미니맵 110px · 테두리 128px, 미니맵이 10px 아래에서 시작). +function LockersLockerMinimap({ + children, + viewport, +}: LockersLockerMinimapProps) { + return ( +
+
+ {/* 축소본 안의 칸 버튼이 Tab·스크린리더에 두 번 잡히지 않게 막는다 */} +
+ {children} +
+
+
+
+ ); +} + +export default LockersLockerMinimap; diff --git a/src/features/lockers/components/LockersSectionPhotoModal.tsx b/src/features/lockers/components/LockersSectionPhotoModal.tsx new file mode 100644 index 0000000..c9cd950 --- /dev/null +++ b/src/features/lockers/components/LockersSectionPhotoModal.tsx @@ -0,0 +1,127 @@ +import { IconButton } from "@wanteddev/wds"; +import { IconClose } from "@wanteddev/wds-icon"; +import type { KeyboardEvent } from "react"; +import { useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; + +import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; +import type { + LockersPhotoPin, + LockersPhotoPinTone, +} from "@/features/lockers/constants/lockersLockers"; + +interface LockersSectionPhotoModalProps { + open: boolean; + sectionName: string; + photo: string; + pins: LockersPhotoPin[]; + onClose: () => void; +} + +const PIN_CLASS_NAMES: Record = { + lower: "bg-orange-90", + middle: "bg-orange-70", + upper: "bg-orange-50", +}; + +// Figma: A-1구역 실제사진 Modal (nodeId 2159:110299) — Stream 로컬. 닫기 아이콘 + 구역 사진에 +// 칸 번호 핀을 얹은 카드다. WDS `Modal`은 너비·모서리가 달라서 ConfirmModal과 같은 방식으로 +// 화면 포털에 직접 그린다. 닫기만 WDS `IconButton` + `IconClose`(Figma "Name=close, Thick=False")다. +// +// Figma 카드는 top 229px에 있지만 화면 높이가 기기마다 달라서 세로 가운데에 둔다. +function LockersSectionPhotoModal({ + open, + sectionName, + photo, + pins, + onClose, +}: LockersSectionPhotoModalProps) { + const portalEl = useScreenSheetPortal(); + const dialogRef = useRef(null); + const lastFocusedRef = useRef(null); + + // 열리면 포커스를 모달로 옮기고, 닫히면 사진 버튼으로 되돌린다 + useEffect(() => { + if (!open) { + return; + } + lastFocusedRef.current = + document.activeElement instanceof HTMLElement + ? document.activeElement + : null; + dialogRef.current?.focus(); + + return () => lastFocusedRef.current?.focus(); + }, [open]); + + // 포커스할 곳이 닫기 버튼뿐이라 Tab 가두기는 필요 없다 — Esc로 닫는 것만 받는다 + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + if (!portalEl) { + return null; + } + + return createPortal( +
+ {/* 딤은 Figma "딤드"(nodeId 2159:110298) 그대로 검정 60% */} +
, + portalEl, + ); +} + +export default LockersSectionPhotoModal; diff --git a/src/features/lockers/components/LockersSelectedLockerBar.tsx b/src/features/lockers/components/LockersSelectedLockerBar.tsx new file mode 100644 index 0000000..cfde78e --- /dev/null +++ b/src/features/lockers/components/LockersSelectedLockerBar.tsx @@ -0,0 +1,63 @@ +import { ActionArea, ActionAreaButton, Typography } from "@wanteddev/wds"; + +interface LockersSelectedLockerBarProps { + sectionName: string; + selectedLockerNumber: number | null; + onSubmit: () => void; +} + +// Figma: Locker Selection Section (nodeId 1658:179182) — Stream 로컬. 위가 둥근 흰 카드에 +// "선택한 사물함" 줄(Figma Locker Selector, 1643:178050)과 WDS Action Area를 담는다. +// 그림자는 Figma Shadow/Spread/Small 값 그대로다. +// +// Figma에는 선택 전(State=Empty) 모습만 있다. 고른 뒤에는 같은 자리에 "A-1구역 12번"처럼 +// 보여주고 버튼을 연다. +function LockersSelectedLockerBar({ + sectionName, + selectedLockerNumber, + onSubmit, +}: LockersSelectedLockerBarProps) { + const hasSelection = selectedLockerNumber !== null; + + return ( +
+
+ + 선택한 사물함 + + + {hasSelection + ? `${sectionName}구역 ${selectedLockerNumber}번` + : "선택한 사물함이 없어요."} + +
+ + {/* 높이 보정 이유는 wds-component-usage.md "Action Area 메인 버튼 높이" 참고 */} + + {hasSelection ? "신청하기" : "사물함을 선택해 주세요"} + + +
+
+ ); +} + +export default LockersSelectedLockerBar; From a7fcb8577c6a8df1f7196c7c3d323d7c9e61f686 Mon Sep 17 00:00:00 2001 From: gain <163835628+leegain1@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:23:26 +0900 Subject: [PATCH 04/19] =?UTF-8?q?docs:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EC=84=A0=ED=83=9D=20=ED=99=94=EB=A9=B4=20WDS=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/wds-component-usage.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 3f996e7..79e8d20 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -474,3 +474,26 @@ WDS에 이름이 비슷한 `ProgressIndicator`가 실제로 존재해서(`제외 ### 구역 카드 배경이 화면 배경과 같은 색이라 라우트에 `background: "normal"`이 필요하다 카드 배경이 `Background/Normal/Alternative`(`#f7f7f8`)인데 `ScreenLayout` 기본 배경도 같은 값이라, handle을 안 주면 카드가 배경에 완전히 묻힌다(실제로 처음 렌더에서 카드가 안 보였다). Figma 화면 배경이 흰 면이라 `handle: { background: "normal" }`을 준다. + +## 사물함 구역별 칸 선택 화면(`2159:110753` A-1, `2159:109533` A-2, `2159:110174` A-1 실제사진) 구현 중 확정된 매핑 + +| WDS 컴포넌트 | 확인 경로 | 비고 | +|---|---|---| +| `Icon/Normal/Image` | A-1 Top Navigation 트레일링 사진 버튼 | `search_design_system`에서 WDS 라이브러리 `component_set`으로 확인(componentKey `ba5095aab91509506b1380975f260bdb0658567f`), 코드 `IconImage` | +| `Name=close, Thick=False` (`Icon/Normal/Close`) | 실제사진 모달 닫기 | 코드 `IconClose`를 WDS `IconButton size={24}`로 감쌌다 | +| `Icon/Normal/Reset` | Top Navigation 트레일링 새로고침 | 구역 선택 화면과 같다 — `IconReset` | +| `Action Area/Action Area` | 하단 선택 영역 | `ActionAreaButton` + `paddingBlock: "16px"` 보정(위 "Action Area 메인 버튼 높이") | + +칸(`Locker Cell`)·칸 묶음(`Zone Area`)·`Shelf Label`·범례·미니맵·`Locker Selector`·실제사진 모달은 전부 Stream 로컬이다. `search_design_system`에서 `Locker Cell`은 결과가 없었다. + +### 선택 불가 칸은 범례 SVG 하나로 그린다 + +Figma 범례의 선택 불가 칸(`2159:110819`)은 `Label/Assistive` 바탕 + `Label/Neutral` 대각선을 합친 15px SVG다. `preserveAspectRatio="none"`이라 28px 칸에 그대로 늘려도 모서리(2.29 → 4.28px)·선 굵기가 본문 칸(radius 4)과 맞아서, 본문 칸도 같은 파일(`locker-unavailable.svg`)을 쓴다. + +### 미니맵은 따로 그리지 않고 본문 배치를 `zoom`으로 줄였다 + +Figma 미니맵은 칸 8px·글자 3.5~4px로 하나하나 다시 그렸지만(값도 칸마다 비율이 제각각), 코드에서는 본문 칸 배치를 `zoom: 0.31`로 줄여 그린다. 칸 배치가 바뀌어도 미니맵을 따로 고칠 일이 없고, 검은 테두리(`Highlight Overlay`)를 가로 스크롤 비율에 그대로 맞출 수 있다. 4px 글자를 직접 쓰면 브라우저 최소 글자 크기에 걸리는 문제도 피한다. A-2처럼 화면에 다 들어오는 구역은 Figma도 미니맵을 `opacity: 0`으로 숨겨서, 코드는 스크롤이 생길 때만 미니맵을 띄우고 128px 자리는 항상 남긴다. + +### Figma 아티팩트 — A-1 `Shelf Label` 높이가 1px이다 + +A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽 칸`이 칸 묶음 아래로 흘러내려 보인다. 미니맵(`2159:110761`)과 A-2(`2159:109622`)에서는 칸 묶음 높이에 맞춰 붙어 있어서, 본문도 칸 묶음 줄 높이(122px)에 맞췄다. From 3c8383be76ff615cc509780bf6e4c4ade401c81a Mon Sep 17 00:00:00 2001 From: gain <163835628+leegain1@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:59:26 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EC=84=A0=ED=83=9D=20=EC=83=81=ED=83=9C=EB=A5=BC=20?= =?UTF-8?q?Figma=20=EB=94=94=EC=9E=90=EC=9D=B8=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/components/LockersLockerCell.tsx | 18 ++++---- .../components/LockersSelectedLockerBar.tsx | 43 +++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/features/lockers/components/LockersLockerCell.tsx b/src/features/lockers/components/LockersLockerCell.tsx index 8847870..7c66ff0 100644 --- a/src/features/lockers/components/LockersLockerCell.tsx +++ b/src/features/lockers/components/LockersLockerCell.tsx @@ -16,8 +16,8 @@ interface LockersLockerCellProps { // 미니맵의 선택 불가 칸(2159:109565)처럼 지운다. 범례 SVG는 `preserveAspectRatio="none"`이라 // 28px로 늘려도 모서리가 4px로 맞는다. // -// 선택된 칸은 Figma에 없다. 구역 카드(LockersSectionCard)의 선택 표현(Blue/95 배경 + -// Primary/Normal 1px 테두리)을 그대로 따른다 — 테두리는 평소에도 투명으로 둬서 칸 크기를 고정한다. +// 선택된 칸은 Figma "사물함 선택 시"(2159:113315)대로 Primary/Normal 배경에 번호를 +// Background/Normal/Normal(흰색) SemiBold로 바꾼다. function LockersLockerCell({ locker, isSelected, @@ -36,19 +36,21 @@ function LockersLockerCell({ return (
, portalEl, From 5bf976c760f9870e68773dc16b4c43ba90ecb71b Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 1 Oct 2026 17:26:57 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EA=B5=AC=EC=97=AD=20=EC=83=81=EC=84=B8=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EA=B3=BC=20A-1=C2=B7A-2=20=EB=AA=A9=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../constants/lockersSectionDetails.ts | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/features/lockers/constants/lockersSectionDetails.ts diff --git a/src/features/lockers/constants/lockersSectionDetails.ts b/src/features/lockers/constants/lockersSectionDetails.ts new file mode 100644 index 0000000..675c3cf --- /dev/null +++ b/src/features/lockers/constants/lockersSectionDetails.ts @@ -0,0 +1,295 @@ +import sectionA1Photo from "@/assets/images/lockers/section-a-1-photo.jpg"; + +// 구역 상세 응답(GET /v1/app/lockers/sections/{sectionId})의 형태. +// 칸 배치(layout)는 구조만 갖고, 칸 상태(선택 가능·내 사물함)는 lockers에서 온다 — 둘은 lockerNumber로 잇는다. + +// "hug"는 내용 크기, "fill"은 부모의 남은 공간 채우기, 숫자는 px 고정. +export type LockersLayoutSize = "hug" | "fill" | number; + +// row·column의 교차축 정렬 +export type LockersLayoutAlign = "start" | "center" | "end" | "stretch"; + +interface LockersLayoutBlockBase { + width?: LockersLayoutSize; + height?: LockersLayoutSize; +} + +export interface LockersLayoutContainerBlock extends LockersLayoutBlockBase { + type: "row" | "column"; + children: LockersLayoutBlock[]; + gap?: number; + align?: LockersLayoutAlign; +} + +// 칸 묶음(Figma Zone Area + Locker Grid). rows[0]이 맨 위 행(위쪽 칸), 숫자는 lockerNumber, +// null은 칸이 없는 빈 자리다. bordered면 테두리 상자로 감싼다(A-1·B-2). +export interface LockersLayoutLockerGroupBlock extends LockersLayoutBlockBase { + type: "lockerGroup"; + rows: (number | null)[][]; + bordered?: boolean; +} + +// 회색 면 라벨(Figma Direction Label·Aisle). 창문은 가로, 벽면은 세로로 한 글자씩 줄을 바꾼다. +export interface LockersLayoutLabelBlock extends LockersLayoutBlockBase { + type: "label"; + text: string; + orientation?: "horizontal" | "vertical"; +} + +// 점선 상자(Figma Zone Label·Room Label·Stairs Area) — 옆 구역, 호실, 계단 +export interface LockersLayoutAreaBlock extends LockersLayoutBlockBase { + type: "area"; + text: string; + icon?: "stairs"; +} + +// 테두리 없는 글자(Figma 복도) +export interface LockersLayoutTextBlock extends LockersLayoutBlockBase { + type: "text"; + text: string; +} + +// 위쪽 칸 / 아래쪽 칸 표시(Figma Shelf Label) +export interface LockersLayoutShelfLabelBlock extends LockersLayoutBlockBase { + type: "shelfLabel"; +} + +export type LockersLayoutBlock = + | LockersLayoutContainerBlock + | LockersLayoutLockerGroupBlock + | LockersLayoutLabelBlock + | LockersLayoutAreaBlock + | LockersLayoutTextBlock + | LockersLayoutShelfLabelBlock; + +export interface LockersLayout { + version: 1; + root: LockersLayoutBlock; +} + +// 사진 위 칸 번호 핀. x·y는 핀 가운데의 위치를 사진 폭·높이 대비 0~1 비율로 적는다. +export interface LockersSectionPhotoPin { + lockerNumber: number; + x: number; + y: number; +} + +export interface LockersSectionPhoto { + url: string; + pins: LockersSectionPhotoPin[]; +} + +export interface LockersSectionLocker { + lockerId: number; + lockerNumber: number; + // 화면에 그대로 보여주는 이름("A-25"). 관리자가 입력하는 값이라 번호에서 만들지 않는다. + lockerLabel: string; + isAvailable: boolean; + isMine: boolean; +} + +export interface LockersSectionDetail { + sectionId: number; + section: string; + layout: LockersLayout; + photo: LockersSectionPhoto; + lockers: LockersSectionLocker[]; +} + +// 목데이터용 — 실제로는 서버가 lockers를 내려준다. Figma 목업이 전부 선택 가능이라 그대로 둔다. +// 이름은 서버가 관리자 입력값으로 주지만, 목데이터는 Figma 표기("A-25", 동 이름 + 번호)대로 만든다. +function createMockLockers( + building: string, + lockerNumbers: number[], +): LockersSectionLocker[] { + return lockerNumbers.map((lockerNumber) => ({ + isAvailable: true, + isMine: false, + lockerId: lockerNumber, + lockerLabel: `${building}-${lockerNumber}`, + lockerNumber, + })); +} + +function range(from: number, to: number) { + return Array.from({ length: to - from + 1 }, (_, index) => from + index); +} + +// Figma: A-1구역 (nodeId 2159:110753) — 창문을 사이에 두고 벽면 세 곳에 테두리 묶음이 붙어 있다. +// 핀은 A-1구역 실제사진(2159:110299)의 좌표를 사진 원본 대비 비율로 옮겼다. Figma는 사진을 +// 1.12° 돌려 붙였는데 서버는 잘라 둔 사진을 줄 거라 회전은 무시했다(가장자리에서 2~3px 차이). +const A1_SECTION: LockersSectionDetail = { + layout: { + root: { + align: "end", + children: [ + { + align: "end", + children: [ + { height: 122, type: "shelfLabel" }, + { + align: "center", + children: [ + { text: "창문", type: "label", width: 224 }, + { + children: [ + { + height: "fill", + orientation: "vertical", + text: "왼쪽 벽면", + type: "label", + }, + { + bordered: true, + rows: [ + [10, 11, 12], + [13, 14, 15], + [16, 17, 18], + ], + type: "lockerGroup", + }, + { + bordered: true, + rows: [ + [25, 26, 27, 28, 29, 30], + [22, 23, 24, 31, 32, 33], + [19, 20, 21, 34, 35, 36], + ], + type: "lockerGroup", + }, + { + bordered: true, + rows: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + type: "lockerGroup", + }, + { + height: "fill", + orientation: "vertical", + text: "오른쪽 벽면", + type: "label", + }, + ], + gap: 12, + type: "row", + }, + ], + gap: 8, + type: "column", + }, + ], + gap: 12, + type: "row", + }, + { text: "복도", type: "text", width: 560 }, + ], + gap: 32, + type: "column", + }, + version: 1, + }, + lockers: createMockLockers("A", range(1, 36)), + photo: { + pins: [ + { lockerNumber: 1, x: 0.758, y: 0.392 }, + { lockerNumber: 2, x: 0.836, y: 0.403 }, + { lockerNumber: 3, x: 0.936, y: 0.418 }, + { lockerNumber: 4, x: 0.758, y: 0.54 }, + { lockerNumber: 5, x: 0.836, y: 0.594 }, + { lockerNumber: 6, x: 0.936, y: 0.667 }, + { lockerNumber: 7, x: 0.758, y: 0.667 }, + { lockerNumber: 8, x: 0.836, y: 0.758 }, + { lockerNumber: 9, x: 0.936, y: 0.852 }, + { lockerNumber: 10, x: 0.081, y: 0.437 }, + { lockerNumber: 11, x: 0.175, y: 0.413 }, + { lockerNumber: 12, x: 0.243, y: 0.396 }, + { lockerNumber: 13, x: 0.081, y: 0.635 }, + { lockerNumber: 14, x: 0.175, y: 0.583 }, + { lockerNumber: 15, x: 0.243, y: 0.54 }, + { lockerNumber: 16, x: 0.081, y: 0.848 }, + { lockerNumber: 17, x: 0.175, y: 0.751 }, + { lockerNumber: 18, x: 0.243, y: 0.678 }, + { lockerNumber: 19, x: 0.345, y: 0.622 }, + { lockerNumber: 20, x: 0.406, y: 0.622 }, + { lockerNumber: 21, x: 0.466, y: 0.622 }, + { lockerNumber: 22, x: 0.345, y: 0.521 }, + { lockerNumber: 23, x: 0.406, y: 0.521 }, + { lockerNumber: 24, x: 0.466, y: 0.521 }, + { lockerNumber: 25, x: 0.345, y: 0.419 }, + { lockerNumber: 26, x: 0.406, y: 0.419 }, + { lockerNumber: 27, x: 0.466, y: 0.419 }, + { lockerNumber: 28, x: 0.527, y: 0.419 }, + { lockerNumber: 29, x: 0.588, y: 0.419 }, + { lockerNumber: 30, x: 0.648, y: 0.419 }, + { lockerNumber: 31, x: 0.527, y: 0.521 }, + { lockerNumber: 32, x: 0.588, y: 0.521 }, + { lockerNumber: 33, x: 0.648, y: 0.521 }, + { lockerNumber: 34, x: 0.527, y: 0.622 }, + { lockerNumber: 35, x: 0.588, y: 0.622 }, + { lockerNumber: 36, x: 0.648, y: 0.622 }, + ], + url: sectionA1Photo, + }, + section: "A-1", + sectionId: 1, +}; + +// Figma: A-2구역 (nodeId 2159:109533) — 칸 묶음 옆이 B-1구역, 복도 건너편이 231호실이다. +// A-2 실제 사진은 아직 없어서(직접 찍을 예정) A-1 사진을 임시로 쓰고, 번호가 맞지 않으니 핀은 비워 둔다. +const A2_SECTION: LockersSectionDetail = { + layout: { + root: { + children: [ + { height: 96, type: "shelfLabel" }, + { + align: "center", + children: [ + { + children: [ + { + rows: [ + [82, 83, 84], + [85, 86, 87], + [88, 89, 90], + ], + type: "lockerGroup", + }, + { + height: "fill", + text: "B-1구역", + type: "area", + width: "fill", + }, + ], + gap: 12, + type: "row", + width: "fill", + }, + { text: "복도", type: "text" }, + { text: "231호실", type: "area", width: "fill" }, + ], + gap: 32, + type: "column", + width: "fill", + }, + ], + gap: 12, + type: "row", + }, + version: 1, + }, + lockers: createMockLockers("A", range(82, 90)), + photo: { pins: [], url: sectionA1Photo }, + section: "A-2", + sectionId: 2, +}; + +// API 연동 전까지 쓰는 구역 상세 목데이터. 지금 주소가 구역 라벨(/lockers/apply/sections/A-1)이라 +// 라벨로 찾는다. 나머지 구역은 서버 시드가 들어오면 응답으로 대체된다. +export const LOCKERS_SECTION_DETAILS: Record = { + "A-1": A1_SECTION, + "A-2": A2_SECTION, +}; From 701f1ad6be0a6a530daeb4f40b357639048426de Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 1 Oct 2026 17:36:43 +0900 Subject: [PATCH 10/19] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=A0=90=EC=84=A0=20=EC=83=81=EC=9E=90=C2=B7=ED=9A=8C?= =?UTF-8?q?=EC=83=89=20=EB=9D=BC=EB=B2=A8=EC=9D=84=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EB=B6=80=ED=92=88=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/components/LockersA1LockerMap.tsx | 44 ++++---------- .../lockers/components/LockersA2LockerMap.tsx | 26 ++------- .../lockers/components/LockersFloorMap.tsx | 57 +++---------------- .../lockers/components/LockersMapArea.tsx | 34 +++++++++++ .../lockers/components/LockersMapLabel.tsx | 43 ++++++++++++++ 5 files changed, 104 insertions(+), 100 deletions(-) create mode 100644 src/features/lockers/components/LockersMapArea.tsx create mode 100644 src/features/lockers/components/LockersMapLabel.tsx diff --git a/src/features/lockers/components/LockersA1LockerMap.tsx b/src/features/lockers/components/LockersA1LockerMap.tsx index a685a3f..d9397b3 100644 --- a/src/features/lockers/components/LockersA1LockerMap.tsx +++ b/src/features/lockers/components/LockersA1LockerMap.tsx @@ -1,7 +1,7 @@ import { Typography } from "@wanteddev/wds"; -import type { ReactNode } from "react"; import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; +import LockersMapLabel from "@/features/lockers/components/LockersMapLabel"; import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; import { LOCKERS_A1_GROUPS, @@ -13,32 +13,6 @@ interface LockersA1LockerMapProps { onSelect: (lockerNumber: number) => void; } -// 창문·벽면처럼 고를 수 없는 자리 표시. Figma "Direction Label"·"Aisle" -function MapLabel({ - children, - className, -}: { - children: ReactNode; - className: string; -}) { - return ( -
- - {children} - -
- ); -} - // Figma: Zone Area (nodeId 2159:110831 외) — 칸 묶음을 감싸는 테두리 function ZoneArea({ group, @@ -73,15 +47,21 @@ function LockersA1LockerMap({
- 창문 +
- {"왼\n쪽\n\n벽\n면"} + - - {"오\n른\n쪽\n\n벽\n면"} - +
diff --git a/src/features/lockers/components/LockersA2LockerMap.tsx b/src/features/lockers/components/LockersA2LockerMap.tsx index b325a49..d9839b9 100644 --- a/src/features/lockers/components/LockersA2LockerMap.tsx +++ b/src/features/lockers/components/LockersA2LockerMap.tsx @@ -1,6 +1,7 @@ import { Typography } from "@wanteddev/wds"; import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; +import LockersMapArea from "@/features/lockers/components/LockersMapArea"; import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; import { LOCKERS_A2_GROUP } from "@/features/lockers/constants/lockersLockers"; @@ -9,24 +10,6 @@ interface LockersA2LockerMapProps { onSelect: (lockerNumber: number) => void; } -// 옆 구역·호실처럼 고를 수 없는 자리. 구역 평면도(LockersFloorMap)의 MapArea와 같은 점선 상자다. -function MapArea({ label, className }: { label: string; className: string }) { - return ( -
- - {label} - -
- ); -} - // Figma: A-2구역 Locker Grid Section (nodeId 2159:109622) — 칸 묶음 옆이 B-1구역, 복도 건너편이 // 231호실이다. 실제 공간 배치라 레이아웃이 갖는다. 화면 폭(335px) 안에 들어가서 스크롤이 생기지 않는다. function LockersA2LockerMap({ @@ -43,7 +26,10 @@ function LockersA2LockerMap({ onSelect={onSelect} selectedLockerNumber={selectedLockerNumber} /> - +
복도 - + ); diff --git a/src/features/lockers/components/LockersFloorMap.tsx b/src/features/lockers/components/LockersFloorMap.tsx index 2965360..b91011d 100644 --- a/src/features/lockers/components/LockersFloorMap.tsx +++ b/src/features/lockers/components/LockersFloorMap.tsx @@ -1,9 +1,9 @@ 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 LockersMapArea from "@/features/lockers/components/LockersMapArea"; import LockersSectionCard from "@/features/lockers/components/LockersSectionCard"; import { LOCKERS_LEFT_SECTIONS, @@ -16,41 +16,6 @@ interface LockersFloorMapProps { 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) // // 호실·화장실·계단·복도 위치는 실제 건물 구조라 데이터로 받지 않고 여기 레이아웃이 갖는다. @@ -78,16 +43,14 @@ function LockersFloorMap({ return (
- - - + {LOCKERS_LEFT_SECTIONS.map(renderCard)} - - - - - - + +
{/* 가운데 열은 복도다 — 카드가 없고 방향 표시만 있다. @@ -121,9 +84,7 @@ function LockersFloorMap({
{LOCKERS_RIGHT_SECTIONS.map(renderCard)} - - - + {LOCKERS_RIGHT_SECTIONS_BELOW_STAIRS.map(renderCard)}
diff --git a/src/features/lockers/components/LockersMapArea.tsx b/src/features/lockers/components/LockersMapArea.tsx new file mode 100644 index 0000000..0aa3c85 --- /dev/null +++ b/src/features/lockers/components/LockersMapArea.tsx @@ -0,0 +1,34 @@ +import { Typography } from "@wanteddev/wds"; + +interface LockersMapAreaProps { + label: string; + icon?: string; + /** 크기·배치(flex 비율, 폭, 세로 여백 등) — 화면마다 다르다 */ + className?: string; +} + +// Figma: 호실·화장실·계단·옆 구역처럼 고를 수 없는 자리(Room Label·Zone Label·Stairs Area) — Stream 로컬. +// 점선 테두리 흰 상자 가운데에 라벨(+ 아이콘)을 둔다. 구역 평면도와 구역별 칸 배치가 같이 쓴다. +function LockersMapArea({ label, icon, className = "" }: LockersMapAreaProps) { + return ( +
+
+ {icon !== undefined && ( + + )} + + {label} + +
+
+ ); +} + +export default LockersMapArea; diff --git a/src/features/lockers/components/LockersMapLabel.tsx b/src/features/lockers/components/LockersMapLabel.tsx new file mode 100644 index 0000000..38ef0de --- /dev/null +++ b/src/features/lockers/components/LockersMapLabel.tsx @@ -0,0 +1,43 @@ +import { Typography } from "@wanteddev/wds"; + +interface LockersMapLabelProps { + text: string; + orientation?: "horizontal" | "vertical"; + /** 크기(창문 폭, 벽면 높이 등) — 화면마다 다르다 */ + className?: string; +} + +// 세로 라벨은 Figma가 한 글자씩 줄을 바꾸고 단어 사이에 빈 줄을 둔다("왼쪽 벽면" → 왼/쪽/ /벽/면). +function toVerticalText(text: string) { + return [...text].map((char) => (char === " " ? "" : char)).join("\n"); +} + +// Figma: 창문·벽면처럼 고를 수 없는 자리(Direction Label·Aisle) — Stream 로컬. 회색 면에 라벨을 둔다. +// 가로(창문)는 위아래, 세로(벽면)는 좌우에만 여백을 둔다. +function LockersMapLabel({ + text, + orientation = "horizontal", + className = "", +}: LockersMapLabelProps) { + const isVertical = orientation === "vertical"; + + return ( +
+ + {isVertical ? toVerticalText(text) : text} + +
+ ); +} + +export default LockersMapLabel; From 5b917a38d3df41cae939ec8aa7db872b2fcbda41 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 1 Oct 2026 17:41:53 +0900 Subject: [PATCH 11/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EB=AC=B6=EC=9D=8C=EC=9D=84=20layout=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A1=9C=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?=EB=82=B4=20=EC=82=AC=EB=AC=BC=ED=95=A8=20=EC=B9=B8=EC=9D=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/components/LockersA1LockerMap.tsx | 13 ++++- .../lockers/components/LockersA2LockerMap.tsx | 15 +++++- .../lockers/components/LockersLockerCell.tsx | 32 ++++++++++-- .../lockers/components/LockersLockerGrid.tsx | 50 ++++++++++++++----- .../lockers/components/LockersShelfLabel.tsx | 11 ++-- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/features/lockers/components/LockersA1LockerMap.tsx b/src/features/lockers/components/LockersA1LockerMap.tsx index d9397b3..018354b 100644 --- a/src/features/lockers/components/LockersA1LockerMap.tsx +++ b/src/features/lockers/components/LockersA1LockerMap.tsx @@ -7,6 +7,14 @@ import { LOCKERS_A1_GROUPS, type LockersLockerGroup, } from "@/features/lockers/constants/lockersLockers"; +import { LOCKERS_SECTION_DETAILS } from "@/features/lockers/constants/lockersSectionDetails"; + +const A1_LOCKERS = new Map( + LOCKERS_SECTION_DETAILS["A-1"].lockers.map((locker) => [ + locker.lockerNumber, + locker, + ]), +); interface LockersA1LockerMapProps { selectedLockerNumber: number | null; @@ -22,8 +30,9 @@ function ZoneArea({ return (
row.map((locker) => locker.number))} selectedLockerNumber={selectedLockerNumber} />
@@ -45,7 +54,7 @@ function LockersA1LockerMap({ return (
- +
diff --git a/src/features/lockers/components/LockersA2LockerMap.tsx b/src/features/lockers/components/LockersA2LockerMap.tsx index d9839b9..b6fb8fd 100644 --- a/src/features/lockers/components/LockersA2LockerMap.tsx +++ b/src/features/lockers/components/LockersA2LockerMap.tsx @@ -4,6 +4,14 @@ import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; import LockersMapArea from "@/features/lockers/components/LockersMapArea"; import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; import { LOCKERS_A2_GROUP } from "@/features/lockers/constants/lockersLockers"; +import { LOCKERS_SECTION_DETAILS } from "@/features/lockers/constants/lockersSectionDetails"; + +const A2_LOCKERS = new Map( + LOCKERS_SECTION_DETAILS["A-2"].lockers.map((locker) => [ + locker.lockerNumber, + locker, + ]), +); interface LockersA2LockerMapProps { selectedLockerNumber: number | null; @@ -18,11 +26,14 @@ function LockersA2LockerMap({ }: LockersA2LockerMapProps) { return (
- +
+ row.map((locker) => locker.number), + )} onSelect={onSelect} selectedLockerNumber={selectedLockerNumber} /> diff --git a/src/features/lockers/components/LockersLockerCell.tsx b/src/features/lockers/components/LockersLockerCell.tsx index 7c66ff0..f59ebbe 100644 --- a/src/features/lockers/components/LockersLockerCell.tsx +++ b/src/features/lockers/components/LockersLockerCell.tsx @@ -1,10 +1,10 @@ import { Typography } from "@wanteddev/wds"; import lockerUnavailableIcon from "@/assets/icons/lockers/locker-unavailable.svg"; -import type { LockersLocker } from "@/features/lockers/constants/lockersLockers"; +import type { LockersSectionLocker } from "@/features/lockers/constants/lockersSectionDetails"; interface LockersLockerCellProps { - locker: LockersLocker; + locker: LockersSectionLocker; isSelected: boolean; onSelect: (lockerNumber: number) => void; } @@ -18,15 +18,37 @@ interface LockersLockerCellProps { // // 선택된 칸은 Figma "사물함 선택 시"(2159:113315)대로 Primary/Normal 배경에 번호를 // Background/Normal/Normal(흰색) SemiBold로 바꾼다. +// +// 내 사물함(이미 신청한 칸)은 Figma에 상태가 없어서 임시로 초록 테두리(Status/Positive)를 두른다. +// 이미 신청한 칸이라 다시 고를 수 없다 — 디자인이 나오면 바꾼다. function LockersLockerCell({ locker, isSelected, onSelect, }: LockersLockerCellProps) { + if (locker.isMine) { + return ( +
+ + {locker.lockerNumber} + +
+ ); + } + if (!locker.isAvailable) { return ( {`${locker.number}번 @@ -39,7 +61,7 @@ function LockersLockerCell({ className={`flex size-7 items-center justify-center rounded-sm ${ isSelected ? "bg-primary" : "bg-orange-95" }`} - onClick={() => onSelect(locker.number)} + onClick={() => onSelect(locker.lockerNumber)} type="button" > - {locker.number} + {locker.lockerNumber} ); diff --git a/src/features/lockers/components/LockersLockerGrid.tsx b/src/features/lockers/components/LockersLockerGrid.tsx index 9101eee..f5192ea 100644 --- a/src/features/lockers/components/LockersLockerGrid.tsx +++ b/src/features/lockers/components/LockersLockerGrid.tsx @@ -1,31 +1,55 @@ import LockersLockerCell from "@/features/lockers/components/LockersLockerCell"; -import type { LockersLockerGroup } from "@/features/lockers/constants/lockersLockers"; +import type { LockersSectionLocker } from "@/features/lockers/constants/lockersSectionDetails"; interface LockersLockerGridProps { - group: LockersLockerGroup; + /** layout의 lockerGroup.rows — 숫자는 lockerNumber, null은 칸이 없는 빈 자리 */ + rows: (number | null)[][]; + /** 구역의 사물함을 lockerNumber로 찾는 표 */ + lockers: ReadonlyMap; selectedLockerNumber: number | null; onSelect: (lockerNumber: number) => void; } -// Figma: Locker Grid (nodeId 2159:110832 외) — 칸 사이 6px. 열 수는 묶음의 첫 행 길이를 따른다. +// Figma: Locker Grid (nodeId 2159:110832 외) — 칸 사이 6px. 열 수는 가장 긴 행을 따른다. +// layout에 있는데 lockers에 없는 번호는 서버 데이터가 어긋난 경우라, 칸 대신 빈 자리로 둔다. function LockersLockerGrid({ - group, + rows, + lockers, selectedLockerNumber, onSelect, }: LockersLockerGridProps) { + const columnCount = Math.max(...rows.map((row) => row.length)); + return (
- {group.flat().map((locker) => ( - - ))} + {rows.flatMap((row, rowIndex) => + row.map((lockerNumber, columnIndex) => { + const locker = + lockerNumber === null ? undefined : lockers.get(lockerNumber); + + if (locker === undefined) { + return ( +
+ ); + } + + return ( + + ); + }), + )}
); } diff --git a/src/features/lockers/components/LockersShelfLabel.tsx b/src/features/lockers/components/LockersShelfLabel.tsx index 4e012a5..d213718 100644 --- a/src/features/lockers/components/LockersShelfLabel.tsx +++ b/src/features/lockers/components/LockersShelfLabel.tsx @@ -3,8 +3,8 @@ import { Typography } from "@wanteddev/wds"; import shelfDividerIcon from "@/assets/icons/lockers/shelf-divider.svg"; interface LockersShelfLabelProps { - /** 칸 묶음 높이에 맞춘다 — A-1은 테두리 있는 묶음(122px), A-2는 칸만(96px) */ - className: string; + /** 칸 묶음 높이(px)에 맞춘다 — 테두리 있는 3행 묶음은 122, 칸만 있는 3행 묶음은 96 */ + height: number; } function ShelfText({ children }: { children: string }) { @@ -24,9 +24,12 @@ function ShelfText({ children }: { children: string }) { // Figma: Shelf Label (nodeId 1628:174373) — Stream 로컬. 칸 묶음 왼쪽에서 위아래 방향을 알려준다. // 가운데 점선은 Figma가 가로 Divider를 90° 돌려 그린 것이라 같은 방식(컨테이너 단위로 길이를 // 맞춘 뒤 회전)으로 옮긴다. SVG 원본 크기(120×1.2)는 건드리지 않는다. -function LockersShelfLabel({ className }: LockersShelfLabelProps) { +function LockersShelfLabel({ height }: LockersShelfLabelProps) { return ( -
+
위쪽 칸
Date: Thu, 1 Oct 2026 23:47:38 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20lay?= =?UTF-8?q?out=20JSON=EC=9D=84=20=EA=B7=B8=EB=A6=AC=EB=8A=94=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/LockersLayoutRenderer.tsx | 193 ++++++++++++++++++ .../lockers/components/LockersMapArea.tsx | 11 +- .../lockers/components/LockersMapLabel.tsx | 5 + .../lockers/components/LockersShelfLabel.tsx | 11 +- 4 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 src/features/lockers/components/LockersLayoutRenderer.tsx diff --git a/src/features/lockers/components/LockersLayoutRenderer.tsx b/src/features/lockers/components/LockersLayoutRenderer.tsx new file mode 100644 index 0000000..ffbad54 --- /dev/null +++ b/src/features/lockers/components/LockersLayoutRenderer.tsx @@ -0,0 +1,193 @@ +import { Typography } from "@wanteddev/wds"; +import type { CSSProperties } from "react"; + +import stairsIcon from "@/assets/icons/lockers/stairs.svg"; +import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; +import LockersMapArea from "@/features/lockers/components/LockersMapArea"; +import LockersMapLabel from "@/features/lockers/components/LockersMapLabel"; +import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; +import type { + LockersLayout, + LockersLayoutAlign, + LockersLayoutAreaBlock, + LockersLayoutBlock, + LockersLayoutSize, + LockersSectionLocker, +} from "@/features/lockers/constants/lockersSectionDetails"; + +interface LockersLayoutRendererProps { + layout: LockersLayout; + /** 구역의 사물함을 lockerNumber로 찾는 표 */ + lockers: ReadonlyMap; + selectedLockerNumber: number | null; + onSelect: (lockerNumber: number) => void; +} + +type LockersLayoutBlockContext = Omit; + +// 블록이 놓이는 부모의 방향. 같은 "fill"이라도 부모 방향과 같은 축이면 남은 공간을 나눠 갖고(flex-1), +// 다른 축이면 부모 폭·높이에 맞춰 늘어난다(self-stretch). +type LockersLayoutDirection = "row" | "column"; + +const ALIGN_CLASS_NAMES: Record = { + center: "items-center", + end: "items-end", + start: "items-start", + stretch: "items-stretch", +}; + +const AREA_ICONS: Record< + NonNullable, + string +> = { + stairs: stairsIcon, +}; + +function getSizing( + block: LockersLayoutBlock, + parentDirection: LockersLayoutDirection, +) { + const isRow = parentDirection === "row"; + const mainSize: LockersLayoutSize = + (isRow ? block.width : block.height) ?? "hug"; + const crossSize: LockersLayoutSize = + (isRow ? block.height : block.width) ?? "hug"; + const mainProperty = isRow ? "width" : "height"; + const crossProperty = isRow ? "height" : "width"; + + const classNames = [mainSize === "fill" ? "flex-1" : "shrink-0"]; + const style: CSSProperties = {}; + + if (typeof mainSize === "number") { + style[mainProperty] = mainSize; + } + if (crossSize === "fill") { + classNames.push("self-stretch"); + } else if (typeof crossSize === "number") { + style[crossProperty] = crossSize; + } + + return { className: classNames.join(" "), style }; +} + +function LockersLayoutBlockView({ + block, + parentDirection, + context, +}: { + block: LockersLayoutBlock; + parentDirection: LockersLayoutDirection; + context: LockersLayoutBlockContext; +}) { + const sizing = getSizing(block, parentDirection); + + switch (block.type) { + case "row": + case "column": + return ( +
+ {block.children.map((child, index) => ( + + ))} +
+ ); + case "lockerGroup": { + const grid = ( + + ); + // Figma: Zone Area (nodeId 2159:110831 외) — 칸 묶음을 감싸는 테두리 + return block.bordered ? ( +
+ {grid} +
+ ) : ( +
+ {grid} +
+ ); + } + case "label": + return ( + + ); + case "area": + return ( + + ); + case "text": + return ( +
+ + {block.text} + +
+ ); + case "shelfLabel": + return ( + + ); + default: + // 서버가 이 버전의 프론트가 모르는 블록을 보내도 화면 전체가 깨지지 않게 그 블록만 건너뛴다 + return null; + } +} + +// layout JSON(블록 7종)을 그대로 그린다. 구역마다 배치 컴포넌트를 따로 두지 않고, 구역별 배치는 +// 서버가 주는 layout이 갖는다. 칸 크기·색·간격 같은 디자인 규칙은 각 부품이 갖는다. +// +// 최상위 블록은 부모(가로 스크롤 영역) 폭보다 좁아지지 않는다 — 화면에 다 들어오는 구역에서 +// "fill" 상자(A-2의 B-1구역 등)가 화면 끝까지 늘어나게 하려는 것이다. +function LockersLayoutRenderer({ + layout, + ...context +}: LockersLayoutRendererProps) { + return ( +
+ +
+ ); +} + +export default LockersLayoutRenderer; diff --git a/src/features/lockers/components/LockersMapArea.tsx b/src/features/lockers/components/LockersMapArea.tsx index 0aa3c85..13ec24f 100644 --- a/src/features/lockers/components/LockersMapArea.tsx +++ b/src/features/lockers/components/LockersMapArea.tsx @@ -1,18 +1,27 @@ import { Typography } from "@wanteddev/wds"; +import type { CSSProperties } from "react"; interface LockersMapAreaProps { label: string; icon?: string; /** 크기·배치(flex 비율, 폭, 세로 여백 등) — 화면마다 다르다 */ className?: string; + /** px로 정한 크기(layout의 숫자 width·height) */ + style?: CSSProperties; } // Figma: 호실·화장실·계단·옆 구역처럼 고를 수 없는 자리(Room Label·Zone Label·Stairs Area) — Stream 로컬. // 점선 테두리 흰 상자 가운데에 라벨(+ 아이콘)을 둔다. 구역 평면도와 구역별 칸 배치가 같이 쓴다. -function LockersMapArea({ label, icon, className = "" }: LockersMapAreaProps) { +function LockersMapArea({ + label, + icon, + className = "", + style, +}: LockersMapAreaProps) { return (
{icon !== undefined && ( diff --git a/src/features/lockers/components/LockersMapLabel.tsx b/src/features/lockers/components/LockersMapLabel.tsx index 38ef0de..cae8e54 100644 --- a/src/features/lockers/components/LockersMapLabel.tsx +++ b/src/features/lockers/components/LockersMapLabel.tsx @@ -1,10 +1,13 @@ import { Typography } from "@wanteddev/wds"; +import type { CSSProperties } from "react"; interface LockersMapLabelProps { text: string; orientation?: "horizontal" | "vertical"; /** 크기(창문 폭, 벽면 높이 등) — 화면마다 다르다 */ className?: string; + /** px로 정한 크기(layout의 숫자 width·height) */ + style?: CSSProperties; } // 세로 라벨은 Figma가 한 글자씩 줄을 바꾸고 단어 사이에 빈 줄을 둔다("왼쪽 벽면" → 왼/쪽/ /벽/면). @@ -18,6 +21,7 @@ function LockersMapLabel({ text, orientation = "horizontal", className = "", + style, }: LockersMapLabelProps) { const isVertical = orientation === "vertical"; @@ -26,6 +30,7 @@ function LockersMapLabel({ className={`flex items-center justify-center rounded-sm bg-background-alternative ${ isVertical ? "px-1.5" : "py-1.5" } ${className}`} + style={style} > 위쪽 칸 From 2dfa19008e7cab2c45fa3d84a2ec6f844ed0a67d Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 1 Oct 2026 23:55:12 +0900 Subject: [PATCH 13/19] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=B9=B8=20=EC=84=A0=ED=83=9D=20=ED=99=94=EB=A9=B4=EC=9D=84?= =?UTF-8?q?=20layout=20=EB=A0=8C=EB=8D=94=EB=9F=AC=EB=A1=9C=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20=EA=B5=AC=EC=97=AD=EB=B3=84=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/LockersLockerSelectScreen.tsx | 83 +++++++---------- .../lockers/components/LockersA1LockerMap.tsx | 93 ------------------- .../lockers/components/LockersA2LockerMap.tsx | 59 ------------ .../components/LockersSelectedLockerBar.tsx | 15 +-- .../lockers/constants/lockersLockers.ts | 42 --------- 5 files changed, 41 insertions(+), 251 deletions(-) delete mode 100644 src/features/lockers/components/LockersA1LockerMap.tsx delete mode 100644 src/features/lockers/components/LockersA2LockerMap.tsx diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index f9a7a22..67225a6 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -1,15 +1,13 @@ import { TopNavigationButton } from "@wanteddev/wds"; import { IconChevronLeft, IconImage, IconReset } from "@wanteddev/wds-icon"; -import type { ComponentType } from "react"; -import { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import ComingSoonScreen from "@/app/ComingSoonScreen"; import sectionA1Photo from "@/assets/images/lockers/section-a-1-photo.jpg"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; -import LockersA1LockerMap from "@/features/lockers/components/LockersA1LockerMap"; -import LockersA2LockerMap from "@/features/lockers/components/LockersA2LockerMap"; +import LockersLayoutRenderer from "@/features/lockers/components/LockersLayoutRenderer"; import LockersLockerLegend from "@/features/lockers/components/LockersLockerLegend"; import LockersLockerMinimap, { type LockersLockerMinimapViewport, @@ -20,53 +18,32 @@ import { LOCKERS_A1_PHOTO_PINS, type LockersPhotoPin, } from "@/features/lockers/constants/lockersLockers"; +import { + LOCKERS_SECTION_DETAILS, + type LockersSectionDetail, +} from "@/features/lockers/constants/lockersSectionDetails"; -interface LockerMapProps { - selectedLockerNumber: number | null; - onSelect: (lockerNumber: number) => void; -} - -interface SectionLayout { - LockerMap: ComponentType; - photo?: { src: string; pins: LockersPhotoPin[] }; -} - -// 구역마다 칸 배치가 실제 공간 구조라 모양이 전부 달라서, 구역별 배치 컴포넌트를 둔다. -// 아직 Figma에 A-1·A-2만 있다. -const SECTION_LAYOUTS: Record = { - "A-1": { - LockerMap: LockersA1LockerMap, - photo: { pins: LOCKERS_A1_PHOTO_PINS, src: sectionA1Photo }, - }, - "A-2": { LockerMap: LockersA2LockerMap }, -}; +// 실제 사진 모달은 아직 예전 핀 형식(px 좌표)을 쓴다 — 구역 상세의 photo(비율 좌표)로 바꿀 때 지운다 +const LEGACY_PHOTOS: Record = + { + "A-1": { pins: LOCKERS_A1_PHOTO_PINS, src: sectionA1Photo }, + }; // Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) +// 구역마다 다른 칸 배치는 구역 상세의 layout이 갖고, 이 화면은 그걸 렌더러로 그리기만 한다. function LockersLockerSelectScreen() { const { sectionId = "" } = useParams(); - const layout = SECTION_LAYOUTS[sectionId]; + const detail = LOCKERS_SECTION_DETAILS[sectionId]; - if (layout === undefined) { + if (detail === undefined) { return ; } // 구역이 바뀌면 고른 칸·스크롤을 처음부터 다시 잡는다 - return ( - - ); + return ; } -function SectionLockerSelect({ - sectionId, - layout, -}: { - sectionId: string; - layout: SectionLayout; -}) { +function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { const navigate = useNavigate(); const location = useLocation(); const [selectedLockerNumber, setSelectedLockerNumber] = useState< @@ -77,7 +54,17 @@ function SectionLockerSelect({ null, ); const scrollRef = useRef(null); - const { LockerMap, photo } = layout; + const photo = LEGACY_PHOTOS[detail.section]; + + const lockers = useMemo( + () => + new Map(detail.lockers.map((locker) => [locker.lockerNumber, locker])), + [detail.lockers], + ); + const selectedLocker = + selectedLockerNumber === null + ? undefined + : lockers.get(selectedLockerNumber); // 칸 배치가 화면보다 넓을 때만 미니맵을 띄운다 — Figma도 화면에 다 들어오는 A-2는 // 미니맵을 투명하게 숨겨 자리만 남겨뒀다. @@ -123,7 +110,7 @@ function SectionLockerSelect({ } - title={`${sectionId}구역 사물함 선택`} + title={`${detail.section}구역 사물함 선택`} trailing={ <> {photo && ( @@ -150,7 +137,9 @@ function SectionLockerSelect({ ); const map = ( - @@ -170,21 +159,21 @@ function SectionLockerSelect({ )}
- {/* 화면 끝까지 스크롤되도록 좌우 여백을 스크롤 영역 안쪽에 둔다 */} + {/* 화면 끝까지 스크롤되도록 좌우 여백을 스크롤 영역 안쪽에 둔다. + min-w-full: 화면에 다 들어오는 구역에서 layout의 "fill" 상자가 화면 폭까지 늘어나게 한다 */}
-
{map}
+
{map}
{}} - sectionName={sectionId} - selectedLockerNumber={selectedLockerNumber} + lockerLabel={selectedLocker?.lockerLabel ?? null} /> {photo && ( @@ -193,7 +182,7 @@ function SectionLockerSelect({ open={isPhotoOpen} photo={photo.src} pins={photo.pins} - sectionName={sectionId} + sectionName={detail.section} /> )}
diff --git a/src/features/lockers/components/LockersA1LockerMap.tsx b/src/features/lockers/components/LockersA1LockerMap.tsx deleted file mode 100644 index 018354b..0000000 --- a/src/features/lockers/components/LockersA1LockerMap.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Typography } from "@wanteddev/wds"; - -import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; -import LockersMapLabel from "@/features/lockers/components/LockersMapLabel"; -import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; -import { - LOCKERS_A1_GROUPS, - type LockersLockerGroup, -} from "@/features/lockers/constants/lockersLockers"; -import { LOCKERS_SECTION_DETAILS } from "@/features/lockers/constants/lockersSectionDetails"; - -const A1_LOCKERS = new Map( - LOCKERS_SECTION_DETAILS["A-1"].lockers.map((locker) => [ - locker.lockerNumber, - locker, - ]), -); - -interface LockersA1LockerMapProps { - selectedLockerNumber: number | null; - onSelect: (lockerNumber: number) => void; -} - -// Figma: Zone Area (nodeId 2159:110831 외) — 칸 묶음을 감싸는 테두리 -function ZoneArea({ - group, - selectedLockerNumber, - onSelect, -}: { group: LockersLockerGroup } & LockersA1LockerMapProps) { - return ( -
- row.map((locker) => locker.number))} - selectedLockerNumber={selectedLockerNumber} - /> -
- ); -} - -// Figma: A-1구역 Locker Grid Container (nodeId 2159:110821) — 창문을 사이에 두고 벽면 세 곳에 -// 칸 묶음이 붙어 있다. 실제 공간 배치라 데이터가 아니라 이 레이아웃이 갖는다(구역 평면도와 같은 방침). -// 폭이 화면보다 넓어서(약 620px) 부모가 좌우 스크롤로 보여준다. -// -// Figma의 Shelf Label은 높이가 1px로 깨져 있어서, 미니맵(2159:110761)처럼 칸 묶음 줄(122px) -// 높이에 맞춘다. -function LockersA1LockerMap({ - selectedLockerNumber, - onSelect, -}: LockersA1LockerMapProps) { - const zoneProps = { onSelect, selectedLockerNumber }; - - return ( -
-
- -
- -
- - - - - -
-
-
- {/* Figma에서 복도 라벨은 560px 폭 가운데 정렬이라 칸 묶음 줄 아래 가운데에 온다 */} -
- - 복도 - -
-
- ); -} - -export default LockersA1LockerMap; diff --git a/src/features/lockers/components/LockersA2LockerMap.tsx b/src/features/lockers/components/LockersA2LockerMap.tsx deleted file mode 100644 index b6fb8fd..0000000 --- a/src/features/lockers/components/LockersA2LockerMap.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Typography } from "@wanteddev/wds"; - -import LockersLockerGrid from "@/features/lockers/components/LockersLockerGrid"; -import LockersMapArea from "@/features/lockers/components/LockersMapArea"; -import LockersShelfLabel from "@/features/lockers/components/LockersShelfLabel"; -import { LOCKERS_A2_GROUP } from "@/features/lockers/constants/lockersLockers"; -import { LOCKERS_SECTION_DETAILS } from "@/features/lockers/constants/lockersSectionDetails"; - -const A2_LOCKERS = new Map( - LOCKERS_SECTION_DETAILS["A-2"].lockers.map((locker) => [ - locker.lockerNumber, - locker, - ]), -); - -interface LockersA2LockerMapProps { - selectedLockerNumber: number | null; - onSelect: (lockerNumber: number) => void; -} - -// Figma: A-2구역 Locker Grid Section (nodeId 2159:109622) — 칸 묶음 옆이 B-1구역, 복도 건너편이 -// 231호실이다. 실제 공간 배치라 레이아웃이 갖는다. 화면 폭(335px) 안에 들어가서 스크롤이 생기지 않는다. -function LockersA2LockerMap({ - selectedLockerNumber, - onSelect, -}: LockersA2LockerMapProps) { - return ( -
- -
-
- - row.map((locker) => locker.number), - )} - onSelect={onSelect} - selectedLockerNumber={selectedLockerNumber} - /> - -
- - 복도 - - -
-
- ); -} - -export default LockersA2LockerMap; diff --git a/src/features/lockers/components/LockersSelectedLockerBar.tsx b/src/features/lockers/components/LockersSelectedLockerBar.tsx index bdc7a2f..b6e4a78 100644 --- a/src/features/lockers/components/LockersSelectedLockerBar.tsx +++ b/src/features/lockers/components/LockersSelectedLockerBar.tsx @@ -1,8 +1,8 @@ import { ActionArea, ActionAreaButton, Typography } from "@wanteddev/wds"; interface LockersSelectedLockerBarProps { - sectionName: string; - selectedLockerNumber: number | null; + /** 고른 사물함의 이름(서버 lockerLabel, 예: "A-25"). 고르기 전이면 null */ + lockerLabel: string | null; onSubmit: () => void; } @@ -11,17 +11,12 @@ interface LockersSelectedLockerBarProps { // 그림자는 Figma Shadow/Spread/Small 값 그대로다. // // 선택 전(State=Empty)은 안내 문구 + 잠긴 버튼, 선택 후(Figma "사물함 선택 시" 2159:113198)는 -// 사물함 번호를 Primary/Normal SemiBold로 보여주고 버튼을 연다. -// -// 사물함 번호는 Figma가 B-1구역 25번을 "B-25"로 적는다 — 구역 번호 없이 동 이름(구역 이름의 -// 앞글자)과 칸 번호만 쓴다. 칸 번호가 같은 동 안에서 겹치지 않아서(A-1은 1~36, A-2는 82~90) 이걸로 충분하다. +// 사물함 이름을 Primary/Normal SemiBold로 보여주고 버튼을 연다. 이름은 서버가 주는 값을 그대로 쓴다. function LockersSelectedLockerBar({ - sectionName, - selectedLockerNumber, + lockerLabel, onSubmit, }: LockersSelectedLockerBarProps) { - const hasSelection = selectedLockerNumber !== null; - const lockerLabel = `${sectionName.split("-")[0]}-${selectedLockerNumber}`; + const hasSelection = lockerLabel !== null; return (
diff --git a/src/features/lockers/constants/lockersLockers.ts b/src/features/lockers/constants/lockersLockers.ts index 8615bbd..ef32812 100644 --- a/src/features/lockers/constants/lockersLockers.ts +++ b/src/features/lockers/constants/lockersLockers.ts @@ -1,45 +1,3 @@ -export interface LockersLocker { - number: number; - isAvailable: boolean; -} - -// 칸 묶음 하나(Figma "Zone Area" 안의 "Locker Grid"). 행 단위로 적는다 — 번호가 행마다 -// 이어지지 않는 묶음이 있어서(A-1 가운데 묶음은 25~30 / 22~24·31~33 / 19~21·34~36) 규칙으로 만들지 않는다. -export type LockersLockerGroup = LockersLocker[][]; - -function toRows(rows: number[][]): LockersLockerGroup { - return rows.map((row) => - row.map((number) => ({ isAvailable: true, number })), - ); -} - -// Figma: A-1구역 Locker Grid Area (nodeId 2159:110825) — 왼쪽 벽면 · 창문 · 오른쪽 벽면 순서. -// Figma 목업은 전부 선택 가능이라 목데이터도 그렇게 둔다. API가 붙으면 isAvailable만 채우면 된다. -export const LOCKERS_A1_GROUPS = { - left: toRows([ - [10, 11, 12], - [13, 14, 15], - [16, 17, 18], - ]), - middle: toRows([ - [25, 26, 27, 28, 29, 30], - [22, 23, 24, 31, 32, 33], - [19, 20, 21, 34, 35, 36], - ]), - right: toRows([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - ]), -} as const satisfies Record; - -// Figma: A-2구역 Locker Grid (nodeId 2159:109626) -export const LOCKERS_A2_GROUP: LockersLockerGroup = toRows([ - [82, 83, 84], - [85, 86, 87], - [88, 89, 90], -]); - // 사진 속 칸 번호 핀 색. 위쪽 칸일수록 진하다(Figma Orange/50 → 70 → 90). export type LockersPhotoPinTone = "upper" | "middle" | "lower"; From b04692594bcbc64853b17788295545367200fa2f Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 00:05:01 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20=EC=82=AC=EC=A7=84=20=ED=95=80=EC=9D=84=20?= =?UTF-8?q?=EB=B9=84=EC=9C=A8=20=EC=A2=8C=ED=91=9C=EB=A1=9C=20=EB=86=93?= =?UTF-8?q?=EA=B3=A0=20=ED=96=89=20=EC=9C=84=EC=B9=98=EB=A1=9C=20=EC=83=89?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/LockersLockerSelectScreen.tsx | 44 +++----- .../components/LockersSectionPhotoModal.tsx | 103 +++++++++++++----- .../lockers/constants/lockersLockers.ts | 49 --------- src/index.css | 5 +- 4 files changed, 93 insertions(+), 108 deletions(-) delete mode 100644 src/features/lockers/constants/lockersLockers.ts diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index 67225a6..988e726 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -4,7 +4,6 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import ComingSoonScreen from "@/app/ComingSoonScreen"; -import sectionA1Photo from "@/assets/images/lockers/section-a-1-photo.jpg"; import ScreenHeader from "@/components/ui/ScreenHeader"; import { useScreenHeader } from "@/components/ui/useScreenHeader"; import LockersLayoutRenderer from "@/features/lockers/components/LockersLayoutRenderer"; @@ -14,21 +13,11 @@ import LockersLockerMinimap, { } from "@/features/lockers/components/LockersLockerMinimap"; import LockersSectionPhotoModal from "@/features/lockers/components/LockersSectionPhotoModal"; import LockersSelectedLockerBar from "@/features/lockers/components/LockersSelectedLockerBar"; -import { - LOCKERS_A1_PHOTO_PINS, - type LockersPhotoPin, -} from "@/features/lockers/constants/lockersLockers"; import { LOCKERS_SECTION_DETAILS, type LockersSectionDetail, } from "@/features/lockers/constants/lockersSectionDetails"; -// 실제 사진 모달은 아직 예전 핀 형식(px 좌표)을 쓴다 — 구역 상세의 photo(비율 좌표)로 바꿀 때 지운다 -const LEGACY_PHOTOS: Record = - { - "A-1": { pins: LOCKERS_A1_PHOTO_PINS, src: sectionA1Photo }, - }; - // Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) // 구역마다 다른 칸 배치는 구역 상세의 layout이 갖고, 이 화면은 그걸 렌더러로 그리기만 한다. function LockersLockerSelectScreen() { @@ -54,7 +43,6 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { null, ); const scrollRef = useRef(null); - const photo = LEGACY_PHOTOS[detail.section]; const lockers = useMemo( () => @@ -113,15 +101,13 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { title={`${detail.section}구역 사물함 선택`} trailing={ <> - {photo && ( - setIsPhotoOpen(true)} - variant="icon" - > - - - )} + setIsPhotoOpen(true)} + variant="icon" + > + + - {photo && ( - setIsPhotoOpen(false)} - open={isPhotoOpen} - photo={photo.src} - pins={photo.pins} - sectionName={detail.section} - /> - )} + setIsPhotoOpen(false)} + open={isPhotoOpen} + photo={detail.photo} + sectionName={detail.section} + />
); } diff --git a/src/features/lockers/components/LockersSectionPhotoModal.tsx b/src/features/lockers/components/LockersSectionPhotoModal.tsx index 3f9c226..f24d9d8 100644 --- a/src/features/lockers/components/LockersSectionPhotoModal.tsx +++ b/src/features/lockers/components/LockersSectionPhotoModal.tsx @@ -1,28 +1,67 @@ import { IconButton, Typography } from "@wanteddev/wds"; import { IconClose } from "@wanteddev/wds-icon"; import type { KeyboardEvent } from "react"; -import { useEffect, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { createPortal } from "react-dom"; import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; import type { - LockersPhotoPin, - LockersPhotoPinTone, -} from "@/features/lockers/constants/lockersLockers"; + LockersLayout, + LockersLayoutBlock, + LockersSectionPhoto, +} from "@/features/lockers/constants/lockersSectionDetails"; interface LockersSectionPhotoModalProps { open: boolean; sectionName: string; - photo: string; - pins: LockersPhotoPin[]; + photo: LockersSectionPhoto; + /** 핀 색을 칸이 묶음의 몇 번째 행인지로 정해서, 같은 구역의 layout을 받는다 */ + layout: LockersLayout; onClose: () => void; } -const PIN_CLASS_NAMES: Record = { - lower: "bg-orange-90", - middle: "bg-orange-70", - upper: "bg-orange-50", -}; +// 위쪽 행부터 진한 순서. 3행 묶음은 50·70·90, 5행 묶음은 50·60·70·80·90을 쓴다(Figma A-1·B-2 실제사진). +const PIN_TONE_CLASS_NAMES = [ + "bg-orange-50", + "bg-orange-60", + "bg-orange-70", + "bg-orange-80", + "bg-orange-90", +]; + +function getPinToneClassName(rowIndex: number, rowCount: number) { + if (rowCount <= 1) { + return PIN_TONE_CLASS_NAMES[0]; + } + const step = Math.round( + (rowIndex / (rowCount - 1)) * (PIN_TONE_CLASS_NAMES.length - 1), + ); + return PIN_TONE_CLASS_NAMES[step]; +} + +// layout의 모든 칸 묶음을 돌며 칸 번호마다 핀 색을 정해 둔다 +function collectPinToneClassNames( + block: LockersLayoutBlock, + toneClassNames: Map, +) { + if (block.type === "row" || block.type === "column") { + for (const child of block.children) { + collectPinToneClassNames(child, toneClassNames); + } + } else if (block.type === "lockerGroup") { + block.rows.forEach((row, rowIndex) => { + for (const lockerNumber of row) { + if (lockerNumber !== null) { + toneClassNames.set( + lockerNumber, + getPinToneClassName(rowIndex, block.rows.length), + ); + } + } + }); + } + return toneClassNames; +} // Figma: A-1구역 실제사진 Modal (nodeId 2159:110299) — Stream 로컬. 닫기 아이콘 + 구역 사진에 // 칸 번호 핀을 얹고 아래에 안내 문구를 단 카드다. WDS `Modal`은 너비·모서리가 달라서 ConfirmModal과 같은 방식으로 @@ -33,12 +72,16 @@ function LockersSectionPhotoModal({ open, sectionName, photo, - pins, + layout, onClose, }: LockersSectionPhotoModalProps) { const portalEl = useScreenSheetPortal(); const dialogRef = useRef(null); const lastFocusedRef = useRef(null); + const pinToneClassNames = useMemo( + () => collectPinToneClassNames(layout.root, new Map()), + [layout], + ); // 열리면 포커스를 모달로 옮기고, 닫히면 사진 버튼으로 되돌린다 useEffect(() => { @@ -95,25 +138,29 @@ function LockersSectionPhotoModal({ - {/* Figma는 사진을 1.12° 돌리고 살짝 키워서(320×240) 300×224 틀에 맞춰 자른다 */} + {/* Figma 사진 틀 300×224. 서버가 잘라 둔 사진을 받아서 틀에 그대로 채운다 + (Figma는 사진을 1.12° 돌리고 키워서 맞췄는데, 그 보정은 사진을 자를 때 끝낸다) */}
-
- {`${sectionName}구역 -
- {pins.map((pin) => ( + {`${sectionName}구역 + {photo.pins.map((pin) => ( - {/* 사진 위 주석이라 사진 크기에 맞춘 값이다 — Figma에서도 이름 있는 타입 스타일이 - 아니라(9.71px) Typography로 옮길 수 없다. 사진이 테마와 무관해 글자도 검정 고정이다. */} - - {pin.number} + {/* 사진 위 주석이라 핀 크기에 맞춘 값이다 — Figma에서도 이름 있는 타입 스타일이 + 아니라(7.69~9.71px) Typography로 옮길 수 없다. 사진이 테마와 무관해 글자도 검정 고정이다. + 핀 크기는 Figma 두 값(12.4px, 14.9px)의 중간인 14px로 고정했다. */} + + {pin.lockerNumber} ))} diff --git a/src/features/lockers/constants/lockersLockers.ts b/src/features/lockers/constants/lockersLockers.ts deleted file mode 100644 index ef32812..0000000 --- a/src/features/lockers/constants/lockersLockers.ts +++ /dev/null @@ -1,49 +0,0 @@ -// 사진 속 칸 번호 핀 색. 위쪽 칸일수록 진하다(Figma Orange/50 → 70 → 90). -export type LockersPhotoPinTone = "upper" | "middle" | "lower"; - -export interface LockersPhotoPin { - number: number; - tone: LockersPhotoPinTone; - /** 사진 프레임(300×224) 기준 좌표(px) */ - left: number; - top: number; -} - -// Figma 가운데 묶음 핀은 "Photo Annotation"(left 94.14 · top 89.83) 안에서 flex로 놓여 있다. -// 핀 12.414px + 가로 간격 7px · 세로 간격 12px을 풀어서 좌표로 적는다. -const MIDDLE_PIN_LEFTS = [94.14, 113.55, 132.97, 152.38, 171.8, 191.21]; -const MIDDLE_PIN_ROWS: [LockersPhotoPinTone, number, number[]][] = [ - ["upper", 89.83, [25, 26, 27, 28, 29, 30]], - ["middle", 114.24, [22, 23, 24, 31, 32, 33]], - ["lower", 138.66, [19, 20, 21, 34, 35, 36]], -]; - -// Figma: A-1구역 실제사진 Modal (nodeId 2159:110299) 안의 핀 좌표 -export const LOCKERS_A1_PHOTO_PINS: LockersPhotoPin[] = [ - { left: 9.5, number: 10, tone: "upper", top: 94.14 }, - { left: 39.5, number: 11, tone: "upper", top: 88.45 }, - { left: 61.5, number: 12, tone: "upper", top: 84.31 }, - { left: 9.5, number: 13, tone: "middle", top: 141.72 }, - { left: 39.5, number: 14, tone: "middle", top: 129.31 }, - { left: 61.5, number: 15, tone: "middle", top: 118.97 }, - { left: 9.5, number: 16, tone: "lower", top: 192.93 }, - { left: 39.5, number: 17, tone: "lower", top: 169.65 }, - { left: 61.5, number: 18, tone: "lower", top: 152.07 }, - ...MIDDLE_PIN_ROWS.flatMap(([tone, top, numbers]) => - numbers.map((number, index) => ({ - left: MIDDLE_PIN_LEFTS[index], - number, - tone, - top, - })), - ), - { left: 226.5, number: 1, tone: "upper", top: 83.28 }, - { left: 251.5, number: 2, tone: "upper", top: 85.86 }, - { left: 283.5, number: 3, tone: "upper", top: 89.48 }, - { left: 226.5, number: 4, tone: "middle", top: 118.97 }, - { left: 251.5, number: 5, tone: "middle", top: 131.9 }, - { left: 283.5, number: 6, tone: "middle", top: 149.48 }, - { left: 226.5, number: 7, tone: "lower", top: 149.48 }, - { left: 251.5, number: 8, tone: "lower", top: 171.21 }, - { left: 283.5, number: 9, tone: "lower", top: 193.97 }, -]; diff --git a/src/index.css b/src/index.css index 4b122fa..d2a2164 100644 --- a/src/index.css +++ b/src/index.css @@ -50,9 +50,12 @@ /* 사물함 칸 선택 — 선택 가능한 칸 배경(Figma "Orange/95")과 칸 묶음 테두리(Figma "Line/Solid/Alternative") */ --color-orange-95: var(--atomic-orange-95); --color-line-solid-alternative: var(--semantic-line-solid-alternative); - /* 구역 실제 사진 위 칸 번호 핀 — 위쪽 칸부터 Figma "Orange/50" → "Orange/70" → "Orange/90" */ + /* 구역 실제 사진 위 칸 번호 핀 — 위쪽 칸일수록 진하다. 3행 묶음은 Figma "Orange/50" → "70" → "90", + 5행 묶음(B-2구역 실제사진 2159:110653)은 50 → 60 → 70 → 80 → 90 */ --color-orange-50: var(--atomic-orange-50); + --color-orange-60: var(--atomic-orange-60); --color-orange-70: var(--atomic-orange-70); + --color-orange-80: var(--atomic-orange-80); --color-orange-90: var(--atomic-orange-90); --color-status-positive: var(--semantic-status-positive); From 0022b333b1c3729d255d02759a6094eda3e1e6df Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 10:21:47 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EB=B0=B0=EC=B9=98=EB=A5=BC=20=ED=95=80=EC=B9=98?= =?UTF-8?q?=C2=B7Ctrl+=ED=9C=A0=EB=A1=9C=20=ED=99=95=EB=8C=80=C2=B7?= =?UTF-8?q?=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/LockersLockerSelectScreen.tsx | 20 +- .../lockers/hooks/useLockersPinchZoom.ts | 203 ++++++++++++++++++ 2 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 src/features/lockers/hooks/useLockersPinchZoom.ts diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index 988e726..fde1984 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -17,6 +17,7 @@ import { LOCKERS_SECTION_DETAILS, type LockersSectionDetail, } from "@/features/lockers/constants/lockersSectionDetails"; +import { useLockersPinchZoom } from "@/features/lockers/hooks/useLockersPinchZoom"; // Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) // 구역마다 다른 칸 배치는 구역 상세의 layout이 갖고, 이 화면은 그걸 렌더러로 그리기만 한다. @@ -43,6 +44,7 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { null, ); const scrollRef = useRef(null); + const contentRef = useRef(null); const lockers = useMemo( () => @@ -81,6 +83,8 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { return () => observer.disconnect(); }, [updateViewport]); + const zoom = useLockersPinchZoom(scrollRef, contentRef, updateViewport); + useScreenHeader( -
+
{/* Figma Minimap Section — 미니맵이 없어도 128px 자리를 지켜서 칸 배치 위치가 구역마다 같다 */}
{viewport ? ( @@ -145,14 +149,20 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { )}
- {/* 화면 끝까지 스크롤되도록 좌우 여백을 스크롤 영역 안쪽에 둔다. - min-w-full: 화면에 다 들어오는 구역에서 layout의 "fill" 상자가 화면 폭까지 늘어나게 한다 */} + {/* touch-pan: 두 손가락 동작을 페이지 확대 대신 이 영역의 확대로 받는다. + min-w-full: 화면보다 좁은 구역에서 "fill" 상자가 화면 폭까지 늘어나게 한다 */}
-
{map}
+
+ {map} +
diff --git a/src/features/lockers/hooks/useLockersPinchZoom.ts b/src/features/lockers/hooks/useLockersPinchZoom.ts new file mode 100644 index 0000000..88d6c43 --- /dev/null +++ b/src/features/lockers/hooks/useLockersPinchZoom.ts @@ -0,0 +1,203 @@ +import { + type RefObject, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; + +const MAX_ZOOM = 3; +// 핀치를 끝낸 손가락이 칸을 누른 것으로 처리되지 않게 막는 시간 +const CLICK_SUPPRESS_MS = 300; +const WHEEL_COMMIT_DELAY_MS = 150; + +interface Point { + x: number; + y: number; +} + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +function getTouchDistance(touches: TouchList) { + return Math.hypot( + touches[0].clientX - touches[1].clientX, + touches[0].clientY - touches[1].clientY, + ); +} + +function getTouchMidpoint(touches: TouchList): Point { + return { + x: (touches[0].clientX + touches[1].clientX) / 2, + y: (touches[0].clientY + touches[1].clientY) / 2, + }; +} + +// 칸 배치를 핀치·Ctrl+휠로 확대·축소한다(최소: 배치 전체가 화면 폭에 들어오는 배율, 최대 3배). +// transform: scale은 스크롤 영역 크기에 반영되지 않아서 CSS zoom을 쓴다. 제스처 중에는 리렌더 없이 +// DOM에 바로 적용하고 끝나면 상태로 확정한다. +export function useLockersPinchZoom( + scrollRef: RefObject, + contentRef: RefObject, + onZoomChange?: () => void, +) { + const [zoom, setZoom] = useState(1); + const zoomRef = useRef(1); + const minZoomRef = useRef(1); + const onZoomChangeRef = useRef(onZoomChange); + + useLayoutEffect(() => { + onZoomChangeRef.current = onZoomChange; + }, [onZoomChange]); + + useEffect(() => { + const scroller = scrollRef.current; + const content = contentRef.current; + if (!scroller || !content) { + return; + } + + const toContentPoint = (anchor: Point): Point => ({ + x: (scroller.scrollLeft + anchor.x) / zoomRef.current, + y: (scroller.scrollTop + anchor.y) / zoomRef.current, + }); + const toAnchor = (clientPoint: Point): Point => { + const rect = scroller.getBoundingClientRect(); + return { x: clientPoint.x - rect.left, y: clientPoint.y - rect.top }; + }; + + const zoomAround = (nextZoom: number, anchor: Point, point: Point) => { + let clamped = clamp(nextZoom, minZoomRef.current, MAX_ZOOM); + content.style.zoom = String(clamped); + // zoom은 길이마다 반올림돼서 최소 배율에서도 1~2px 넘칠 수 있다 + if ( + clamped === minZoomRef.current && + scroller.scrollWidth > scroller.clientWidth + ) { + minZoomRef.current *= scroller.clientWidth / scroller.scrollWidth; + clamped = minZoomRef.current; + content.style.zoom = String(clamped); + } + zoomRef.current = clamped; + scroller.scrollLeft = point.x * clamped - anchor.x; + scroller.scrollTop = point.y * clamped - anchor.y; + onZoomChangeRef.current?.(); + }; + + const updateMinZoom = () => { + const naturalWidth = + content.getBoundingClientRect().width / zoomRef.current; + minZoomRef.current = Math.min(1, scroller.clientWidth / naturalWidth); + if (zoomRef.current < minZoomRef.current) { + zoomAround( + minZoomRef.current, + { x: 0, y: 0 }, + toContentPoint({ x: 0, y: 0 }), + ); + setZoom(zoomRef.current); + } + }; + + let pinch: { distance: number; zoom: number; point: Point } | null = null; + let suppressClickUntil = 0; + let wheelCommitTimer: number | undefined; + + const handleTouchStart = (event: TouchEvent) => { + if (event.touches.length !== 2) { + return; + } + pinch = { + distance: getTouchDistance(event.touches), + point: toContentPoint(toAnchor(getTouchMidpoint(event.touches))), + zoom: zoomRef.current, + }; + suppressClickUntil = Number.POSITIVE_INFINITY; + }; + + const handleTouchMove = (event: TouchEvent) => { + if (!pinch || event.touches.length !== 2) { + return; + } + event.preventDefault(); + zoomAround( + (pinch.zoom * getTouchDistance(event.touches)) / pinch.distance, + toAnchor(getTouchMidpoint(event.touches)), + pinch.point, + ); + }; + + const handleTouchEnd = (event: TouchEvent) => { + if (!pinch || event.touches.length >= 2) { + return; + } + pinch = null; + suppressClickUntil = performance.now() + CLICK_SUPPRESS_MS; + setZoom(zoomRef.current); + }; + + const handleClickCapture = (event: MouseEvent) => { + if (performance.now() < suppressClickUntil) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + const handleWheel = (event: WheelEvent) => { + // 트랙패드 핀치는 Ctrl+휠로 들어온다 + if (!event.ctrlKey) { + return; + } + event.preventDefault(); + const anchor = toAnchor({ x: event.clientX, y: event.clientY }); + zoomAround( + zoomRef.current * Math.exp(-event.deltaY * 0.01), + anchor, + toContentPoint(anchor), + ); + window.clearTimeout(wheelCommitTimer); + wheelCommitTimer = window.setTimeout( + () => setZoom(zoomRef.current), + WHEEL_COMMIT_DELAY_MS, + ); + }; + + updateMinZoom(); + const resizeObserver = new ResizeObserver(updateMinZoom); + resizeObserver.observe(scroller); + + scroller.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + // 페이지 확대를 preventDefault로 막아야 해서 passive가 아니다 + scroller.addEventListener("touchmove", handleTouchMove, { passive: false }); + scroller.addEventListener("touchend", handleTouchEnd); + scroller.addEventListener("touchcancel", handleTouchEnd); + scroller.addEventListener("click", handleClickCapture, { capture: true }); + scroller.addEventListener("wheel", handleWheel, { passive: false }); + + return () => { + resizeObserver.disconnect(); + window.clearTimeout(wheelCommitTimer); + scroller.removeEventListener("touchstart", handleTouchStart); + scroller.removeEventListener("touchmove", handleTouchMove); + scroller.removeEventListener("touchend", handleTouchEnd); + scroller.removeEventListener("touchcancel", handleTouchEnd); + scroller.removeEventListener("click", handleClickCapture, { + capture: true, + }); + scroller.removeEventListener("wheel", handleWheel); + }; + }, [scrollRef, contentRef]); + + // 처음에는 가운데부터 보여준다 + useLayoutEffect(() => { + const scroller = scrollRef.current; + if (!scroller) { + return; + } + scroller.scrollLeft = (scroller.scrollWidth - scroller.clientWidth) / 2; + }, [scrollRef]); + + return zoom; +} From 157ebe285711f01948d9abc2d4dd2f456a17e9d7 Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 10:58:13 +0900 Subject: [PATCH 16/19] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=B9=B8=20=EB=B0=B0=EC=B9=98=20=EC=BD=94=EB=93=9C=EC=9D=98?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/LockersLockerSelectScreen.tsx | 1 - .../components/LockersLayoutRenderer.tsx | 15 +++------ .../lockers/components/LockersLockerCell.tsx | 3 +- .../lockers/components/LockersLockerGrid.tsx | 6 ++-- .../lockers/components/LockersMapArea.tsx | 5 +-- .../lockers/components/LockersMapLabel.tsx | 7 ++-- .../components/LockersSectionPhotoModal.tsx | 14 +++----- .../components/LockersSelectedLockerBar.tsx | 4 +-- .../lockers/components/LockersShelfLabel.tsx | 5 +-- .../constants/lockersSectionDetails.ts | 33 +++++++------------ 10 files changed, 30 insertions(+), 63 deletions(-) diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index fde1984..a37e4fe 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -20,7 +20,6 @@ import { import { useLockersPinchZoom } from "@/features/lockers/hooks/useLockersPinchZoom"; // Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) -// 구역마다 다른 칸 배치는 구역 상세의 layout이 갖고, 이 화면은 그걸 렌더러로 그리기만 한다. function LockersLockerSelectScreen() { const { sectionId = "" } = useParams(); const detail = LOCKERS_SECTION_DETAILS[sectionId]; diff --git a/src/features/lockers/components/LockersLayoutRenderer.tsx b/src/features/lockers/components/LockersLayoutRenderer.tsx index ffbad54..57d5f00 100644 --- a/src/features/lockers/components/LockersLayoutRenderer.tsx +++ b/src/features/lockers/components/LockersLayoutRenderer.tsx @@ -17,7 +17,6 @@ import type { interface LockersLayoutRendererProps { layout: LockersLayout; - /** 구역의 사물함을 lockerNumber로 찾는 표 */ lockers: ReadonlyMap; selectedLockerNumber: number | null; onSelect: (lockerNumber: number) => void; @@ -25,8 +24,7 @@ interface LockersLayoutRendererProps { type LockersLayoutBlockContext = Omit; -// 블록이 놓이는 부모의 방향. 같은 "fill"이라도 부모 방향과 같은 축이면 남은 공간을 나눠 갖고(flex-1), -// 다른 축이면 부모 폭·높이에 맞춰 늘어난다(self-stretch). +// "fill"은 부모와 같은 축이면 남은 공간을 나눠 갖고(flex-1), 다른 축이면 부모에 맞춰 늘어난다 type LockersLayoutDirection = "row" | "column"; const ALIGN_CLASS_NAMES: Record = { @@ -111,7 +109,6 @@ function LockersLayoutBlockView({ selectedLockerNumber={context.selectedLockerNumber} /> ); - // Figma: Zone Area (nodeId 2159:110831 외) — 칸 묶음을 감싸는 테두리 return block.bordered ? (
); default: - // 서버가 이 버전의 프론트가 모르는 블록을 보내도 화면 전체가 깨지지 않게 그 블록만 건너뛴다 + // 모르는 블록은 화면 전체가 깨지지 않게 건너뛴다 return null; } } -// layout JSON(블록 7종)을 그대로 그린다. 구역마다 배치 컴포넌트를 따로 두지 않고, 구역별 배치는 -// 서버가 주는 layout이 갖는다. 칸 크기·색·간격 같은 디자인 규칙은 각 부품이 갖는다. -// -// 최상위 블록은 부모(가로 스크롤 영역) 폭보다 좁아지지 않는다 — 화면에 다 들어오는 구역에서 -// "fill" 상자(A-2의 B-1구역 등)가 화면 끝까지 늘어나게 하려는 것이다. +// 최상위 블록은 부모 폭보다 좁아지지 않게 해서, 화면보다 좁은 구역의 "fill" 상자가 화면 끝까지 늘어난다 function LockersLayoutRenderer({ layout, ...context diff --git a/src/features/lockers/components/LockersLockerCell.tsx b/src/features/lockers/components/LockersLockerCell.tsx index f59ebbe..a338328 100644 --- a/src/features/lockers/components/LockersLockerCell.tsx +++ b/src/features/lockers/components/LockersLockerCell.tsx @@ -19,8 +19,7 @@ interface LockersLockerCellProps { // 선택된 칸은 Figma "사물함 선택 시"(2159:113315)대로 Primary/Normal 배경에 번호를 // Background/Normal/Normal(흰색) SemiBold로 바꾼다. // -// 내 사물함(이미 신청한 칸)은 Figma에 상태가 없어서 임시로 초록 테두리(Status/Positive)를 두른다. -// 이미 신청한 칸이라 다시 고를 수 없다 — 디자인이 나오면 바꾼다. +// 내 사물함은 Figma에 상태가 없어서 임시로 초록 테두리를 둔다(이미 신청한 칸이라 고를 수 없다). function LockersLockerCell({ locker, isSelected, diff --git a/src/features/lockers/components/LockersLockerGrid.tsx b/src/features/lockers/components/LockersLockerGrid.tsx index f5192ea..945379f 100644 --- a/src/features/lockers/components/LockersLockerGrid.tsx +++ b/src/features/lockers/components/LockersLockerGrid.tsx @@ -2,16 +2,14 @@ import LockersLockerCell from "@/features/lockers/components/LockersLockerCell"; import type { LockersSectionLocker } from "@/features/lockers/constants/lockersSectionDetails"; interface LockersLockerGridProps { - /** layout의 lockerGroup.rows — 숫자는 lockerNumber, null은 칸이 없는 빈 자리 */ rows: (number | null)[][]; - /** 구역의 사물함을 lockerNumber로 찾는 표 */ lockers: ReadonlyMap; selectedLockerNumber: number | null; onSelect: (lockerNumber: number) => void; } -// Figma: Locker Grid (nodeId 2159:110832 외) — 칸 사이 6px. 열 수는 가장 긴 행을 따른다. -// layout에 있는데 lockers에 없는 번호는 서버 데이터가 어긋난 경우라, 칸 대신 빈 자리로 둔다. +// Figma: Locker Grid (nodeId 2159:110832 외) +// layout에는 있는데 lockers에 없는 번호(서버 데이터가 어긋난 경우)는 빈 자리로 둔다. function LockersLockerGrid({ rows, lockers, diff --git a/src/features/lockers/components/LockersMapArea.tsx b/src/features/lockers/components/LockersMapArea.tsx index 13ec24f..b3f9e3f 100644 --- a/src/features/lockers/components/LockersMapArea.tsx +++ b/src/features/lockers/components/LockersMapArea.tsx @@ -4,14 +4,11 @@ import type { CSSProperties } from "react"; interface LockersMapAreaProps { label: string; icon?: string; - /** 크기·배치(flex 비율, 폭, 세로 여백 등) — 화면마다 다르다 */ className?: string; - /** px로 정한 크기(layout의 숫자 width·height) */ style?: CSSProperties; } -// Figma: 호실·화장실·계단·옆 구역처럼 고를 수 없는 자리(Room Label·Zone Label·Stairs Area) — Stream 로컬. -// 점선 테두리 흰 상자 가운데에 라벨(+ 아이콘)을 둔다. 구역 평면도와 구역별 칸 배치가 같이 쓴다. +// Figma: Room Label·Zone Label·Stairs Area — 호실·계단처럼 고를 수 없는 자리 function LockersMapArea({ label, icon, diff --git a/src/features/lockers/components/LockersMapLabel.tsx b/src/features/lockers/components/LockersMapLabel.tsx index cae8e54..2739daf 100644 --- a/src/features/lockers/components/LockersMapLabel.tsx +++ b/src/features/lockers/components/LockersMapLabel.tsx @@ -4,19 +4,16 @@ import type { CSSProperties } from "react"; interface LockersMapLabelProps { text: string; orientation?: "horizontal" | "vertical"; - /** 크기(창문 폭, 벽면 높이 등) — 화면마다 다르다 */ className?: string; - /** px로 정한 크기(layout의 숫자 width·height) */ style?: CSSProperties; } -// 세로 라벨은 Figma가 한 글자씩 줄을 바꾸고 단어 사이에 빈 줄을 둔다("왼쪽 벽면" → 왼/쪽/ /벽/면). +// Figma는 세로 라벨을 한 글자씩 줄바꿈하고 단어 사이에 빈 줄을 둔다 function toVerticalText(text: string) { return [...text].map((char) => (char === " " ? "" : char)).join("\n"); } -// Figma: 창문·벽면처럼 고를 수 없는 자리(Direction Label·Aisle) — Stream 로컬. 회색 면에 라벨을 둔다. -// 가로(창문)는 위아래, 세로(벽면)는 좌우에만 여백을 둔다. +// Figma: Direction Label·Aisle — 창문·벽면 표시 function LockersMapLabel({ text, orientation = "horizontal", diff --git a/src/features/lockers/components/LockersSectionPhotoModal.tsx b/src/features/lockers/components/LockersSectionPhotoModal.tsx index f24d9d8..ad4edd5 100644 --- a/src/features/lockers/components/LockersSectionPhotoModal.tsx +++ b/src/features/lockers/components/LockersSectionPhotoModal.tsx @@ -15,12 +15,12 @@ interface LockersSectionPhotoModalProps { open: boolean; sectionName: string; photo: LockersSectionPhoto; - /** 핀 색을 칸이 묶음의 몇 번째 행인지로 정해서, 같은 구역의 layout을 받는다 */ + /** 핀 색을 칸의 행 위치로 정하는 데 쓴다 */ layout: LockersLayout; onClose: () => void; } -// 위쪽 행부터 진한 순서. 3행 묶음은 50·70·90, 5행 묶음은 50·60·70·80·90을 쓴다(Figma A-1·B-2 실제사진). +// 위쪽 행일수록 진하다 — 3행은 50·70·90, 5행은 50·60·70·80·90(Figma A-1·B-2 실제사진) const PIN_TONE_CLASS_NAMES = [ "bg-orange-50", "bg-orange-60", @@ -39,7 +39,6 @@ function getPinToneClassName(rowIndex: number, rowCount: number) { return PIN_TONE_CLASS_NAMES[step]; } -// layout의 모든 칸 묶음을 돌며 칸 번호마다 핀 색을 정해 둔다 function collectPinToneClassNames( block: LockersLayoutBlock, toneClassNames: Map, @@ -138,8 +137,7 @@ function LockersSectionPhotoModal({ - {/* Figma 사진 틀 300×224. 서버가 잘라 둔 사진을 받아서 틀에 그대로 채운다 - (Figma는 사진을 1.12° 돌리고 키워서 맞췄는데, 그 보정은 사진을 자를 때 끝낸다) */} + {/* Figma 사진 틀 300×224. Figma의 회전·확대 보정은 사진을 자를 때 끝낸다 */}
{`${sectionName}구역 - {/* 사진 위 주석이라 핀 크기에 맞춘 값이다 — Figma에서도 이름 있는 타입 스타일이 - 아니라(7.69~9.71px) Typography로 옮길 수 없다. 사진이 테마와 무관해 글자도 검정 고정이다. - 핀 크기는 Figma 두 값(12.4px, 14.9px)의 중간인 14px로 고정했다. */} + {/* Figma에서도 이름 있는 타입 스타일이 아니라 Typography로 옮길 수 없다. + 사진이 테마와 무관해 글자도 검정 고정이다. 핀 크기는 Figma 두 값(12.4·14.9px)의 중간 */} {pin.lockerNumber} diff --git a/src/features/lockers/components/LockersSelectedLockerBar.tsx b/src/features/lockers/components/LockersSelectedLockerBar.tsx index b6e4a78..6e42e53 100644 --- a/src/features/lockers/components/LockersSelectedLockerBar.tsx +++ b/src/features/lockers/components/LockersSelectedLockerBar.tsx @@ -1,7 +1,7 @@ import { ActionArea, ActionAreaButton, Typography } from "@wanteddev/wds"; interface LockersSelectedLockerBarProps { - /** 고른 사물함의 이름(서버 lockerLabel, 예: "A-25"). 고르기 전이면 null */ + /** 고르기 전이면 null */ lockerLabel: string | null; onSubmit: () => void; } @@ -11,7 +11,7 @@ interface LockersSelectedLockerBarProps { // 그림자는 Figma Shadow/Spread/Small 값 그대로다. // // 선택 전(State=Empty)은 안내 문구 + 잠긴 버튼, 선택 후(Figma "사물함 선택 시" 2159:113198)는 -// 사물함 이름을 Primary/Normal SemiBold로 보여주고 버튼을 연다. 이름은 서버가 주는 값을 그대로 쓴다. +// 사물함 이름을 Primary/Normal SemiBold로 보여주고 버튼을 연다. function LockersSelectedLockerBar({ lockerLabel, onSubmit, diff --git a/src/features/lockers/components/LockersShelfLabel.tsx b/src/features/lockers/components/LockersShelfLabel.tsx index 7a2a7f2..1ff65f9 100644 --- a/src/features/lockers/components/LockersShelfLabel.tsx +++ b/src/features/lockers/components/LockersShelfLabel.tsx @@ -3,10 +3,7 @@ import { Typography } from "@wanteddev/wds"; import shelfDividerIcon from "@/assets/icons/lockers/shelf-divider.svg"; interface LockersShelfLabelProps { - /** - * 칸 묶음 높이(px)에 맞춘다 — 테두리 있는 3행 묶음은 122, 칸만 있는 3행 묶음은 96. - * 주지 않으면 같은 줄의 높이에 맞춰 늘어난다. - */ + /** 칸 묶음 높이(px). 없으면 같은 줄 높이에 맞춰 늘어난다 */ height?: number; } diff --git a/src/features/lockers/constants/lockersSectionDetails.ts b/src/features/lockers/constants/lockersSectionDetails.ts index 675c3cf..d59fbcb 100644 --- a/src/features/lockers/constants/lockersSectionDetails.ts +++ b/src/features/lockers/constants/lockersSectionDetails.ts @@ -1,12 +1,11 @@ import sectionA1Photo from "@/assets/images/lockers/section-a-1-photo.jpg"; -// 구역 상세 응답(GET /v1/app/lockers/sections/{sectionId})의 형태. -// 칸 배치(layout)는 구조만 갖고, 칸 상태(선택 가능·내 사물함)는 lockers에서 온다 — 둘은 lockerNumber로 잇는다. +// 구역 상세 응답(GET /v1/app/lockers/sections/{sectionId}). layout은 구조만 갖고 칸 상태는 lockers에서 +// 온다 — 둘은 lockerNumber로 잇는다. -// "hug"는 내용 크기, "fill"은 부모의 남은 공간 채우기, 숫자는 px 고정. +// "hug"는 내용 크기, "fill"은 부모의 남은 공간, 숫자는 px export type LockersLayoutSize = "hug" | "fill" | number; -// row·column의 교차축 정렬 export type LockersLayoutAlign = "start" | "center" | "end" | "stretch"; interface LockersLayoutBlockBase { @@ -21,35 +20,30 @@ export interface LockersLayoutContainerBlock extends LockersLayoutBlockBase { align?: LockersLayoutAlign; } -// 칸 묶음(Figma Zone Area + Locker Grid). rows[0]이 맨 위 행(위쪽 칸), 숫자는 lockerNumber, -// null은 칸이 없는 빈 자리다. bordered면 테두리 상자로 감싼다(A-1·B-2). +// rows[0]이 맨 위 행(위쪽 칸), 숫자는 lockerNumber, null은 빈 자리 export interface LockersLayoutLockerGroupBlock extends LockersLayoutBlockBase { type: "lockerGroup"; rows: (number | null)[][]; bordered?: boolean; } -// 회색 면 라벨(Figma Direction Label·Aisle). 창문은 가로, 벽면은 세로로 한 글자씩 줄을 바꾼다. export interface LockersLayoutLabelBlock extends LockersLayoutBlockBase { type: "label"; text: string; orientation?: "horizontal" | "vertical"; } -// 점선 상자(Figma Zone Label·Room Label·Stairs Area) — 옆 구역, 호실, 계단 export interface LockersLayoutAreaBlock extends LockersLayoutBlockBase { type: "area"; text: string; icon?: "stairs"; } -// 테두리 없는 글자(Figma 복도) export interface LockersLayoutTextBlock extends LockersLayoutBlockBase { type: "text"; text: string; } -// 위쪽 칸 / 아래쪽 칸 표시(Figma Shelf Label) export interface LockersLayoutShelfLabelBlock extends LockersLayoutBlockBase { type: "shelfLabel"; } @@ -67,7 +61,7 @@ export interface LockersLayout { root: LockersLayoutBlock; } -// 사진 위 칸 번호 핀. x·y는 핀 가운데의 위치를 사진 폭·높이 대비 0~1 비율로 적는다. +// x·y는 핀 가운데 위치를 사진 폭·높이 대비 0~1 비율로 적는다 export interface LockersSectionPhotoPin { lockerNumber: number; x: number; @@ -82,7 +76,7 @@ export interface LockersSectionPhoto { export interface LockersSectionLocker { lockerId: number; lockerNumber: number; - // 화면에 그대로 보여주는 이름("A-25"). 관리자가 입력하는 값이라 번호에서 만들지 않는다. + // 관리자가 입력하는 값이라 번호에서 만들지 않는다 lockerLabel: string; isAvailable: boolean; isMine: boolean; @@ -96,8 +90,7 @@ export interface LockersSectionDetail { lockers: LockersSectionLocker[]; } -// 목데이터용 — 실제로는 서버가 lockers를 내려준다. Figma 목업이 전부 선택 가능이라 그대로 둔다. -// 이름은 서버가 관리자 입력값으로 주지만, 목데이터는 Figma 표기("A-25", 동 이름 + 번호)대로 만든다. +// Figma 목업이 전부 선택 가능이라 그대로 두고, 이름은 Figma 표기("A-25")대로 만든다 function createMockLockers( building: string, lockerNumbers: number[], @@ -115,9 +108,8 @@ function range(from: number, to: number) { return Array.from({ length: to - from + 1 }, (_, index) => from + index); } -// Figma: A-1구역 (nodeId 2159:110753) — 창문을 사이에 두고 벽면 세 곳에 테두리 묶음이 붙어 있다. -// 핀은 A-1구역 실제사진(2159:110299)의 좌표를 사진 원본 대비 비율로 옮겼다. Figma는 사진을 -// 1.12° 돌려 붙였는데 서버는 잘라 둔 사진을 줄 거라 회전은 무시했다(가장자리에서 2~3px 차이). +// Figma: A-1구역 (nodeId 2159:110753), 핀은 A-1구역 실제사진 (2159:110299) +// Figma는 사진을 1.12° 돌려 붙였는데 회전은 무시하고 비율로 옮겼다(가장자리에서 2~3px 차이). const A1_SECTION: LockersSectionDetail = { layout: { root: { @@ -237,8 +229,8 @@ const A1_SECTION: LockersSectionDetail = { sectionId: 1, }; -// Figma: A-2구역 (nodeId 2159:109533) — 칸 묶음 옆이 B-1구역, 복도 건너편이 231호실이다. -// A-2 실제 사진은 아직 없어서(직접 찍을 예정) A-1 사진을 임시로 쓰고, 번호가 맞지 않으니 핀은 비워 둔다. +// Figma: A-2구역 (nodeId 2159:109533) +// A-2 사진이 아직 없어서 A-1 사진을 임시로 쓰고, 번호가 맞지 않아 핀은 비워 둔다. const A2_SECTION: LockersSectionDetail = { layout: { root: { @@ -287,8 +279,7 @@ const A2_SECTION: LockersSectionDetail = { sectionId: 2, }; -// API 연동 전까지 쓰는 구역 상세 목데이터. 지금 주소가 구역 라벨(/lockers/apply/sections/A-1)이라 -// 라벨로 찾는다. 나머지 구역은 서버 시드가 들어오면 응답으로 대체된다. +// 주소가 구역 라벨(/lockers/apply/sections/A-1)이라 라벨로 찾는다 export const LOCKERS_SECTION_DETAILS: Record = { "A-1": A1_SECTION, "A-2": A2_SECTION, From f27387eca08a3b35e6d34dd98ed0448c7b7515ec Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 11:03:28 +0900 Subject: [PATCH 17/19] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EB=AF=B8=EB=8B=88=EB=A7=B5=EC=9D=84=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EB=B9=84=EC=9C=A8=EC=97=90=20=EB=A7=9E=EC=B6=94=EA=B3=A0=20?= =?UTF-8?q?=ED=85=8C=EB=91=90=EB=A6=AC=EA=B0=80=20=ED=99=95=EB=8C=80=C2=B7?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=EC=9D=84=20=EB=94=B0=EB=9D=BC?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockers/LockersLockerSelectScreen.tsx | 74 +++++++--- .../components/LockersLockerMinimap.tsx | 134 ++++++++++++++---- 2 files changed, 161 insertions(+), 47 deletions(-) diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index a37e4fe..1f40504 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -19,6 +19,19 @@ import { } from "@/features/lockers/constants/lockersSectionDetails"; import { useLockersPinchZoom } from "@/features/lockers/hooks/useLockersPinchZoom"; +// 배치 영역 안쪽 여백(px-5, pb-5) — 미니맵에 보이는 영역을 배치 기준으로 계산할 때 뺀다 +const CONTENT_PADDING = 20; + +interface MinimapState { + viewport: LockersLockerMinimapViewport; + layoutWidth: number; + isScrollable: boolean; +} + +function clampRatio(value: number) { + return Math.min(Math.max(value, 0), 1); +} + // Figma: A-1구역 (nodeId 2159:110753), A-2구역 (2159:109533), A-1구역 실제사진 (2159:110174) function LockersLockerSelectScreen() { const { sectionId = "" } = useParams(); @@ -39,9 +52,8 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { number | null >(null); const [isPhotoOpen, setIsPhotoOpen] = useState(false); - const [viewport, setViewport] = useState( - null, - ); + const [minimap, setMinimap] = useState(null); + const [scrollerWidth, setScrollerWidth] = useState(); const scrollRef = useRef(null); const contentRef = useRef(null); @@ -55,19 +67,36 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { ? undefined : lockers.get(selectedLockerNumber); - // 칸 배치가 화면보다 넓을 때만 미니맵을 띄운다 — Figma도 화면에 다 들어오는 A-2는 - // 미니맵을 투명하게 숨겨 자리만 남겨뒀다. const updateViewport = useCallback(() => { - const el = scrollRef.current; - if (!el) { + const scroller = scrollRef.current; + const content = contentRef.current; + if (!scroller || !content) { return; } - const { clientWidth, scrollLeft, scrollWidth } = el; - setViewport( - scrollWidth > clientWidth - ? { size: clientWidth / scrollWidth, start: scrollLeft / scrollWidth } - : null, + // 제스처 중에는 배율이 상태보다 앞서 DOM에만 반영돼 있어서 DOM 값을 읽는다 + const zoom = Number(content.style.zoom) || 1; + const rect = content.getBoundingClientRect(); + const layoutWidth = rect.width / zoom - CONTENT_PADDING * 2; + const layoutHeight = rect.height / zoom - CONTENT_PADDING; + const visibleLeft = scroller.scrollLeft / zoom - CONTENT_PADDING; + const visibleTop = scroller.scrollTop / zoom; + const left = clampRatio(visibleLeft / layoutWidth); + const top = clampRatio(visibleTop / layoutHeight); + const right = clampRatio( + (visibleLeft + scroller.clientWidth / zoom) / layoutWidth, ); + const bottom = clampRatio( + (visibleTop + scroller.clientHeight / zoom) / layoutHeight, + ); + + setScrollerWidth(scroller.clientWidth); + setMinimap({ + isScrollable: + scroller.scrollWidth > scroller.clientWidth + 1 || + scroller.scrollHeight > scroller.clientHeight + 1, + layoutWidth, + viewport: { height: bottom - top, left, top, width: right - left }, + }); }, []); useLayoutEffect(() => { @@ -137,28 +166,33 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { return (
- {/* Figma Minimap Section — 미니맵이 없어도 128px 자리를 지켜서 칸 배치 위치가 구역마다 같다 */} -
- {viewport ? ( - + {/* Figma Minimap Section */} +
+ {minimap ? ( + {map} ) : ( -
+
)}
{/* touch-pan: 두 손가락 동작을 페이지 확대 대신 이 영역의 확대로 받는다. - min-w-full: 화면보다 좁은 구역에서 "fill" 상자가 화면 폭까지 늘어나게 한다 */} + minWidth를 %가 아니라 px로 주는 이유: zoom을 걸면 %는 배율과 상관없이 화면 폭으로 + 계산돼서, 화면보다 좁은 구역을 확대할 때 "fill" 상자가 비율대로 커지지 않는다 */}
{map}
diff --git a/src/features/lockers/components/LockersLockerMinimap.tsx b/src/features/lockers/components/LockersLockerMinimap.tsx index ff04943..fc63bc7 100644 --- a/src/features/lockers/components/LockersLockerMinimap.tsx +++ b/src/features/lockers/components/LockersLockerMinimap.tsx @@ -1,48 +1,128 @@ -import type { ReactNode } from "react"; +import { type ReactNode, useLayoutEffect, useRef, useState } from "react"; +/** 칸 배치 전체 대비 지금 보이는 영역(0~1 비율) */ export interface LockersLockerMinimapViewport { - /** 스크롤 영역 전체 폭 대비 보이는 영역의 시작 위치(0~1) */ - start: number; - /** 스크롤 영역 전체 폭 대비 보이는 영역의 폭(0~1) */ - size: number; + left: number; + top: number; + width: number; + height: number; } interface LockersLockerMinimapProps { - /** 본문과 같은 칸 배치를 그대로 넘긴다 — 축소해서 그린다 */ + /** 본문과 같은 칸 배치 — 축소해서 그린다 */ children: ReactNode; + /** 본문 칸 배치의 1배 기준 폭. 같은 폭으로 그려야 "fill" 상자 비율이 본문과 같다 */ + layoutWidth: number; viewport: LockersLockerMinimapViewport; + /** 스크롤이 없으면(전체가 보이면) 테두리를 숨긴다 */ + isScrollable: boolean; } -// Figma 미니맵(2159:110757)은 본문 칸 배치(약 620px 폭)를 약 192px로 줄여 그렸다. -const MINIMAP_ZOOM = 0.31; - -// Figma: Minimap (nodeId 2159:110757) — Stream 로컬. 좌우로 스크롤되는 칸 배치의 전체 모습과 -// 지금 보이는 자리(검은 테두리, Figma "Highlight Overlay")를 보여준다. -// -// Figma는 칸·글자 크기를 하나하나 줄여서(칸 8px·글자 4px) 따로 그렸지만, 여기서는 본문 배치를 -// `zoom`으로 줄여서 그린다. 칸 배치가 바뀌어도 미니맵을 따로 고칠 필요가 없고, 테두리 위치를 -// 스크롤 비율에 그대로 맞출 수 있다. 4px 글자를 직접 쓰면 브라우저 최소 글자 크기에 걸리는 것도 피한다. -// -// 테두리는 Figma처럼 미니맵보다 위아래로 크다(미니맵 110px · 테두리 128px, 미니맵이 10px 아래에서 시작). +// Figma 미니맵 상자 110px, 안쪽 여백 8px. 테두리(Highlight Overlay)는 상자보다 위아래로 9px씩 크다(128px). +const BOX_HEIGHT = 110; +const BOX_PADDING = 8; +const HIGHLIGHT_OVERHANG = 9; +const AREA_HEIGHT = BOX_HEIGHT + HIGHLIGHT_OVERHANG * 2; +// 테두리가 끝에 닿았는지 볼 때 반올림 오차를 봐준다 +const EDGE_EPSILON = 0.001; + +// Figma: Minimap (nodeId 2159:110757) — Stream 로컬 +// Figma는 칸·글자를 따로 줄여 그렸지만, 본문 배치를 zoom으로 줄여 그린다(배치를 두 벌 관리하지 않고, +// 4px 글자가 브라우저 최소 글자 크기에 걸리지 않는다). 높이는 고정, 폭은 배치 비율에 맞추되 남은 폭을 넘지 않는다. function LockersLockerMinimap({ children, + layoutWidth, viewport, + isScrollable, }: LockersLockerMinimapProps) { + const areaRef = useRef(null); + const contentRef = useRef(null); + const [scale, setScale] = useState(null); + const [layoutHeight, setLayoutHeight] = useState(0); + + useLayoutEffect(() => { + const area = areaRef.current; + const content = contentRef.current; + if (!area || !content) { + return; + } + const update = () => { + const currentScale = Number(content.style.zoom) || 1; + const height = content.getBoundingClientRect().height / currentScale; + setLayoutHeight(height); + setScale( + Math.min( + (BOX_HEIGHT - BOX_PADDING * 2) / height, + (area.clientWidth - BOX_PADDING * 2) / layoutWidth, + ), + ); + }; + update(); + const observer = new ResizeObserver(update); + observer.observe(area); + return () => observer.disconnect(); + }, [layoutWidth]); + + const innerWidth = layoutWidth * (scale ?? 0); + const innerHeight = layoutHeight * (scale ?? 0); + const boxWidth = innerWidth + BOX_PADDING * 2; + const contentTop = HIGHLIGHT_OVERHANG + BOX_PADDING; + const right = viewport.left + viewport.width; + const bottom = viewport.top + viewport.height; + + // 끝에 닿은 쪽은 Figma처럼 상자 밖(가로는 상자 끝, 세로는 9px 바깥)까지 늘린다 + const highlightLeft = + viewport.left <= EDGE_EPSILON + ? 0 + : BOX_PADDING + viewport.left * innerWidth; + const highlightRight = + right >= 1 - EDGE_EPSILON ? boxWidth : BOX_PADDING + right * innerWidth; + const highlightTop = + viewport.top <= EDGE_EPSILON ? 0 : contentTop + viewport.top * innerHeight; + const highlightBottom = + bottom >= 1 - EDGE_EPSILON + ? AREA_HEIGHT + : contentTop + bottom * innerHeight; + return ( -
-
+
+
{/* 축소본 안의 칸 버튼이 Tab·스크린리더에 두 번 잡히지 않게 막는다 */} -
+
{children}
-
+ {isScrollable && ( +
+ )}
); } From d892cfe8a2edd717e44a69a79b827e5f158f1574 Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 11:10:05 +0900 Subject: [PATCH 18/19] =?UTF-8?q?docs:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=B9=B8=20=EB=B0=B0=EC=B9=98=20layout=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=9F=AC=C2=B7=ED=99=95=EB=8C=80=C2=B7=EB=AF=B8=EB=8B=88?= =?UTF-8?q?=EB=A7=B5=20=EB=A7=A4=ED=95=91=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 | 33 ++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index ad31e05..0542141 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -486,18 +486,45 @@ WDS에 이름이 비슷한 `ProgressIndicator`가 실제로 존재해서(`제외 칸(`Locker Cell`)·칸 묶음(`Zone Area`)·`Shelf Label`·범례·미니맵·`Locker Selector`·실제사진 모달은 전부 Stream 로컬이다. `search_design_system`에서 `Locker Cell`은 결과가 없었다. +### 구역별 칸 배치는 서버 layout JSON을 렌더러로 그린다 + +구역마다 배치 컴포넌트를 두지 않고, 구역 상세 응답의 `layout`(블록 7종: `row`·`column`·`lockerGroup`·`label`·`area`·`text`·`shelfLabel`)을 `LockersLayoutRenderer`가 그린다. 타입과 A-1·A-2 목데이터는 `constants/lockersSectionDetails.ts`. 블록은 Figma 로컬 요소와 이렇게 대응한다. + +| 블록 | Figma | 코드 | +|---|---|---| +| `lockerGroup` | Zone Area + Locker Grid | `LockersLockerGrid`(`bordered`면 `Line/Solid/Alternative` 테두리 상자) | +| `label` | Direction Label·Aisle | `LockersMapLabel` — 세로면 한 글자씩 줄바꿈하고 단어 사이에 빈 줄 | +| `area` | Room Label·Zone Label·Stairs Area | `LockersMapArea` — 구역 선택 평면도와 같은 점선 상자 | +| `shelfLabel` | Shelf Label | `LockersShelfLabel` | + +크기는 `hug`·`fill`·px다. `fill`은 부모와 같은 축이면 남은 공간을 나누고(`flex-1`) 다른 축이면 부모에 맞춰 늘어난다(`self-stretch`). 칸 상태는 layout에 없고 `lockers`에서 `lockerNumber`로 찾는다. + ### 선택 불가 칸은 범례 SVG 하나로 그린다 Figma 범례의 선택 불가 칸(`2159:110819`)은 `Label/Assistive` 바탕 + `Label/Neutral` 대각선을 합친 15px SVG다. `preserveAspectRatio="none"`이라 28px 칸에 그대로 늘려도 모서리(2.29 → 4.28px)·선 굵기가 본문 칸(radius 4)과 맞아서, 본문 칸도 같은 파일(`locker-unavailable.svg`)을 쓴다. ### 미니맵은 따로 그리지 않고 본문 배치를 `zoom`으로 줄였다 -Figma 미니맵은 칸 8px·글자 3.5~4px로 하나하나 다시 그렸지만(값도 칸마다 비율이 제각각), 코드에서는 본문 칸 배치를 `zoom: 0.31`로 줄여 그린다. 칸 배치가 바뀌어도 미니맵을 따로 고칠 일이 없고, 검은 테두리(`Highlight Overlay`)를 가로 스크롤 비율에 그대로 맞출 수 있다. 4px 글자를 직접 쓰면 브라우저 최소 글자 크기에 걸리는 문제도 피한다. A-2처럼 화면에 다 들어오는 구역은 Figma도 미니맵을 `opacity: 0`으로 숨겨서, 코드는 스크롤이 생길 때만 미니맵을 띄우고 128px 자리는 항상 남긴다. +Figma 미니맵은 칸 8px·글자 3.5~4px로 하나하나 다시 그렸지만, 코드에서는 본문 칸 배치를 `zoom`으로 줄여 그린다. 배치를 두 벌 관리하지 않고, 4px 글자가 브라우저 최소 글자 크기에 걸리는 문제도 피한다. + +- 높이는 Figma대로 110px 고정, 폭은 배치 비율에 맞춘다(Figma 미니맵 폭 156~208px도 내용에 맞춘 값이다). 범례 앞 남은 폭을 넘으면 더 줄인다 +- 본문과 같은 1배 기준 폭으로 그려야 `fill` 상자 비율이 본문과 같다 +- 확대·축소가 있어서 모든 구역에 띄운다. 검은 테두리(`Highlight Overlay`)는 보이는 영역의 가로·세로 비율을 따라가고(확대하면 작아진다), 스크롤이 없으면 숨긴다 + +### 칸 배치 확대·축소는 CSS `zoom`으로 한다 + +`useLockersPinchZoom` — 핀치·Ctrl+휠(트랙패드 핀치), 1배에서 가운데부터, 최소는 배치 전체가 화면 폭에 들어오는 배율, 최대 3배(CGV 좌석 선택 참고). `transform: scale`은 스크롤 영역 크기에 반영되지 않아 `zoom`을 쓴다. 확대하는 래퍼의 최소 폭을 `%`로 주면 배율과 상관없이 화면 폭으로 계산돼 `fill` 상자가 비율대로 커지지 않으므로 px로 준다. 배치 영역에 `touch-pan-x touch-pan-y`를 줘서 두 손가락 동작이 페이지 확대로 가지 않게 한다 — 앱 WebView(iOS·안드로이드)에서는 실기기 확인이 필요하다. ### Figma 아티팩트 — A-1 `Shelf Label` 높이가 1px이다 -A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽 칸`이 칸 묶음 아래로 흘러내려 보인다. 미니맵(`2159:110761`)과 A-2(`2159:109622`)에서는 칸 묶음 높이에 맞춰 붙어 있어서, 본문도 칸 묶음 줄 높이(122px)에 맞췄다. +A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽 칸`이 칸 묶음 아래로 흘러내려 보인다. 미니맵(`2159:110761`)과 A-2(`2159:109622`)에서는 칸 묶음 높이에 맞춰 붙어 있어서, 칸 묶음 높이에 맞춘다 — layout의 `shelfLabel.height`(테두리 있는 3행 묶음 122, 칸만 있는 3행 묶음 96). ### 사물함 선택 시(`2159:113197`) — 선택 상태는 구역 카드와 다르다 -구역 카드(`LockersSectionCard`)는 선택되면 Blue/95 배경 + Primary 테두리지만, 사물함 칸은 **Primary/Normal 배경 + 흰 SemiBold 번호**다(`2159:113315`). 구역 카드 표현을 그대로 베끼면 틀린다. 하단 `Locker Selector`는 번호를 `Label 1/Normal - Bold` + Primary/Normal로 `B-25`처럼(동 이름 + 칸 번호, 구역 번호 없음) 쓰고, 버튼 문구는 `사물함 신청하기`다. +구역 카드(`LockersSectionCard`)는 선택되면 Blue/95 배경 + Primary 테두리지만, 사물함 칸은 **Primary/Normal 배경 + 흰 SemiBold 번호**다(`2159:113315`). 구역 카드 표현을 그대로 베끼면 틀린다. 하단 `Locker Selector`는 번호를 `Label 1/Normal - Bold` + Primary/Normal로 `B-25`처럼 쓰고, 버튼 문구는 `사물함 신청하기`다. 이름은 조합하지 않고 서버 `lockerLabel`을 그대로 쓴다. + +내 사물함(`isMine`)은 Figma에 상태가 없어서 임시로 `Status/Positive` 초록 테두리를 둔다(이미 신청한 칸이라 고를 수 없다). + +### 실제 사진 핀 + +핀 좌표는 사진 대비 0~1 비율(핀 가운데)로 받고, 색은 칸이 묶음의 몇 번째 행인지로 정한다 — 위쪽 행일수록 진하게, 3행은 Orange/50·70·90, 5행은 50·60·70·80·90(B-2구역 실제사진 `2159:110653`). 핀 크기는 Figma 두 값(12.4·14.9px)의 중간인 14px로 고정했다. Figma의 사진 회전(1.12°)·확대 보정은 사진을 자를 때 끝내고 코드는 틀에 그대로 채운다. From 3abaa94886ad607fe70403440c9731dc34b9d58f Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 2 Oct 2026 14:32:11 +0900 Subject: [PATCH 19/19] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=82=AC=EC=A7=84=20=ED=95=80=20=EC=A2=8C=ED=91=9C=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EC=97=90=EC=84=9C=20=EB=B9=BC=EA=B3=A0=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/wds-component-usage.md | 2 +- .../lockers/LockersLockerSelectScreen.tsx | 4 +- .../components/LockersSectionPhotoModal.tsx | 12 ++-- .../constants/lockersSectionDetails.ts | 63 ++----------------- .../constants/lockersSectionPhotoPins.ts | 56 +++++++++++++++++ 5 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 src/features/lockers/constants/lockersSectionPhotoPins.ts diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 0542141..6f60bf2 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -527,4 +527,4 @@ A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽 ### 실제 사진 핀 -핀 좌표는 사진 대비 0~1 비율(핀 가운데)로 받고, 색은 칸이 묶음의 몇 번째 행인지로 정한다 — 위쪽 행일수록 진하게, 3행은 Orange/50·70·90, 5행은 50·60·70·80·90(B-2구역 실제사진 `2159:110653`). 핀 크기는 Figma 두 값(12.4·14.9px)의 중간인 14px로 고정했다. Figma의 사진 회전(1.12°)·확대 보정은 사진을 자를 때 끝내고 코드는 틀에 그대로 채운다. +서버는 사진 주소(`photoUrl`)만 주고 핀 좌표는 프론트가 구역별로 관리한다(`constants/lockersSectionPhotoPins.ts`, 사진 대비 0~1 비율·핀 가운데 기준). 사진을 바꾸면 좌표도 같이 고친다. 색은 칸이 묶음의 몇 번째 행인지로 정한다 — 위쪽 행일수록 진하게, 3행은 Orange/50·70·90, 5행은 50·60·70·80·90(B-2구역 실제사진 `2159:110653`). 핀 크기는 Figma 두 값(12.4·14.9px)의 중간인 14px로 고정했다. Figma의 사진 회전(1.12°)·확대 보정은 사진을 자를 때 끝내고 코드는 틀에 그대로 채운다. diff --git a/src/features/lockers/LockersLockerSelectScreen.tsx b/src/features/lockers/LockersLockerSelectScreen.tsx index 1f40504..761f192 100644 --- a/src/features/lockers/LockersLockerSelectScreen.tsx +++ b/src/features/lockers/LockersLockerSelectScreen.tsx @@ -17,6 +17,7 @@ import { LOCKERS_SECTION_DETAILS, type LockersSectionDetail, } from "@/features/lockers/constants/lockersSectionDetails"; +import { LOCKERS_SECTION_PHOTO_PINS } from "@/features/lockers/constants/lockersSectionPhotoPins"; import { useLockersPinchZoom } from "@/features/lockers/hooks/useLockersPinchZoom"; // 배치 영역 안쪽 여백(px-5, pb-5) — 미니맵에 보이는 영역을 배치 기준으로 계산할 때 뺀다 @@ -209,7 +210,8 @@ function SectionLockerSelect({ detail }: { detail: LockersSectionDetail }) { layout={detail.layout} onClose={() => setIsPhotoOpen(false)} open={isPhotoOpen} - photo={detail.photo} + photoUrl={detail.photoUrl} + pins={LOCKERS_SECTION_PHOTO_PINS[detail.section] ?? []} sectionName={detail.section} />
diff --git a/src/features/lockers/components/LockersSectionPhotoModal.tsx b/src/features/lockers/components/LockersSectionPhotoModal.tsx index ad4edd5..9612861 100644 --- a/src/features/lockers/components/LockersSectionPhotoModal.tsx +++ b/src/features/lockers/components/LockersSectionPhotoModal.tsx @@ -8,13 +8,14 @@ import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; import type { LockersLayout, LockersLayoutBlock, - LockersSectionPhoto, } from "@/features/lockers/constants/lockersSectionDetails"; +import type { LockersSectionPhotoPin } from "@/features/lockers/constants/lockersSectionPhotoPins"; interface LockersSectionPhotoModalProps { open: boolean; sectionName: string; - photo: LockersSectionPhoto; + photoUrl: string; + pins: LockersSectionPhotoPin[]; /** 핀 색을 칸의 행 위치로 정하는 데 쓴다 */ layout: LockersLayout; onClose: () => void; @@ -70,7 +71,8 @@ function collectPinToneClassNames( function LockersSectionPhotoModal({ open, sectionName, - photo, + photoUrl, + pins, layout, onClose, }: LockersSectionPhotoModalProps) { @@ -142,9 +144,9 @@ function LockersSectionPhotoModal({ {`${sectionName}구역 - {photo.pins.map((pin) => ( + {pins.map((pin) => ( from + index); } -// Figma: A-1구역 (nodeId 2159:110753), 핀은 A-1구역 실제사진 (2159:110299) -// Figma는 사진을 1.12° 돌려 붙였는데 회전은 무시하고 비율로 옮겼다(가장자리에서 2~3px 차이). +// Figma: A-1구역 (nodeId 2159:110753) const A1_SECTION: LockersSectionDetail = { layout: { root: { @@ -184,53 +171,13 @@ const A1_SECTION: LockersSectionDetail = { version: 1, }, lockers: createMockLockers("A", range(1, 36)), - photo: { - pins: [ - { lockerNumber: 1, x: 0.758, y: 0.392 }, - { lockerNumber: 2, x: 0.836, y: 0.403 }, - { lockerNumber: 3, x: 0.936, y: 0.418 }, - { lockerNumber: 4, x: 0.758, y: 0.54 }, - { lockerNumber: 5, x: 0.836, y: 0.594 }, - { lockerNumber: 6, x: 0.936, y: 0.667 }, - { lockerNumber: 7, x: 0.758, y: 0.667 }, - { lockerNumber: 8, x: 0.836, y: 0.758 }, - { lockerNumber: 9, x: 0.936, y: 0.852 }, - { lockerNumber: 10, x: 0.081, y: 0.437 }, - { lockerNumber: 11, x: 0.175, y: 0.413 }, - { lockerNumber: 12, x: 0.243, y: 0.396 }, - { lockerNumber: 13, x: 0.081, y: 0.635 }, - { lockerNumber: 14, x: 0.175, y: 0.583 }, - { lockerNumber: 15, x: 0.243, y: 0.54 }, - { lockerNumber: 16, x: 0.081, y: 0.848 }, - { lockerNumber: 17, x: 0.175, y: 0.751 }, - { lockerNumber: 18, x: 0.243, y: 0.678 }, - { lockerNumber: 19, x: 0.345, y: 0.622 }, - { lockerNumber: 20, x: 0.406, y: 0.622 }, - { lockerNumber: 21, x: 0.466, y: 0.622 }, - { lockerNumber: 22, x: 0.345, y: 0.521 }, - { lockerNumber: 23, x: 0.406, y: 0.521 }, - { lockerNumber: 24, x: 0.466, y: 0.521 }, - { lockerNumber: 25, x: 0.345, y: 0.419 }, - { lockerNumber: 26, x: 0.406, y: 0.419 }, - { lockerNumber: 27, x: 0.466, y: 0.419 }, - { lockerNumber: 28, x: 0.527, y: 0.419 }, - { lockerNumber: 29, x: 0.588, y: 0.419 }, - { lockerNumber: 30, x: 0.648, y: 0.419 }, - { lockerNumber: 31, x: 0.527, y: 0.521 }, - { lockerNumber: 32, x: 0.588, y: 0.521 }, - { lockerNumber: 33, x: 0.648, y: 0.521 }, - { lockerNumber: 34, x: 0.527, y: 0.622 }, - { lockerNumber: 35, x: 0.588, y: 0.622 }, - { lockerNumber: 36, x: 0.648, y: 0.622 }, - ], - url: sectionA1Photo, - }, + photoUrl: sectionA1Photo, section: "A-1", sectionId: 1, }; // Figma: A-2구역 (nodeId 2159:109533) -// A-2 사진이 아직 없어서 A-1 사진을 임시로 쓰고, 번호가 맞지 않아 핀은 비워 둔다. +// A-2 사진이 아직 없어서 A-1 사진을 임시로 쓴다. const A2_SECTION: LockersSectionDetail = { layout: { root: { @@ -274,7 +221,7 @@ const A2_SECTION: LockersSectionDetail = { version: 1, }, lockers: createMockLockers("A", range(82, 90)), - photo: { pins: [], url: sectionA1Photo }, + photoUrl: sectionA1Photo, section: "A-2", sectionId: 2, }; diff --git a/src/features/lockers/constants/lockersSectionPhotoPins.ts b/src/features/lockers/constants/lockersSectionPhotoPins.ts new file mode 100644 index 0000000..d7d8b60 --- /dev/null +++ b/src/features/lockers/constants/lockersSectionPhotoPins.ts @@ -0,0 +1,56 @@ +// 사진 위 칸 번호 핀. 서버는 사진 주소만 주고, 핀 좌표는 구역별로 여기서 관리한다 — 사진을 바꾸면 같이 고친다. +// x·y는 핀 가운데 위치를 사진 폭·높이 대비 0~1 비율로 적는다. +export interface LockersSectionPhotoPin { + lockerNumber: number; + x: number; + y: number; +} + +// Figma: A-1구역 실제사진 (nodeId 2159:110299) +// Figma는 사진을 1.12° 돌려 붙였는데 회전은 무시하고 비율로 옮겼다(가장자리에서 2~3px 차이). +const A1_PINS: LockersSectionPhotoPin[] = [ + { lockerNumber: 1, x: 0.758, y: 0.392 }, + { lockerNumber: 2, x: 0.836, y: 0.403 }, + { lockerNumber: 3, x: 0.936, y: 0.418 }, + { lockerNumber: 4, x: 0.758, y: 0.54 }, + { lockerNumber: 5, x: 0.836, y: 0.594 }, + { lockerNumber: 6, x: 0.936, y: 0.667 }, + { lockerNumber: 7, x: 0.758, y: 0.667 }, + { lockerNumber: 8, x: 0.836, y: 0.758 }, + { lockerNumber: 9, x: 0.936, y: 0.852 }, + { lockerNumber: 10, x: 0.081, y: 0.437 }, + { lockerNumber: 11, x: 0.175, y: 0.413 }, + { lockerNumber: 12, x: 0.243, y: 0.396 }, + { lockerNumber: 13, x: 0.081, y: 0.635 }, + { lockerNumber: 14, x: 0.175, y: 0.583 }, + { lockerNumber: 15, x: 0.243, y: 0.54 }, + { lockerNumber: 16, x: 0.081, y: 0.848 }, + { lockerNumber: 17, x: 0.175, y: 0.751 }, + { lockerNumber: 18, x: 0.243, y: 0.678 }, + { lockerNumber: 19, x: 0.345, y: 0.622 }, + { lockerNumber: 20, x: 0.406, y: 0.622 }, + { lockerNumber: 21, x: 0.466, y: 0.622 }, + { lockerNumber: 22, x: 0.345, y: 0.521 }, + { lockerNumber: 23, x: 0.406, y: 0.521 }, + { lockerNumber: 24, x: 0.466, y: 0.521 }, + { lockerNumber: 25, x: 0.345, y: 0.419 }, + { lockerNumber: 26, x: 0.406, y: 0.419 }, + { lockerNumber: 27, x: 0.466, y: 0.419 }, + { lockerNumber: 28, x: 0.527, y: 0.419 }, + { lockerNumber: 29, x: 0.588, y: 0.419 }, + { lockerNumber: 30, x: 0.648, y: 0.419 }, + { lockerNumber: 31, x: 0.527, y: 0.521 }, + { lockerNumber: 32, x: 0.588, y: 0.521 }, + { lockerNumber: 33, x: 0.648, y: 0.521 }, + { lockerNumber: 34, x: 0.527, y: 0.622 }, + { lockerNumber: 35, x: 0.588, y: 0.622 }, + { lockerNumber: 36, x: 0.648, y: 0.622 }, +]; + +// 구역 라벨로 찾는다. A-2는 사진이 아직 없어서(A-1 사진 임시 사용) 핀이 없다. +export const LOCKERS_SECTION_PHOTO_PINS: Record< + string, + LockersSectionPhotoPin[] +> = { + "A-1": A1_PINS, +};