[Developer] #533 언급 적은 식당 제외 필터 (min_mentions + video_count)

- backend: findAll 응답에 video_count(strong/unknown 링크수) + min_mentions 서버필터
  - Restaurant.videoCount, RestaurantMapper(xml/iface), Service, Controller(+cache key)
- frontend: Restaurant.video_count 타입 + 데스크톱 툴바 "언급"(2/3/5회+) 클라 필터
- 검증: min_mentions=2→116, =3→36 (위반 0). 빌드/tsc 통과
- 설계서 docs/design/533-min-mentions-filter/README.md

Refs #533
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-06-28 16:23:46 +09:00
parent 4760b5a284
commit 7639fd009d
9 changed files with 100 additions and 10 deletions

View File

@@ -196,6 +196,7 @@ export default function Home() {
const [channelFilter, setChannelFilter] = useState("");
const [cuisineFilter, setCuisineFilter] = useState("");
const [priceFilter, setPriceFilter] = useState("");
const [minMentions, setMinMentions] = useState(0); // #533 — 의미있는 언급 N회 미만 식당 제외 (0=전체)
const [viewMode, setViewMode] = useState<"map" | "list">("list");
const [mobileTab, setMobileTab] = useState<"home" | "list" | "nearby" | "favorites" | "profile">("home");
const [showMobileFilters, setShowMobileFilters] = useState(false);
@@ -248,6 +249,7 @@ export default function Home() {
if (channelFilter && !(r.channels || []).includes(channelFilter)) return false;
if (cuisineFilter && !matchCuisineFilter(r.cuisine_type, cuisineFilter)) return false;
if (priceFilter && !matchPriceGroup(r.price_range, priceFilter)) return false;
if (minMentions > 0 && (r.video_count ?? 0) < minMentions) return false;
if (countryFilter) {
const parsed = parseRegion(r.region);
if (!parsed || parsed.country !== countryFilter) return false;
@@ -271,7 +273,7 @@ export default function Home() {
if (da !== db) return da - db;
return (b.rating || 0) - (a.rating || 0);
});
}, [restaurants, isSearchResult, channelFilter, cuisineFilter, priceFilter, countryFilter, cityFilter, districtFilter, boundsFilterOn, mapBounds, userLoc]);
}, [restaurants, isSearchResult, channelFilter, cuisineFilter, priceFilter, minMentions, countryFilter, cityFilter, districtFilter, boundsFilterOn, mapBounds, userLoc]);
// FilterSheet option builders
const cuisineOptions = useMemo<FilterOption[]>(() => {
@@ -366,6 +368,7 @@ export default function Home() {
setChannelFilter("");
setCuisineFilter("");
setPriceFilter("");
setMinMentions(0);
setCountryFilter("");
setCityFilter("");
setDistrictFilter("");
@@ -477,6 +480,7 @@ export default function Home() {
setChannelFilter("");
setCuisineFilter("");
setPriceFilter("");
setMinMentions(0);
setCountryFilter("");
setCityFilter("");
setDistrictFilter("");
@@ -818,9 +822,23 @@ export default function Home() {
<option key={g.label} value={g.label}>{g.label}</option>
))}
</select>
{(cuisineFilter || priceFilter) && (
<div className="w-px h-3 bg-gray-200 dark:bg-gray-700" />
<select
value={minMentions}
onChange={(e) => { setMinMentions(Number(e.target.value)); if (Number(e.target.value)) setBoundsFilterOn(false); }}
className={`bg-transparent border-none outline-none cursor-pointer pr-1 ${
minMentions > 0 ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
}`}
title="영상에서 충분히 언급된(관련도 높은) 식당만 보기"
>
<option value={0}></option>
<option value={2}>2+</option>
<option value={3}>3+</option>
<option value={5}>5+</option>
</select>
{(cuisineFilter || priceFilter || minMentions > 0) && (
<button
onClick={() => { setCuisineFilter(""); setPriceFilter(""); }}
onClick={() => { setCuisineFilter(""); setPriceFilter(""); setMinMentions(0); }}
className="p-1.5 -mr-1 text-gray-400 hover:text-brand-500 transition-colors touch-manipulation"
title="음식 필터 초기화"
>

View File

@@ -51,6 +51,8 @@ export interface Restaurant {
website: string | null;
channels?: string[];
foods_mentioned?: string[];
// #533 — 의미있는(strong/unknown) 영상 언급 수
video_count?: number | null;
// #322 LLM 검증
hidden?: boolean;
hidden_reason?: string | null;
@@ -168,6 +170,7 @@ export const api = {
channel?: string;
limit?: number;
offset?: number;
min_mentions?: number;
}) {
const sp = new URLSearchParams();
if (params?.cuisine) sp.set("cuisine", params.cuisine);
@@ -175,6 +178,7 @@ export const api = {
if (params?.channel) sp.set("channel", params.channel);
if (params?.limit) sp.set("limit", String(params.limit));
if (params?.offset) sp.set("offset", String(params.offset));
if (params?.min_mentions) sp.set("min_mentions", String(params.min_mentions));
const qs = sp.toString();
return fetchApi<Restaurant[]>(`/api/restaurants${qs ? `?${qs}` : ""}`);
},