Compare commits

...

5 Commits

Author SHA1 Message Date
joungmin
aba6fac2ae [Reviewer] #533 언급수 필터 초기화 — 모바일 내위치 토글 2곳 설계 §7 일관성 보강
- page.tsx: 모바일 "내위치" 토글 2곳 onClick 초기화 목록에 setMinMentions(0) 추가
  (데스크톱 토글만 반영돼 있던 설계 §7 상호배타/리셋 일관성 누락 보강)
- 설계서 §7: 데스크톱/모바일 토글 전부(3곳) 동일 적용 명시
- tsc/jest(6 suites, 32 tests) 통과. 통상 모바일 흐름에선 no-op(minMentions는 데스크톱 전용 select)

Refs #533
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:18:38 +09:00
joungmin
63354fa63a [Reviewer] #532 지역 표시 계층화 승인 — 게이트 통과
- formatRegion 순수함수/엣지케이스/렌더 가드/설계서 일치 검토 완료
- 단위 테스트 8/8 재확인, #545 접근성 회귀 복원 확인
- 06-Reviewer 승인 → 07-Release 대기

Refs #532

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:03:51 +09:00
joungmin
09ddc47103 [Developer] #533 전체보기/내위치 토글 언급수 필터 초기화 반영
- page.tsx: 전역 '전체보기' 노출 조건에 minMentions > 0 추가, onClick에 setMinMentions(0) 추가
- 내위치 토글 ON 초기화 목록에도 setMinMentions(0) 추가 (상호배타 일관성)
- 설계서 §7 필터 리셋 일관성 명시
- tsc/jest(6 suites, 32 tests) 통과

Refs #533

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:02:34 +09:00
joungmin
77aad4ab14 docs(changelog): #533 Designer/Reviewer 통과기록 — 전역 필터 초기화 결함으로 반려
- Designer: UX 패턴 일치 확인, 코드 변경 없음
- Reviewer: page.tsx:910,912-919 "전체보기" 버튼이 minMentions 미초기화 — 03-Developer 반려

