From 39beb3af04abe9f3b06001d4dad890175287a1f1 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:04:26 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat=20:=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8:=20BottomSheet=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 206 +++++++++++++++++++ src/features/pin/components/PinListSheet.tsx | 91 ++++++++ src/features/pin/components/PinSongCard.tsx | 46 +++++ src/features/pin/components/SortTabs.tsx | 39 ++++ src/types/pin.ts | 18 ++ 5 files changed, 400 insertions(+) create mode 100644 src/components/ui/BottomSheet.tsx create mode 100644 src/features/pin/components/PinListSheet.tsx create mode 100644 src/features/pin/components/PinSongCard.tsx create mode 100644 src/features/pin/components/SortTabs.tsx create mode 100644 src/types/pin.ts diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx new file mode 100644 index 00000000..17785412 --- /dev/null +++ b/src/components/ui/BottomSheet.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; +import { ChevronLeft, X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +type SnapPoint = number | string; + +const DEFAULT_SNAP_POINTS: SnapPoint[] = [0.5, 1]; + +/** Vaul Drawer 루트. 시트 열림/닫힘·스냅 포인트 상태를 관리한다. */ +function SheetRoot({ ...props }: React.ComponentProps) { + return ; +} + +/** 시트를 document body 포털로 렌더링한다. */ +function SheetPortal({ ...props }: React.ComponentProps) { + return ; +} + +type SheetContentProps = React.ComponentProps; + +/** 화면 하단에 고정되는 시트 패널. 기본 레이아웃·스타일을 적용한다. */ +function SheetContent({ className, children, ...props }: SheetContentProps) { + return ( + + + {children} + + + ); +} + +/** 상단 드래그 핸들. 사용자가 시트 높이를 조절할 때 잡는 영역이다. */ +function SheetHandle({ ...props }: React.ComponentProps) { + return ( + + ); +} + +type BottomSheetContextValue = { + isFullPage: boolean; + onClose: () => void; + /** 풀페이지 → 이전 스냅(절반)으로 내리기 */ + collapse: () => void; +}; + +const BottomSheetContext = React.createContext(null); + +/** BottomSheet 하위 컴포넌트에서 풀페이지 여부·닫기·접기 상태에 접근하는 훅. */ +function useBottomSheet() { + const context = React.useContext(BottomSheetContext); + if (!context) { + throw new Error('useBottomSheet must be used within BottomSheet'); + } + return context; +} + +type BottomSheetProps = { + open: boolean; + onClose: () => void; + children: React.ReactNode; + /** drag handle 표시 여부 */ + showHandle?: boolean; + /** 오버레이 클릭·드래그로 닫기 */ + dismissible?: boolean; + /** + * 스냅 지점(높이 단계). 기본 [0.5, 1] = 절반 → 전체. + * 마지막 지점(끝까지 올림)에 도달하면 페이지 형식으로 전환된다. + */ + snapPoints?: SnapPoint[]; + className?: string; +}; + +/** + * 바텀시트 메인 컴포넌트. + * 스냅 포인트(기본 절반 → 전체)를 제어하고, 마지막 스냅에 도달하면 풀페이지 UI로 전환한다. + */ +function BottomSheet({ + open, + onClose, + children, + showHandle = true, + dismissible = true, + snapPoints = DEFAULT_SNAP_POINTS, + className, +}: BottomSheetProps) { + const firstSnap = snapPoints[0] ?? null; + const lastSnap = snapPoints[snapPoints.length - 1]; + const [activeSnap, setActiveSnap] = React.useState(firstSnap); + const [prevOpen, setPrevOpen] = React.useState(open); + + if (open !== prevOpen) { + setPrevOpen(open); + if (open) { + setActiveSnap(firstSnap); + } + } + + const isFullPage = activeSnap === lastSnap; + + const collapse = React.useCallback(() => { + setActiveSnap(firstSnap); + }, [firstSnap]); + + return ( + + { + if (!isOpen) { + onClose(); + } + }} + > + + {showHandle && !isFullPage ? : null} +
{children}
+
+
+
+ ); +} + +type SectionProps = React.ComponentProps<'div'>; + +/** 시트 상단 고정 영역. 제목·검색창 등 스크롤되지 않는 헤더에 사용한다. */ +function BottomSheetHeader({ className, ...props }: SectionProps) { + return
; +} + +/** 시트 본문 스크롤 영역. 리스트·카드 등 긴 콘텐츠를 담는다. */ +function BottomSheetContent({ className, ...props }: SectionProps) { + return ( +
+ ); +} + +type FullPageNavProps = { + className?: string; +}; + +/** 풀페이지 모드에서만 표시되는 상단 네비. 뒤로(접기)·닫기 버튼을 제공한다. */ +function BottomSheetFullPageNav({ className }: FullPageNavProps) { + const { isFullPage, collapse, onClose } = useBottomSheet(); + + if (!isFullPage) return null; + + return ( +
+ + +
+ ); +} + +BottomSheet.Header = BottomSheetHeader; +BottomSheet.Content = BottomSheetContent; +BottomSheet.FullPageNav = BottomSheetFullPageNav; + +export { BottomSheet, useBottomSheet }; diff --git a/src/features/pin/components/PinListSheet.tsx b/src/features/pin/components/PinListSheet.tsx new file mode 100644 index 00000000..a0b601ec --- /dev/null +++ b/src/features/pin/components/PinListSheet.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; + +import { BottomSheet } from '@/components/ui/BottomSheet'; +import { PinSongCard } from '@/features/pin/components/PinSongCard'; +import { SortTabs } from '@/features/pin/components/SortTabs'; +import { Bookmark } from '@/assets/icons'; +import type { PinSong, PinSort, PlaceInfo } from '@/types/pin'; + +type PinListSheetProps = { + place?: PlaceInfo; + pins?: PinSong[]; + onPinClick?: (pin: PinSong) => void; +}; + +const MOCK_PLACE: PlaceInfo = { + id: '1', + name: '물빛무대 앞 광장', + creatorName: '냥코', + distance: 100, +}; + +const MOCK_PINS: PinSong[] = [ + { + id: '1', + title: 'LOVE ATTACK', + artist: 'RESCENE', + pinCount: 1, + likeCount: 1, + liked: true, + }, +]; + +export function PinListSheet({ + place = MOCK_PLACE, + pins = MOCK_PINS, + onPinClick, +}: PinListSheetProps) { + const [open, setOpen] = useState(false); + const [sort, setSort] = useState('popular'); + + return ( +
+

PIN 목록 바텀시트 미리보기

+ + + + setOpen(false)}> + + + +
+
+

{place.name}

+

+ {place.creatorName} 님이 + 생성한 PIN · {place.distance}m +

+
+ + +
+
+ +
+
+ + +
    + {pins.map((pin) => ( +
  • + onPinClick?.(pin)} /> +
  • + ))} +
