[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:
@@ -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="음식 필터 초기화"
|
||||
>
|
||||
|
||||
@@ -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}` : ""}`);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user