#281 (리뷰/메모 UI): - Stars 컴포넌트 신규 (lib 분리 가능한 공통 별점) — 0.5 단위 절반 채우기 시각 구분 - ReviewSection/MemoSection의 StarDisplay 제거 → 공통 Stars 사용 (시각 일관성) - StarSelector: role='radiogroup'/role='radio' + aria-checked, 44×44px 터치 영역, 반쪽 별 '⯨' 표시로 시각 차별화 - ReviewSection/MemoSection: API 실패 try/catch + alert 사용자 피드백 - MyReviewsList: Math.round 별점 → Stars 0.5단위 정확 렌더 #283 (로그인 메뉴): - LoginMenu: useEscapeKey + useFocusTrap + useBodyScrollLock 적용 - role='dialog' / aria-modal / aria-labelledby / aria-label='로그인 창 닫기' - onError 콘솔만 → 인라인 role='alert' 메시지로 사용자 피드백 - max-w-xs → max-w-sm (위젯 260px + 패딩 24px = 308px 안전 수용) 후속 분리: - #343 (next/image + ARIA Tabs + Stars 테스트) - #344 (z-index 토큰 + i18n) Refs: #281 #283
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { Review, Memo } from "@/lib/api";
|
|
import Icon from "@/components/Icon";
|
|
import Stars from "@/components/Stars";
|
|
|
|
interface MyReview extends Review {
|
|
restaurant_id: string;
|
|
restaurant_name: string | null;
|
|
}
|
|
|
|
interface MyMemo extends Memo {
|
|
restaurant_name: string | null;
|
|
}
|
|
|
|
interface MyReviewsListProps {
|
|
reviews: MyReview[];
|
|
memos: MyMemo[];
|
|
onClose: () => void;
|
|
onSelectRestaurant: (restaurantId: string) => void;
|
|
}
|
|
|
|
export default function MyReviewsList({
|
|
reviews,
|
|
memos,
|
|
onClose,
|
|
onSelectRestaurant,
|
|
}: MyReviewsListProps) {
|
|
const [tab, setTab] = useState<"reviews" | "memos">("reviews");
|
|
|
|
return (
|
|
<div className="p-4 space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="font-bold text-lg">내 기록</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<Icon name="close" size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex gap-1 border-b">
|
|
<button
|
|
onClick={() => setTab("reviews")}
|
|
className={`px-3 py-1.5 text-sm font-medium border-b-2 transition-colors ${
|
|
tab === "reviews"
|
|
? "border-brand-500 text-brand-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
}`}
|
|
>
|
|
<Icon name="rate_review" size={14} className="mr-1" />
|
|
리뷰 ({reviews.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setTab("memos")}
|
|
className={`px-3 py-1.5 text-sm font-medium border-b-2 transition-colors ${
|
|
tab === "memos"
|
|
? "border-brand-500 text-brand-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
}`}
|
|
>
|
|
<Icon name="edit_note" size={14} className="mr-1" />
|
|
메모 ({memos.length})
|
|
</button>
|
|
</div>
|
|
|
|
{tab === "reviews" ? (
|
|
reviews.length === 0 ? (
|
|
<p className="text-sm text-gray-500 py-8 text-center">
|
|
아직 작성한 리뷰가 없습니다.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{reviews.map((r) => (
|
|
<button
|
|
key={r.id}
|
|
onClick={() => onSelectRestaurant(r.restaurant_id)}
|
|
className="w-full text-left border rounded-lg p-3 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="font-semibold text-sm truncate">
|
|
{r.restaurant_name || "알 수 없는 식당"}
|
|
</span>
|
|
<span className="text-sm shrink-0 ml-2 flex items-center gap-1">
|
|
<Stars rating={r.rating} />
|
|
<span className="text-gray-500">{r.rating}</span>
|
|
</span>
|
|
</div>
|
|
{r.review_text && (
|
|
<p className="text-xs text-gray-600 line-clamp-2">
|
|
{r.review_text}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-1.5 text-[10px] text-gray-400">
|
|
{r.visited_at && <span>방문: {r.visited_at}</span>}
|
|
{r.created_at && <span>{r.created_at.slice(0, 10)}</span>}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
) : (
|
|
memos.length === 0 ? (
|
|
<p className="text-sm text-gray-500 py-8 text-center">
|
|
아직 작성한 메모가 없습니다.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{memos.map((m) => (
|
|
<button
|
|
key={m.id}
|
|
onClick={() => onSelectRestaurant(m.restaurant_id)}
|
|
className="w-full text-left border border-brand-200 rounded-lg p-3 bg-brand-50/30 hover:bg-brand-50 transition-colors"
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="font-semibold text-sm truncate">
|
|
{m.restaurant_name || "알 수 없는 식당"}
|
|
</span>
|
|
{m.rating && (
|
|
<span className="text-sm shrink-0 ml-2 flex items-center gap-1">
|
|
<Stars rating={m.rating} />
|
|
<span className="text-gray-500">{m.rating}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
{m.memo_text && (
|
|
<p className="text-xs text-gray-600 line-clamp-2">
|
|
{m.memo_text}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-1.5 text-[10px] text-gray-400">
|
|
{m.visited_at && <span>방문: {m.visited_at}</span>}
|
|
<span className="text-brand-400">비공개</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
}
|