+
+
+
+ ); +} diff --git a/src/features/pin/components/PinSongCard.tsx b/src/features/pin/components/PinSongCard.tsx new file mode 100644 index 00000000..95bcc2e4 --- /dev/null +++ b/src/features/pin/components/PinSongCard.tsx @@ -0,0 +1,46 @@ +import { ChevronRight, Heart } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import type { PinSong } from '@/types/pin'; + +type PinSongCardProps = { + pin: PinSong; + onClick?: () => void; + className?: string; +}; + +export function PinSongCard({ pin, onClick }: PinSongCardProps) { + const { title, artist, pinCount, likeCount, liked } = pin; + + return ( + + ); +} diff --git a/src/features/pin/components/SortTabs.tsx b/src/features/pin/components/SortTabs.tsx new file mode 100644 index 00000000..1cf8a56b --- /dev/null +++ b/src/features/pin/components/SortTabs.tsx @@ -0,0 +1,39 @@ +import { cn } from '@/lib/utils'; +import type { PinSort } from '@/types/pin'; + +type SortTabsProps = { + value: PinSort; + onChange: (value: PinSort) => void; + className?: string; +}; + +const OPTIONS: { value: PinSort; label: string }[] = [ + { value: 'popular', label: '인기순' }, + { value: 'latest', label: '최신순' }, +]; + +export function SortTabs({ value, onChange }: SortTabsProps) { + return ( +
+ {OPTIONS.map((option) => { + const selected = value === option.value; + + return ( + + ); + })} +
+ ); +} diff --git a/src/types/pin.ts b/src/types/pin.ts new file mode 100644 index 00000000..e11fc3e9 --- /dev/null +++ b/src/types/pin.ts @@ -0,0 +1,18 @@ +export type PinSort = 'popular' | 'latest'; + +export type PlaceInfo = { + id: string; + name: string; + creatorName: string; + distance: number; +}; + +export type PinSong = { + id: string; + title: string; + artist: string; + pinCount: number; + likeCount?: number; + liked?: boolean; + thumbnailUrl?: string; +}; From b655dc9749fe577ae75146ee745cf15f9acc78b1 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:31:06 +0900 Subject: [PATCH 02/12] =?UTF-8?q?chore=20:=20vaul=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- pnpm-lock.yaml | 53 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index bd055199..55556a90 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,13 @@ "axios": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "radix-ui": "^1.6.1", "lucide-react": "^1.23.0", + "radix-ui": "^1.6.1", "react": "latest", "react-dom": "latest", "react-router-dom": "latest", "tailwind-merge": "^3.6.0", + "vaul": "^1.1.2", "zustand": "latest" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bb7e487..1e4a7403 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: tailwind-merge: specifier: ^3.6.0 version: 3.6.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) zustand: specifier: latest version: 5.0.14(@types/react@19.2.17)(react@19.2.7) @@ -46,13 +49,13 @@ importers: version: 10.0.1(eslint@10.6.0(jiti@2.7.0)) '@tailwindcss/vite': specifier: latest - version: 4.3.2(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.2(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@types/google.maps': specifier: ^3.65.2 version: 3.65.2 '@types/node': specifier: latest - version: 26.1.0 + version: 26.1.1 '@types/react': specifier: latest version: 19.2.17 @@ -61,7 +64,7 @@ importers: version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': specifier: latest - version: 6.0.3(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)) + version: 6.0.3(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) eslint: specifier: latest version: 10.6.0(jiti@2.7.0) @@ -97,10 +100,10 @@ importers: version: 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) vite: specifier: latest - version: 8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0) + version: 8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) vite-plugin-svgr: specifier: ^5.2.0 - version: 5.2.0(typescript@6.0.3)(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)) + version: 5.2.0(typescript@6.0.3)(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) packages: '@babel/code-frame@7.29.7': @@ -1895,10 +1898,10 @@ packages: integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, } - '@types/node@26.1.0': + '@types/node@26.1.1': resolution: { - integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==, + integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==, } '@types/react-dom@19.2.3': @@ -4782,6 +4785,15 @@ packages: } engines: { node: '>= 0.8' } + vaul@1.1.2: + resolution: + { + integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vite-plugin-svgr@5.2.0: resolution: { @@ -6264,12 +6276,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 - '@tailwindcss/vite@4.3.2(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.2(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.2 '@tailwindcss/oxide': 4.3.2 tailwindcss: 4.3.2 - vite: 8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) '@tanstack/query-core@5.101.1': {} @@ -6297,7 +6309,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@26.1.0': + '@types/node@26.1.1': dependencies: undici-types: 8.3.0 @@ -6402,10 +6414,10 @@ snapshots: '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.3(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.3(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) accepts@2.0.0: dependencies: @@ -7979,18 +7991,27 @@ snapshots: vary@1.1.2: {} - vite-plugin-svgr@5.2.0(typescript@6.0.3)(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)): + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vite-plugin-svgr@5.2.0(typescript@6.0.3)(vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@rollup/pluginutils': 5.4.0 '@svgr/core': 8.1.0(typescript@6.0.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@6.0.3)) - vite: 8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.3(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -7998,7 +8019,7 @@ snapshots: rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.1.1 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 From dcec5ed6d128d39314c6025a3e43f196abfa1873 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:31:45 +0900 Subject: [PATCH 03/12] =?UTF-8?q?style=20:=20SortTabs=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/pin/components/SortTabs.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/features/pin/components/SortTabs.tsx b/src/features/pin/components/SortTabs.tsx index 1cf8a56b..d176bd3b 100644 --- a/src/features/pin/components/SortTabs.tsx +++ b/src/features/pin/components/SortTabs.tsx @@ -4,7 +4,6 @@ import type { PinSort } from '@/types/pin'; type SortTabsProps = { value: PinSort; onChange: (value: PinSort) => void; - className?: string; }; const OPTIONS: { value: PinSort; label: string }[] = [ @@ -14,7 +13,7 @@ const OPTIONS: { value: PinSort; label: string }[] = [ export function SortTabs({ value, onChange }: SortTabsProps) { return ( -
+
{OPTIONS.map((option) => { const selected = value === option.value; @@ -22,12 +21,11 @@ export function SortTabs({ value, onChange }: SortTabsProps) { ); From f0598b194f5eb5cf2a76dfde849365eb35e58331 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:05:06 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat=20:=20=ED=95=98=ED=8A=B8,=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=95=84=EC=9D=B4=EC=BD=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 --- src/assets/icons/bookmark.svg | 3 +++ src/assets/icons/heart.svg | 3 +++ src/assets/icons/next.svg | 3 +++ src/features/pin/components/PinListSheet.tsx | 11 ++++++----- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/assets/icons/bookmark.svg create mode 100644 src/assets/icons/heart.svg create mode 100644 src/assets/icons/next.svg diff --git a/src/assets/icons/bookmark.svg b/src/assets/icons/bookmark.svg new file mode 100644 index 00000000..7d4e8d7c --- /dev/null +++ b/src/assets/icons/bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/heart.svg b/src/assets/icons/heart.svg new file mode 100644 index 00000000..69da18c5 --- /dev/null +++ b/src/assets/icons/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/next.svg b/src/assets/icons/next.svg new file mode 100644 index 00000000..7bdf55d1 --- /dev/null +++ b/src/assets/icons/next.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/pin/components/PinListSheet.tsx b/src/features/pin/components/PinListSheet.tsx index a0b601ec..b2e8a716 100644 --- a/src/features/pin/components/PinListSheet.tsx +++ b/src/features/pin/components/PinListSheet.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { BottomSheet } from '@/components/ui/BottomSheet'; -import { PinSongCard } from '@/features/pin/components/PinSongCard'; +import { PinCard } from '@/features/pin/components/PinCard'; import { SortTabs } from '@/features/pin/components/SortTabs'; -import { Bookmark } from '@/assets/icons'; +import BookmarkIcon from '@/assets/icons/bookmark.svg?react'; import type { PinSong, PinSort, PlaceInfo } from '@/types/pin'; type PinListSheetProps = { @@ -40,6 +40,7 @@ export function PinListSheet({ return (
+ {/* 지도 부분 onclick시 바텀시트 열기 */}

PIN 목록 바텀시트 미리보기

@@ -77,10 +78,10 @@ export function PinListSheet({ -
    +
      {pins.map((pin) => (
    • - onPinClick?.(pin)} /> + onPinClick?.(pin)} />
    • ))}
    From 2316193810159c56df779c25d568b4a0c6024b10 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:25:28 +0900 Subject: [PATCH 06/12] =?UTF-8?q?style=20:=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=20&=20=ED=81=AC=EA=B8=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index 17785412..0d4daf2b 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Drawer as DrawerPrimitive } from 'vaul'; -import { ChevronLeft, X } from 'lucide-react'; +import BackIcon from '@/assets/icons/back.svg?react'; +import CloseIcon from '@/assets/icons/close.svg?react'; import { cn } from '@/lib/utils'; @@ -185,7 +186,7 @@ function BottomSheetFullPageNav({ className }: FullPageNavProps) { onClick={collapse} className="flex size-10 items-center justify-center text-grayscale-30" > - +
); From e8086c9e81a2b8571045a851ec74e4ccb935315a Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:14:58 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat=20:=20=EB=85=B8=EB=9E=98=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=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 --- src/features/pin/components/PinCard.tsx | 4 +- src/features/pin/components/PinListSheet.tsx | 8 +- src/features/pin/components/SongBottom.tsx | 77 ++++++++++++++++++++ src/features/pin/components/SongCard.tsx | 22 ++++++ src/types/pin.ts | 9 ++- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/features/pin/components/SongBottom.tsx create mode 100644 src/features/pin/components/SongCard.tsx diff --git a/src/features/pin/components/PinCard.tsx b/src/features/pin/components/PinCard.tsx index 1e89b106..32a27efc 100644 --- a/src/features/pin/components/PinCard.tsx +++ b/src/features/pin/components/PinCard.tsx @@ -1,11 +1,11 @@ import { cn } from '@/lib/utils'; -import type { PinSong } from '@/types/pin'; +import type { Pin } from '@/types/pin'; import NextIcon from '@/assets/icons/next.svg?react'; import HeartIcon from '@/assets/icons/heart.svg?react'; type PingCardProps = { - pin: PinSong; + pin: Pin; onClick?: () => void; className?: string; }; diff --git a/src/features/pin/components/PinListSheet.tsx b/src/features/pin/components/PinListSheet.tsx index b2e8a716..38a8f43b 100644 --- a/src/features/pin/components/PinListSheet.tsx +++ b/src/features/pin/components/PinListSheet.tsx @@ -4,12 +4,12 @@ import { BottomSheet } from '@/components/ui/BottomSheet'; import { PinCard } from '@/features/pin/components/PinCard'; import { SortTabs } from '@/features/pin/components/SortTabs'; import BookmarkIcon from '@/assets/icons/bookmark.svg?react'; -import type { PinSong, PinSort, PlaceInfo } from '@/types/pin'; +import type { Pin, PinSort, PlaceInfo } from '@/types/pin'; type PinListSheetProps = { place?: PlaceInfo; - pins?: PinSong[]; - onPinClick?: (pin: PinSong) => void; + pins?: Pin[]; + onPinClick?: (pin: Pin) => void; }; const MOCK_PLACE: PlaceInfo = { @@ -19,7 +19,7 @@ const MOCK_PLACE: PlaceInfo = { distance: 100, }; -const MOCK_PINS: PinSong[] = [ +const MOCK_PINS: Pin[] = [ { id: '1', title: 'LOVE ATTACK', diff --git a/src/features/pin/components/SongBottom.tsx b/src/features/pin/components/SongBottom.tsx new file mode 100644 index 00000000..c3def25f --- /dev/null +++ b/src/features/pin/components/SongBottom.tsx @@ -0,0 +1,77 @@ +import { useMemo, useState } from 'react'; + +import { BottomSheet } from '@/components/ui/BottomSheet'; +import { SearchInput } from '@/components/ui/SearchInput'; +import { SongCard } from '@/features/pin/components/SongCard'; +import type { Song } from '@/types/pin'; + +type SongCardProps = { + songs?: Song[]; + onSelect?: (song: Song) => void; +}; + +const MOCK_SONGS: Song[] = [ + { id: '1', title: 'Hype Boy', artist: 'NewJeans' }, + { id: '2', title: 'Hype Boy (250 Remix)', artist: 'NewJeans' }, + { id: '3', title: 'Hype Boy (Instrumental)', artist: 'NewJeans' }, + { id: '4', title: 'Attention', artist: 'NewJeans' }, +]; + +export default function SongPage({ songs = MOCK_SONGS, onSelect }: SongCardProps) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + + const filteredSongs = useMemo(() => { + const keyword = query.trim().toLowerCase(); + if (!keyword) return songs; + + return songs.filter( + (song) => + song.title.toLowerCase().includes(keyword) || song.artist.toLowerCase().includes(keyword), + ); + }, [query, songs]); + + return ( +
+ {/* 지도 부분 onclick시 바텀시트 열기 */} +

노래 선택 바텀시트 미리보기

+ + + + setOpen(false)}> + +

노래 선택하기

+ + setQuery(event.target.value)} + placeholder="노래를 검색하세요" + /> +
+ + +
    + {filteredSongs.map((song) => ( +
  • + { + onSelect?.(song); + setOpen(false); + }} + /> +
  • + ))} +
+
+
+
+ ); +} diff --git a/src/features/pin/components/SongCard.tsx b/src/features/pin/components/SongCard.tsx new file mode 100644 index 00000000..83b3ca7c --- /dev/null +++ b/src/features/pin/components/SongCard.tsx @@ -0,0 +1,22 @@ +import type { Song } from '@/types/pin'; + +type SongCardProps = { + song: Song; + onClick?: () => void; +}; + +export function SongCard({ song, onClick }: SongCardProps) { + return ( + + ); +} diff --git a/src/types/pin.ts b/src/types/pin.ts index e11fc3e9..662850af 100644 --- a/src/types/pin.ts +++ b/src/types/pin.ts @@ -7,12 +7,17 @@ export type PlaceInfo = { distance: number; }; -export type PinSong = { +export type Pin = { id: string; title: string; artist: string; pinCount: number; likeCount?: number; liked?: boolean; - thumbnailUrl?: string; +}; + +export type Song = { + id: string; + title: string; + artist: string; }; From bc44101193b946d76686d8dcff2f8f50520712e9 Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:55:14 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix=20:=20=EC=BD=94=EB=93=9C=EB=A0=88?= =?UTF-8?q?=EB=B9=97=20=EC=A0=9C=EC=95=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/pin/components/PinCard.tsx | 1 - src/features/pin/components/SongBottom.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/pin/components/PinCard.tsx b/src/features/pin/components/PinCard.tsx index 32a27efc..ce9311bc 100644 --- a/src/features/pin/components/PinCard.tsx +++ b/src/features/pin/components/PinCard.tsx @@ -7,7 +7,6 @@ import HeartIcon from '@/assets/icons/heart.svg?react'; type PingCardProps = { pin: Pin; onClick?: () => void; - className?: string; }; export function PinCard({ pin, onClick }: PingCardProps) { diff --git a/src/features/pin/components/SongBottom.tsx b/src/features/pin/components/SongBottom.tsx index c3def25f..11821e0f 100644 --- a/src/features/pin/components/SongBottom.tsx +++ b/src/features/pin/components/SongBottom.tsx @@ -65,6 +65,7 @@ export default function SongPage({ songs = MOCK_SONGS, onSelect }: SongCardProps onClick={() => { onSelect?.(song); setOpen(false); + setQuery(''); }} /> From 6e379e44668b3a7877e554b2245aa53f890e2b6e Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:14:59 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat=20:=20BottomSheetTitle=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/BottomSheet.tsx | 8 ++++++++ src/features/pin/components/PinListSheet.tsx | 4 +++- src/features/pin/components/SongBottom.tsx | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/ui/BottomSheet.tsx b/src/components/ui/BottomSheet.tsx index 0d4daf2b..e00b729a 100644 --- a/src/components/ui/BottomSheet.tsx +++ b/src/components/ui/BottomSheet.tsx @@ -153,11 +153,18 @@ function BottomSheet({ type SectionProps = React.ComponentProps<'div'>; +type TitleProps = React.ComponentProps; + /** 시트 상단 고정 영역. 제목·검색창 등 스크롤되지 않는 헤더에 사용한다. */ function BottomSheetHeader({ className, ...props }: SectionProps) { return
; } +/** 시트 접근성 제목. Header 안의 h2를 이 컴포넌트로 감싼다. */ +function BottomSheetTitle({ className, ...props }: TitleProps) { + return ; +} + /** 시트 본문 스크롤 영역. 리스트·카드 등 긴 콘텐츠를 담는다. */ function BottomSheetContent({ className, ...props }: SectionProps) { return ( @@ -201,6 +208,7 @@ function BottomSheetFullPageNav({ className }: FullPageNavProps) { } BottomSheet.Header = BottomSheetHeader; +BottomSheet.Title = BottomSheetTitle; BottomSheet.Content = BottomSheetContent; BottomSheet.FullPageNav = BottomSheetFullPageNav; diff --git a/src/features/pin/components/PinListSheet.tsx b/src/features/pin/components/PinListSheet.tsx index 38a8f43b..7fc1c472 100644 --- a/src/features/pin/components/PinListSheet.tsx +++ b/src/features/pin/components/PinListSheet.tsx @@ -57,7 +57,9 @@ export function PinListSheet({
-

{place.name}

+ + {place.name} +

{place.creatorName} 님이 생성한 PIN · {place.distance}m diff --git a/src/features/pin/components/SongBottom.tsx b/src/features/pin/components/SongBottom.tsx index 11821e0f..bcfd8353 100644 --- a/src/features/pin/components/SongBottom.tsx +++ b/src/features/pin/components/SongBottom.tsx @@ -46,7 +46,9 @@ export default function SongPage({ songs = MOCK_SONGS, onSelect }: SongCardProps setOpen(false)}> -

노래 선택하기

+ + 노래 선택하기 + Date: Mon, 13 Jul 2026 12:15:26 +0900 Subject: [PATCH 10/12] =?UTF-8?q?style=20:=20Topbar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=97=AC=EB=B0=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/TopBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/TopBar.tsx b/src/components/ui/TopBar.tsx index 128fa210..48d7aa3c 100644 --- a/src/components/ui/TopBar.tsx +++ b/src/components/ui/TopBar.tsx @@ -25,7 +25,7 @@ export function TopBar({ className, }: TopBarProps) { return ( -
+
{onBack && ( - -
- ); + return ; } BottomSheet.Header = BottomSheetHeader; From a053aeebbd360a202d9b3f73fdd2ca7b237594aa Mon Sep 17 00:00:00 2001 From: kimtaewoo <70637743+kim3360@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:41:20 +0900 Subject: [PATCH 12/12] =?UTF-8?q?style=20:=20Topbar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/TopBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/TopBar.tsx b/src/components/ui/TopBar.tsx index 48d7aa3c..128fa210 100644 --- a/src/components/ui/TopBar.tsx +++ b/src/components/ui/TopBar.tsx @@ -25,7 +25,7 @@ export function TopBar({ className, }: TopBarProps) { return ( -
+
{onBack && (