Feat/#51 frontend UI batch#56
Conversation
백엔드는 지원하지만 프론트에서 노출되지 않던 기능들을 추가하고, 가로 스크롤 UX 개선을 위한 드래그 스크롤을 도입. - 빈자리 알림 시간대 선택 UI 추가 (#51) 매장 상세에서 시간 슬롯 상태에 따라 진입 버튼/모달 헤더를 3-way로 분기, 만석 슬롯에 점선 테두리 + 종 아이콘 적용 - 카테고리 페이지 지역 필터 드롭다운 추가 (#52) 25개 서울 자치구 드롭다운 추가, 카테고리와 독립 동작, URL 파라미터 동기화 - 가로 드래그 스크롤 기능 추가 (#53) useDragScroll 커스텀 훅으로 마우스 클릭+드래그 스크롤 지원, 카테고리 탭과 14일 날짜 스트립에 적용
- 진입 버튼 라벨 조건 단순화 (분기 4개 → 3개) - useDragScroll의 offsetLeft 계산 제거 (delta 산출에 불필요)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a district-based filtering system on the category page and a new useDragScroll hook to enable mouse-drag horizontal scrolling for UI components. The store detail page has been updated with logic to handle various reservation states, including "vacancy notifications" for fully booked time slots. Review feedback focuses on optimizing performance via useCallback, improving the robustness of query parameter parsing for empty strings, addressing a potential UX dead-end in the reservation flow, and refactoring complex conditional styling for better maintainability.
| 'use client'; | ||
|
|
||
| import { Suspense, useEffect, useRef } from 'react'; | ||
| import { Suspense, useEffect, useRef, useState } from 'react'; |
| category: selectedCategory ?? undefined, | ||
| district: selectedDistrict ?? undefined, |
There was a problem hiding this comment.
Using ?? undefined only handles null values. If the query parameter is present but empty (e.g., ?selected=&district=), searchParams.get returns an empty string, which might be sent to the API. Using || undefined ensures that empty strings are also treated as undefined, preventing unnecessary empty filter values from being sent to the backend.
| category: selectedCategory ?? undefined, | |
| district: selectedDistrict ?? undefined, | |
| category: selectedCategory || undefined, | |
| district: selectedDistrict || undefined, |
| const handleCategoryClick = (categoryEnum: string | null) => { | ||
| if (categoryEnum) { | ||
| router.push(`/category?selected=${categoryEnum}`); | ||
| } else { | ||
| router.push('/category'); | ||
| } | ||
| router.push(buildHref(categoryEnum, selectedDistrict)); | ||
| }; | ||
|
|
||
| const handleDistrictClick = (districtEnum: string | null) => { | ||
| router.push(buildHref(selectedCategory, districtEnum)); | ||
| setIsDistrictOpen(false); | ||
| }; |
There was a problem hiding this comment.
These handlers should be wrapped in useCallback to maintain stable references. This is a best practice in React to prevent unnecessary re-renders of child components (like the buttons in the map loops) and to keep the dependency graph clean.
| const handleCategoryClick = (categoryEnum: string | null) => { | |
| if (categoryEnum) { | |
| router.push(`/category?selected=${categoryEnum}`); | |
| } else { | |
| router.push('/category'); | |
| } | |
| router.push(buildHref(categoryEnum, selectedDistrict)); | |
| }; | |
| const handleDistrictClick = (districtEnum: string | null) => { | |
| router.push(buildHref(selectedCategory, districtEnum)); | |
| setIsDistrictOpen(false); | |
| }; | |
| const handleCategoryClick = useCallback((categoryEnum: string | null) => { | |
| router.push(buildHref(categoryEnum, selectedDistrict)); | |
| }, [router, selectedDistrict]); | |
| const handleDistrictClick = useCallback((districtEnum: string | null) => { | |
| router.push(buildHref(selectedCategory, districtEnum)); | |
| setIsDistrictOpen(false); | |
| }, [router, selectedCategory]); |
| {slotState === 'mixed' | ||
| ? '예약 또는 빈자리 알림' | ||
| : (slotState === 'vacancy' || (slotState === 'unknown' && isSelectedFullyBooked)) | ||
| ? '시간대 선택하고 빈자리 알림 받기' | ||
| : '예약하기'} |
There was a problem hiding this comment.
There is a potential UX issue when slotState is unknown but isSelectedFullyBooked is true. The button text suggests the user can 'Select a time and get a vacancy notification', but if times is empty (which causes the unknown state), the BottomSheet will show 'No available times' and the user won't be able to select anything. Consider handling the case where no time slots are available at all for a fully booked day to avoid leading the user into a dead end.
| className={`flex items-center justify-center gap-1 rounded-lg border py-2 text-sm font-medium ${ | ||
| selectedTime === displayTime | ||
| ? selectedTimeIsFull ? 'border-blue-500 bg-blue-50 text-blue-500' : 'border-orange-500 bg-orange-50 text-orange-500' | ||
| : isFull | ||
| ? 'border-gray-100 bg-gray-50 text-gray-400' | ||
| ? 'border-dashed border-gray-300 text-gray-600 hover:border-blue-500 hover:text-blue-500' | ||
| : 'border-gray-200 text-gray-700 hover:border-orange-500 hover:text-orange-500' | ||
| }`} |
There was a problem hiding this comment.
The nested ternary for the button's className is quite complex and hard to read. Consider refactoring this into a helper function or a set of variables to improve maintainability and clarity. For example, you could define a getButtonStyles function that returns the appropriate Tailwind classes based on selectedTime, displayTime, and isFull.
- 카테고리 페이지: 빈 문자열 쿼리 파라미터 처리를 위해 ?? → || 변경 - 카테고리 페이지: handleCategoryClick, handleDistrictClick을 useCallback으로 래핑 - 매장 상세 페이지: slotState가 unknown일 때 버튼 비활성화 처리 - 매장 상세 페이지: 시간 슬롯 버튼 스타일 분기를 getTimeButtonStyles 헬퍼 함수로 추출
📢 기능 설명
필요시 실행결과 스크린샷 첨부
연결된 issue
연결된 issue를 자동을 닫기 위해 아래 {이슈넘버}를 입력해주세요.
close #{이슈넘버}
✅ 체크리스트