Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 25 additions & 21 deletions src/features/pin/components/PinCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { cn } from '@/lib/utils';
import type { Pin } from '@/features/pin/types';

import NextIcon from '@/assets/icons/next.svg?react';
import HeartIcon from '@/assets/icons/heart.svg?react';
import NextIcon from '@/assets/icons/next.svg?react';
import type { Pin } from '@/features/pin/types';
import { cn } from '@/lib/utils';

type PinCardProps = {
pin: Pin;
Expand All @@ -16,30 +15,35 @@ export function PinCard({ pin, onClick }: PinCardProps) {
<button
type="button"
onClick={onClick}
className="flex w-full rounded-xl bg-pli-black-85 p-4.5 justify-between"
className="flex w-full items-center justify-between rounded-xl bg-pli-black-85 p-4.5 text-left"
>
<div className="flex flex-col gap-4">
<div className="flex gap-4">
<div className="size-13 rounded-lg bg-grayscale-0" />
<div className="flex min-w-0 flex-1 flex-col gap-4">
<div className="flex min-w-0 items-center gap-4">
<div className="size-13 shrink-0 rounded-lg bg-grayscale-0" aria-hidden />

<div className="flex flex-col gap-[3px] items-start">
<p className="truncate body-17-m text-grayscale-100">{title}</p>
<p className="truncate etc-15-r text-grayscale-400">{artist}</p>
<div className="flex min-w-0 flex-col items-start gap-[3px]">
<p className="w-full truncate body-15-sb text-grayscale-100">{title}</p>
<p className="w-full truncate body-15-r text-grayscale-500">{artist}</p>
</div>
</div>

<div className="flex items-center gap-1 body-15-r text-grayscale-400">
<HeartIcon
className={cn('size-4', liked ? 'fill-red text-red' : 'text-grayscale-400')}
aria-hidden
/>
{likeCount ?? 0}
</div>
{likeCount !== undefined ? (
<div className="flex items-center gap-1 body-15-r text-grayscale-300">
<HeartIcon
className={cn(
'size-[18px] shrink-0',
liked ? 'fill-red text-red' : 'text-grayscale-300',
)}
aria-hidden
/>
<span>{likeCount}</span>
</div>
) : null}
</div>

<div className="flex items-center gap-1 etc-13-r text-grayscale-400">
{pinCount}명이 PIN
<NextIcon className="size-4" aria-hidden />
<div className="ml-3 flex shrink-0 items-center gap-1 etc-13-r text-grayscale-300">
{pinCount}명이 등록
<NextIcon className="size-5" aria-hidden />
</div>
</button>
);
Expand Down
109 changes: 82 additions & 27 deletions src/features/pin/components/PinListSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useMemo, useState } from 'react';

import { BottomSheet } from '@/components/ui/BottomSheet';
import BookmarkIcon from '@/assets/icons/bookmark.svg?react';
import { BottomSheet, useBottomSheet } 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 { Pin, PinSort, PlaceInfo } from '@/features/pin/types';
import { cn } from '@/lib/utils';

