Material Symbols 아이콘 전환 + 로고 이미지 적용 + 테이블링 이름 유사도 체크

- 전체 인라인 SVG를 Google Material Symbols Rounded로 교체
- Icon 컴포넌트 추가, cuisine-icons 매핑 리팩토링
- Tasteby 핀 로고 이미지 적용 (라이트/다크 버전)
- 테이블링/캐치테이블 이름 유사도 체크 및 리셋 API 추가
- 어드민 페이지 리셋 버튼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-12 12:55:04 +09:00
parent 4f8b4f435e
commit 824c171158
22 changed files with 327 additions and 170 deletions

View File

@@ -0,0 +1,23 @@
"use client";
interface IconProps {
name: string;
size?: number;
filled?: boolean;
className?: string;
}
/**
* Material Symbols Rounded icon wrapper.
* Usage: <Icon name="search" size={20} />
*/
export default function Icon({ name, size = 20, filled, className = "" }: IconProps) {
return (
<span
className={`material-symbols-rounded ${filled ? "filled" : ""} ${className}`}
style={{ fontSize: size }}
>
{name}
</span>
);
}

View File

@@ -10,6 +10,7 @@ import {
} from "@vis.gl/react-google-maps";
import type { Restaurant } from "@/lib/api";
import { getCuisineIcon } from "@/lib/cuisine-icons";
import Icon from "@/components/Icon";
const SEOUL_CENTER = { lat: 37.5665, lng: 126.978 };
const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || "";
@@ -124,7 +125,7 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
textDecoration: isClosed ? "line-through" : "none",
}}
>
<span style={{ marginRight: 3 }}>{getCuisineIcon(r.cuisine_type)}</span>
<span className="material-symbols-rounded" style={{ fontSize: 14, marginRight: 3, verticalAlign: "middle", color: "#E8720C" }}>{getCuisineIcon(r.cuisine_type)}</span>
{r.name}
</div>
<div
@@ -149,7 +150,7 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
>
<div style={{ backgroundColor: "#ffffff", color: "#171717", colorScheme: "light" }} className="max-w-xs p-1">
<div className="flex items-center gap-2">
<h3 className="font-bold text-base" style={{ color: "#171717" }}>{getCuisineIcon(infoTarget.cuisine_type)} {infoTarget.name}</h3>
<h3 className="font-bold text-base" style={{ color: "#171717" }}><span className="material-symbols-rounded" style={{ fontSize: 18, verticalAlign: "middle", color: "#E8720C", marginRight: 4 }}>{getCuisineIcon(infoTarget.cuisine_type)}</span>{infoTarget.name}</h3>
{infoTarget.business_status === "CLOSED_PERMANENTLY" && (
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded text-[10px] font-semibold"></span>
)}
@@ -234,9 +235,7 @@ export default function MapView({ restaurants, selected, onSelectRestaurant, onB
className="absolute top-2 right-2 w-9 h-9 bg-surface rounded-lg shadow-md flex items-center justify-center text-gray-600 dark:text-gray-300 hover:text-brand-500 dark:hover:text-brand-400 transition-colors z-10"
title="내 위치"
>
<svg viewBox="0 0 24 24" className="w-5 h-5 fill-current">
<path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3A8.994 8.994 0 0013 3.06V1h-2v2.06A8.994 8.994 0 003.06 11H1v2h2.06A8.994 8.994 0 0011 20.94V23h2v-2.06A8.994 8.994 0 0020.94 13H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>
</svg>
<Icon name="my_location" size={20} />
</button>
)}
{channelNames.length > 0 && (

View File

@@ -5,6 +5,7 @@ import { api, getToken } from "@/lib/api";
import type { Restaurant, VideoLink } from "@/lib/api";
import ReviewSection from "@/components/ReviewSection";
import { RestaurantDetailSkeleton } from "@/components/Skeleton";
import Icon from "@/components/Icon";
interface RestaurantDetailProps {
restaurant: Restaurant;
@@ -60,7 +61,7 @@ export default function RestaurantDetail({
}`}
title={favorited ? "찜 해제" : "찜하기"}
>
{favorited ? "♥" : "♡"}
<Icon name="favorite" size={20} filled={favorited} />
</button>
)}
{restaurant.business_status === "CLOSED_PERMANENTLY" && (
@@ -78,7 +79,7 @@ export default function RestaurantDetail({
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-xl leading-none"
>
x
<Icon name="close" size={18} />
</button>
</div>
@@ -197,7 +198,7 @@ export default function RestaurantDetail({
<div className="flex items-center gap-2 mb-1">
{v.channel_name && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400 rounded text-[10px] font-semibold">
<span className="text-[9px]"></span>
<Icon name="play_circle" size={11} filled className="text-red-400" />
{v.channel_name}
</span>
)}
@@ -213,9 +214,7 @@ export default function RestaurantDetail({
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-red-600 dark:text-red-400 hover:underline"
>
<svg viewBox="0 0 24 24" className="w-4 h-4 flex-shrink-0 fill-current" aria-hidden="true">
<path d="M23.5 6.2c-.3-1-1-1.8-2-2.1C19.6 3.5 12 3.5 12 3.5s-7.6 0-9.5.6c-1 .3-1.7 1.1-2 2.1C0 8.1 0 12 0 12s0 3.9.5 5.8c.3 1 1 1.8 2 2.1 1.9.6 9.5.6 9.5.6s7.6 0 9.5-.6c1-.3 1.7-1.1 2-2.1.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8zM9.5 15.5V8.5l6.3 3.5-6.3 3.5z"/>
</svg>
<Icon name="play_circle" size={16} filled className="flex-shrink-0" />
{v.title}
</a>
{v.foods_mentioned.length > 0 && (

View File

@@ -2,6 +2,7 @@
import type { Restaurant } from "@/lib/api";
import { getCuisineIcon } from "@/lib/cuisine-icons";
import Icon from "@/components/Icon";
import { RestaurantListSkeleton } from "@/components/Skeleton";
interface RestaurantListProps {
@@ -45,7 +46,7 @@ export default function RestaurantList({
>
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
<span className="mr-1">{getCuisineIcon(r.cuisine_type)}</span>
<Icon name={getCuisineIcon(r.cuisine_type)} size={16} className="mr-0.5 text-brand-600" />
{r.name}
</h4>
{r.rating && (
@@ -87,7 +88,7 @@ export default function RestaurantList({
key={ch}
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 rounded-full text-[10px] font-medium"
>
<svg className="w-2.5 h-2.5 shrink-0 text-red-400" viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
<Icon name="play_circle" size={11} filled className="shrink-0 text-red-400" />
{ch}
</span>
))}

View File

@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import Icon from "@/components/Icon";
interface SearchBarProps {
onSearch: (query: string, mode: "keyword" | "semantic" | "hybrid") => void;
@@ -19,18 +20,9 @@ export default function SearchBar({ onSearch, isLoading }: SearchBarProps) {
return (
<form onSubmit={handleSubmit} className="relative">
<svg
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500 pointer-events-none"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
<Icon name="search" size={16} />
</span>
<input
type="text"
value={query}