From 1815a4c4b2a2586448d0c16c1729dd29d36d919e Mon Sep 17 00:00:00 2001 From: joungmin Date: Mon, 27 Jul 2026 13:55:41 +0900 Subject: [PATCH] =?UTF-8?q?[QA]=20#533=20=EC=96=B8=EA=B8=89=20=EC=A0=81?= =?UTF-8?q?=EC=9D=80=20=EC=8B=9D=EB=8B=B9=20=EC=A0=9C=EC=99=B8=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=E2=80=94=20=EA=B2=80=EC=A6=9D=20=ED=86=B5=EA=B3=BC?= =?UTF-8?q?,=20=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설계서(docs/design/533-min-mentions-filter/README.md) 대비 구현 일치 확인 (backend RestaurantMapper video_count/min_mentions, frontend 필터 UI) - dev API 실측: min_mentions 0/1/2/3/5/100 전 구간 경계 위반 0건 (=2→116개, =3→36개, DISTINCT+서브쿼리 중복 없음, 기존 필터 조합 회귀 없음) - passesMinMentions()를 lib/filters.ts로 추출(동작 변경 없음)하고 frontend/__tests__/filters.test.ts 6케이스 추가(경계값/null 처리 등) - 백엔드 빌드, tsc, 프론트 전체 jest(6 suites, 32 tests) 통과 Refs #533 --- CHANGELOG.md | 11 +++++++++ frontend/__tests__/filters.test.ts | 36 ++++++++++++++++++++++++++++++ frontend/src/app/page.tsx | 3 ++- frontend/src/lib/filters.ts | 23 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 frontend/__tests__/filters.test.ts create mode 100644 frontend/src/lib/filters.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8b841..db98217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ ## 2026-07-27 +### ✅ [QA] #533 언급 적은 식당 제외 필터 — 검증 통과 +- 설계서(`docs/design/533-min-mentions-filter/README.md`)와 구현(commit 7639fd0) 일치 확인 + - 백엔드: `RestaurantMapper.xml findAll` 스칼라 서브쿼리(`video_count`) + `min_mentions` 필터, `DISTINCT` 조합에도 중복 없음 확인 + - 프론트: `page.tsx` `filteredRestaurants`에서 `video_count` 가드 적용, 데스크톱 툴바 "언급" 셀렉트(2/3/5회+) +- dev API 실측 검증: `min_mentions=0/1/2/3/5/100` 전 구간에서 경계 위반 0건 (=2→116개, =3→36개, =5→5개, =100→0개, 음수/0은 미적용과 동일) +- `cuisine`/`channel` 등 기존 필터와 조합 시에도 회귀 없음 확인 +- 회귀 테스트 추가: `passesMinMentions()`을 `frontend/src/lib/filters.ts`로 추출(동작 변경 없음, 테스트 가능성 확보) + `frontend/__tests__/filters.test.ts`(6케이스: 미적용/음수/미만/경계값/초과/null·undefined) +- 백엔드 빌드, `tsc --noEmit`, 프론트 전체 jest 스위트(6 suites, 32 tests) 통과 +- 갭(참고, 반려 사유 아님): `min_mentions` 필터 UI가 데스크톱 툴바에만 있고 모바일 FilterSheet에는 미적용 — 설계서에서 서버사이드 일원화(#535)로 이미 분리한 범위, 모바일 UI 적용은 #535 진행 시 함께 처리 권고 +- 04-QA → 05-Designer 로 전진 + ### 🎨 [Designer] #532 지역 표시 빈 배지 방지 — 렌더 가드 정리 - 문제: `RestaurantList.tsx`/`RestaurantDetail.tsx`가 원본 `region` 문자열(예: `"|"`, `"한국|"`)의 truthy 여부로 표시 여부를 결정 → 전 토큰이 더미/빈 값이어서 `formatRegion()` 결과가 `""`이 되는 경우에도 빈 지역 배지/줄이 렌더되어 레이아웃에 불필요한 간격이 남을 수 있었음(dev 데이터에서는 미발견, 이론적 엣지케이스) - 해결: 표시 가드를 `r.region &&` → `formatRegion(r.region) &&` 로 변경(두 파일 모두). `formatRegion()` 로직/시그니처는 변경 없음 — 호출부 조건만 정리 diff --git a/frontend/__tests__/filters.test.ts b/frontend/__tests__/filters.test.ts new file mode 100644 index 0000000..2af020d --- /dev/null +++ b/frontend/__tests__/filters.test.ts @@ -0,0 +1,36 @@ +/** + * #533 — passesMinMentions() 순수 함수 단위 테스트. + * 설계서: docs/design/533-min-mentions-filter/README.md + */ +import { passesMinMentions } from "@/lib/filters"; + +describe("passesMinMentions", () => { + it("minMentions=0(기본)이면 필터 미적용 — 항상 true", () => { + expect(passesMinMentions(0, 0)).toBe(true); + expect(passesMinMentions(null, 0)).toBe(true); + expect(passesMinMentions(100, 0)).toBe(true); + }); + + it("음수 minMentions도 필터 미적용으로 취급한다", () => { + expect(passesMinMentions(0, -1)).toBe(true); + }); + + it("video_count 가 minMentions 미만이면 제외(false)", () => { + expect(passesMinMentions(1, 2)).toBe(false); + expect(passesMinMentions(0, 1)).toBe(false); + }); + + it("video_count 가 minMentions 와 같으면 포함(경계값 포함)", () => { + expect(passesMinMentions(2, 2)).toBe(true); + }); + + it("video_count 가 minMentions 초과면 포함", () => { + expect(passesMinMentions(5, 2)).toBe(true); + }); + + it("video_count 가 null/undefined(미산정)이면 0으로 간주한다", () => { + expect(passesMinMentions(null, 2)).toBe(false); + expect(passesMinMentions(undefined, 2)).toBe(false); + expect(passesMinMentions(null, 0)).toBe(true); + }); +}); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f8d3049..7c4e2eb 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -15,6 +15,7 @@ import MyReviewsList from "@/components/MyReviewsList"; import BottomSheet from "@/components/BottomSheet"; import FilterSheet, { FilterOption } from "@/components/FilterSheet"; import { getCuisineIcon, getPhosphorCuisineIcon } from "@/lib/cuisine-icons"; +import { passesMinMentions } from "@/lib/filters"; import Icon from "@/components/Icon"; import FoodIcon from "@/components/FoodIcon"; import * as PhosphorIcons from "@phosphor-icons/react"; @@ -249,7 +250,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 (!passesMinMentions(r.video_count, minMentions)) return false; if (countryFilter) { const parsed = parseRegion(r.region); if (!parsed || parsed.country !== countryFilter) return false; diff --git a/frontend/src/lib/filters.ts b/frontend/src/lib/filters.ts new file mode 100644 index 0000000..6b6e3ed --- /dev/null +++ b/frontend/src/lib/filters.ts @@ -0,0 +1,23 @@ +// 클라이언트 필터 순수 함수 — #533 +// page.tsx 의 filteredRestaurants 에서 사용하는 경계 로직을 테스트 가능하도록 분리. + +/** + * "의미있는 언급 수(video_count)" 기준으로 식당을 포함할지 판정한다. + * 설계서: docs/design/533-min-mentions-filter/README.md + * + * - minMentions <= 0(기본값) → 필터 미적용, 항상 true. + * - videoCount 가 null/undefined(미산정) → 0 으로 간주. + * - videoCount >= minMentions 인 경우에만 true (경계값 포함). + * + * 예) passesMinMentions(1, 2) → false (1회는 2회 미만이라 제외) + * passesMinMentions(2, 2) → true (경계값 포함) + * passesMinMentions(null, 2) → false (미산정은 0 취급) + * passesMinMentions(0, 0) → true (필터 미적용) + */ +export function passesMinMentions( + videoCount: number | null | undefined, + minMentions: number +): boolean { + if (minMentions <= 0) return true; + return (videoCount ?? 0) >= minMentions; +}