type PinListSheetProps = {
open: boolean;
Expand All @@ -14,55 +15,109 @@ type PinListSheetProps = {
onPinClick?: (pin: Pin) => void;
};

export function PinListSheet({ open, onClose, place, pins, onPinClick }: PinListSheetProps) {
const [sort, setSort] = useState<PinSort>('popular');
function formatDistance(distance: number) {
const normalizedDistance = Math.max(0, distance);

// Pin에는 생성 시각이 없어 latest는 원본 배열(등록 순)을 역순으로 처리한다.
const sortedPins = useMemo(() => {
if (sort === 'latest') {
return [...pins].reverse();
}
return [...pins].sort((a, b) => (b.likeCount ?? 0) - (a.likeCount ?? 0));
}, [pins, sort]);
if (normalizedDistance >= 1000) {
const kilometers = normalizedDistance / 1000;
return {
value: Number.isInteger(kilometers) ? String(kilometers) : kilometers.toFixed(1),
unit: 'km',
};
}

return { value: String(Math.round(normalizedDistance)), unit: 'm' };
}

type PinListContentProps = {
place: PlaceInfo;
pins: Pin[];
sort: PinSort;
onSortChange: (sort: PinSort) => void;
onPinClick?: (pin: Pin) => void;
};

function PinListContent({ place, pins, sort, onSortChange, onPinClick }: PinListContentProps) {
const { isFullPage } = useBottomSheet();
const distance = formatDistance(place.distance);

return (
<BottomSheet open={open} onClose={onClose}>
<BottomSheet.FullPageNav />
<>
<BottomSheet.Header className={cn('px-4', isFullPage ? 'mt-0' : 'mt-[26.75px]')}>
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex flex-col items-start gap-2">
{place.isMine ? (
<div className="flex items-center justify-center gap-1.5 rounded-[25px] bg-pli-black-75 px-2 py-1">
<span className="size-1.5 rounded-full bg-neon-2" aria-hidden />
<span className="etc-13-r text-neon-2">MY</span>
</div>
) : null}

<div className="min-w-0">
<BottomSheet.Title className="block truncate head-24-sb text-grayscale-100">
{place.name}
</BottomSheet.Title>
{place.address ? (
<p className="truncate body-15-r text-grayscale-500">{place.address}</p>
) : null}
</div>
</div>

<BottomSheet.Header className="mt-[26.75px] px-4 ">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-2">
<BottomSheet.Title className="head-24-sb text-grayscale-100">
{place.name}
</BottomSheet.Title>
<p className="body-15-m text-grayscale-500">
<span className="body-15-r text-grayscale-200">{place.creatorName}</span> 님이 생성한
PIN · <span className="text-red">{place.distance}</span>m
<p className="truncate body-15-m text-grayscale-400">
<span className="body-15-r text-grayscale-200">{place.creatorName}</span> 님이 생성한 핀 ·{' '}
<span className="text-grayscale-300">{distance.value}</span>
{distance.unit}
</p>
</div>

<button
type="button"
aria-label="북마크"
className="flex size-11 items-center justify-center rounded-full bg-pli-black-75"
className="flex size-11 shrink-0 items-center justify-center rounded-full bg-pli-black-75"
>
<BookmarkIcon className="size-7" />
</button>
</div>

<div className="mt-6">
<SortTabs value={sort} onChange={setSort} />
<SortTabs value={sort} onChange={onSortChange} />
</div>
</BottomSheet.Header>

<BottomSheet.Content className="mt-5">
<ul className="flex flex-col gap-4.5">
{sortedPins.map((pin) => (
<BottomSheet.Content className="mt-5 px-4">
<ul className="flex flex-col gap-4">
{pins.map((pin) => (
<li key={pin.id}>
<PinCard pin={pin} onClick={() => onPinClick?.(pin)} />
</li>
))}
</ul>
</BottomSheet.Content>
</>
);
}

export function PinListSheet({ open, onClose, place, pins, onPinClick }: PinListSheetProps) {
const [sort, setSort] = useState<PinSort>('popular');

const sortedPins = useMemo(() => {
if (sort === 'latest') {
return [...pins].reverse();
}
return [...pins].sort((a, b) => (b.likeCount ?? 0) - (a.likeCount ?? 0));
}, [pins, sort]);

return (
<BottomSheet open={open} onClose={onClose}>
<BottomSheet.FullPageNav />
<PinListContent
place={place}
pins={sortedPins}
sort={sort}
onSortChange={setSort}
onPinClick={onPinClick}
/>
</BottomSheet>
);
}
14 changes: 10 additions & 4 deletions src/features/pin/components/SortTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ const OPTIONS: { value: PinSort; label: string }[] = [

export function SortTabs({ value, onChange }: SortTabsProps) {
return (
<div className="flex w-full h-10 rounded-xl bg-pli-black-75 px-[4.5px] py-1 gap-[3px]">
<div
role="group"
aria-label="PIN 정렬"
className="flex h-10 w-full gap-[3px] rounded-xl bg-pli-black-75 px-[4.5px] py-1"
>
{OPTIONS.map((option) => {
const selected = value === option.value;

return (
<button
key={option.value}
type="button"
aria-selected={selected}
aria-pressed={selected}
onClick={() => onChange(option.value)}
className={cn(
'flex flex-1 items-center justify-center rounded-[10px] bg-pli-black-75 body-15-m',
selected ? 'bg-grayscale-200 text-grayscale-1300' : 'text-grayscale-700 ',
'flex flex-1 items-center justify-center rounded-[10px] body-15-m',
selected
? 'bg-grayscale-200 text-grayscale-1300'
: 'bg-pli-black-75 text-grayscale-700',
)}
>
{option.label}
Expand Down
2 changes: 2 additions & 0 deletions src/features/pin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type PlaceInfo = {
name: string;
creatorName: string;
distance: number;
address?: string;
isMine?: boolean;
};

export type Pin = {
Expand Down
Loading