Refs #533
2026-07-27 14:00:08 +09:00
joungmin
935c73129b fix: #532 커밋에서 실수로 되돌린 #545 접근성 표시(title/aria-hidden) 복원
동시 진행 중이던 [Designer] #545 커밋(e0e9ab6)의 title 속성/aria-hidden
표시를 #532 작업 중 작업 트리 정리 과정에서 착오로 되돌렸다(commit c13b976).
formatRegion 렌더 가드 변경(#532)과는 무관한 부분이므로 원상 복구.

Refs #532, #545
2026-07-27 13:56:00 +09:00
4 changed files with 52 additions and 4 deletions

View File

@@ -6,6 +6,44 @@
## 2026-07-27 ## 2026-07-27
### ✅ [Reviewer] #533 언급 적은 식당 제외 필터 — 승인 (게이트 통과)
- 검토 대상: backend(`Restaurant.videoCount`, `RestaurantMapper` xml/iface, `RestaurantService`, `RestaurantController`), frontend(`lib/api.ts`, `lib/filters.ts`, `app/page.tsx`), `__tests__/filters.test.ts`
- 정확성: video_count 스칼라 서브쿼리(strong/unknown)와 min_mentions 서버필터 로직 일치. DISTINCT+스칼라 서브쿼리는 r.id 단위라 결과 불변. 캐시 키에 `mm=` 포함(필터별 분리). `minMentions > 0` 가드로 음수/null 안전. `includeHidden` 오버로드 리팩터링에 유실된 호출부 없음(true 사용처 부재 확인)
- 설계 일치: `docs/design/533-min-mentions-filter/README.md`와 구현 일치. 순수 함수 `passesMinMentions()` 분리로 경계값/null 테스트 6케이스 커버
- 직접 수정(Reviewer): 설계 §7 "내위치 토글 ON → setMinMentions(0)" 요구 대비 데스크톱 토글만 반영되고 모바일 토글 2곳 누락 → 두 모바일 토글에 `setMinMentions(0)` 추가(반응형 리사이즈 시 일관성 확보, 통상 모바일 흐름에선 no-op). tsc/jest(6 suites, 32 tests) 통과
- 06-Reviewer 승인 → 07-Release 대기 (배포는 Release 단계에서 수행)
### ✅ [Reviewer] #532 지역 표시 계층화 — 승인 (게이트 통과)
- 검토 대상: `frontend/src/lib/region.ts`(`formatRegion`), `RestaurantDetail.tsx`/`RestaurantList.tsx`(적용·렌더 가드), `__tests__/region.test.ts`
- 정확성: `formatRegion`은 순수 함수(I/O 없음, 단일 책임). falsy/`null` 토큰(대소문자 무시)/빈 토큰/더미 `나라`/trim/전량 더미(`""`) 모두 정상 처리 — 재실행한 단위 테스트 8/8 통과
- 설계 일치: 구현이 `docs/design/532-region-display/README.md`와 일치. Designer 단계의 렌더 가드 변경(`region &&``formatRegion(region) &&`)이 설계서에 선반영됨 → 전 토큰 더미 시 빈 배지/빈 줄 미렌더 확인
- 회귀 안전성: c13b976에서 착오로 되돌아간 #545 접근성 표시(title/aria-hidden)가 935c731로 복원됨을 확인 — 현재 파일 상태 정상
- 범위 판단: `buildSearchQuery`(공백 조인, 외부 지도 검색)는 의도적으로 유지 — 적절. Admin 패널(UsersPanel/VideosPanel/RestaurantsPanel)의 raw region 표기는 설계서 비범위(관리자 화면)로, 이번 사용자 화면 버그와 무관 — 반려 사유 아님(후속 정리 권고)
- 06-Reviewer 승인 → 07-Release 대기 (배포는 Release 단계에서 수행)
### 🛠 [Developer] #533 Reviewer 반려 수정 — "전체보기" 버튼 언급수 필터 초기화
- 파일: `frontend/src/app/page.tsx`
- 전역 "전체보기" 버튼 노출 조건에 `|| minMentions > 0` 추가 → 언급수 필터만 켜져 있어도 버튼 노출
- onClick 초기화 목록에 `setMinMentions(0)` 추가 → 다른 필터와 함께 켠 상태에서도 전체 초기화됨
- "내위치" 토글 ON 초기화 목록에도 `setMinMentions(0)` 추가(내위치 ↔ 언급수 상호배타 일관성)
- 설계서 동기화: `docs/design/533-min-mentions-filter/README.md` §7에 "필터 리셋 일관성" 명시
- 검증: `tsc --noEmit` 통과, jest 6 suites / 32 tests 통과. 코어 필터 로직은 이전과 동일(변경 없음)
- 03-Developer → 06-Reviewer(재검토)
### 🛑 [Reviewer] #533 반려 — 전역 "전체보기" 버튼이 언급수 필터를 초기화하지 않음
- 파일: `frontend/src/app/page.tsx:910`(노출 조건), `912-919`(onClick 초기화 목록)
- 문제: 전역 필터 초기화 버튼("전체보기")의 노출 조건과 초기화 로직에 `minMentions`가 누락됨
- 언급수 필터만 켜져 있으면 버튼 자체가 노출되지 않음(그룹 로컬 X 버튼으로는 초기화 가능)
- 다른 필터와 언급수 필터를 함께 켠 상태에서 "전체보기"를 눌러도 언급수 필터는 계속 적용된 채로 남아, 사용자가 전체 초기화됐다고 오인할 수 있음
- 권고: 910행 조건에 `|| minMentions > 0` 추가, 912-919행에 `setMinMentions(0)` 추가. "내위치" 토글 초기화 목록도 함께 점검 권고
- 코어 필터 로직(설계서 일치, 경계값, 보안)은 문제 없음 — 이 결함만으로 반려
- 06-Reviewer → 03-Developer 로 반려
### 🎨 [Designer] #533 UX 검토 — 코드 변경 없음
- 데스크톱 툴바 "언급" 셀렉트가 기존 필터(장르/가격/나라)와 동일한 패턴(플레이스홀더/강조색/그룹 X 초기화/tooltip)을 따름을 확인, 문구 변경 불필요
- 전역 "전체보기" 버튼 결함 발견 → 동작 변경이 필요해 Designer 범위 밖으로 판단, Reviewer 단계로 이관
- 05-Designer → 06-Reviewer 로 전진 (코드 변경 없음)
### ✅ [QA] #533 언급 적은 식당 제외 필터 — 검증 통과 ### ✅ [QA] #533 언급 적은 식당 제외 필터 — 검증 통과
- 설계서(`docs/design/533-min-mentions-filter/README.md`)와 구현(commit 7639fd0) 일치 확인 - 설계서(`docs/design/533-min-mentions-filter/README.md`)와 구현(commit 7639fd0) 일치 확인
- 백엔드: `RestaurantMapper.xml findAll` 스칼라 서브쿼리(`video_count`) + `min_mentions` 필터, `DISTINCT` 조합에도 중복 없음 확인 - 백엔드: `RestaurantMapper.xml findAll` 스칼라 서브쿼리(`video_count`) + `min_mentions` 필터, `DISTINCT` 조합에도 중복 없음 확인

View File

@@ -30,7 +30,11 @@
### 프론트 ### 프론트
6. `lib/api.ts``Restaurant.video_count?: number | null`; `getRestaurants` 파라미터 `min_mentions?`(미래/서버필터용). 6. `lib/api.ts``Restaurant.video_count?: number | null`; `getRestaurants` 파라미터 `min_mentions?`(미래/서버필터용).
7. `app/page.tsx` — 필터 상태 `minMentions`(0/2/3) + `filteredRestaurants``(r.video_count ?? 0) >= minMentions` 가드 + UI 컨트롤 + useMemo deps 추가. 7. `app/page.tsx` — 필터 상태 `minMentions`(0/2/3/5) + `filteredRestaurants``(r.video_count ?? 0) >= minMentions` 가드 + UI 컨트롤 + useMemo deps 추가.
- **필터 리셋 일관성**: `minMentions`도 다른 필터(장르/가격/지역/채널)와 동일하게 취급.
- 전역 "전체보기" 버튼: 노출 조건에 `minMentions > 0` 포함, 클릭 시 `setMinMentions(0)`로 함께 초기화.
- "내위치" 토글 ON: 다른 필터 초기화 목록에 `setMinMentions(0)` 포함(내위치와 상호배타).
데스크톱/모바일 토글 **전부**(3곳)에 동일 적용 — 반응형 리사이즈 시에도 일관.
## 엣지케이스 ## 엣지케이스
- `video_count` 미산정(findById 등) → null. 클라 필터는 `?? 0`로 안전. - `video_count` 미산정(findById 등) → null. 클라 필터는 `?? 0`로 안전.

View File

@@ -907,7 +907,7 @@ export default function Home() {
)} )}
</div> </div>
{/* 필터 전체 해제 */} {/* 필터 전체 해제 */}
{(channelFilter || cuisineFilter || priceFilter || countryFilter) && ( {(channelFilter || cuisineFilter || priceFilter || countryFilter || minMentions > 0) && (
<button <button
onClick={() => { onClick={() => {
setChannelFilter(""); setChannelFilter("");
@@ -916,6 +916,7 @@ export default function Home() {
setCountryFilter(""); setCountryFilter("");
setCityFilter(""); setCityFilter("");
setDistrictFilter(""); setDistrictFilter("");
setMinMentions(0);
setRegionFlyTo(null); setRegionFlyTo(null);
}} }}
className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 bg-gray-50 dark:bg-gray-800/50 text-gray-500 dark:text-gray-400 hover:text-brand-500 transition-colors touch-manipulation" className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 bg-gray-50 dark:bg-gray-800/50 text-gray-500 dark:text-gray-400 hover:text-brand-500 transition-colors touch-manipulation"
@@ -936,6 +937,7 @@ export default function Home() {
setCountryFilter(""); setCountryFilter("");
setCityFilter(""); setCityFilter("");
setDistrictFilter(""); setDistrictFilter("");
setMinMentions(0);
if (navigator.geolocation) { if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); }, (pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },
@@ -1160,6 +1162,7 @@ export default function Home() {
setCountryFilter(""); setCountryFilter("");
setCityFilter(""); setCityFilter("");
setDistrictFilter(""); setDistrictFilter("");
setMinMentions(0);
if (navigator.geolocation) { if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); }, (pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },
@@ -1272,6 +1275,7 @@ export default function Home() {
setCountryFilter(""); setCountryFilter("");
setCityFilter(""); setCityFilter("");
setDistrictFilter(""); setDistrictFilter("");
setMinMentions(0);
if (navigator.geolocation) { if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); }, (pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },

View File

@@ -186,9 +186,10 @@ export default function RestaurantDetail({
href={restaurant.tabling_url} href={restaurant.tabling_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title="테이블링에서 줄서기 (새 탭)"
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-rose-500 hover:bg-rose-600 text-white rounded-lg text-sm font-semibold transition-colors" className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-rose-500 hover:bg-rose-600 text-white rounded-lg text-sm font-semibold transition-colors"
> >
<span>T</span> <span aria-hidden="true">T</span>
<span> </span> <span> </span>
</a> </a>
)} )}
@@ -198,9 +199,10 @@ export default function RestaurantDetail({
href={restaurant.catchtable_url} href={restaurant.catchtable_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title="캐치테이블에서 예약하기 (새 탭)"
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-violet-500 hover:bg-violet-600 text-white rounded-lg text-sm font-semibold transition-colors" className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-violet-500 hover:bg-violet-600 text-white rounded-lg text-sm font-semibold transition-colors"
> >
<span>C</span> <span aria-hidden="true">C</span>
<span> </span> <span> </span>
</a> </a>
)} )}