feat: notFoundPage UI 구현#46
Conversation
📝 Walkthrough📋 Walkthrough라우트 설정에 catch-all 경로를 추가하고, NotFound 컴포넌트를 간단한 JSX에서 완성도 있는 React 컴포넌트로 개선했습니다. 뒤로 가기 및 홈 이동 네비게이션 로직이 추가되었습니다. 📊 Changes
🎯 코드 리뷰 포인트안녕하세요! 👋 NotFound 페이지 구현 고생 많으셨어요. 몇 가지 체크할 점들을 공유드립니다. 안정성 & 접근성1️⃣ history.length 체크의 신뢰성 현재 코드에서 제안: 더 견고하게 처리하려면: const handleGoBack = () => {
// history.length는 신뢰도가 100%가 아니므로,
// 브라우저 백 버튼 클릭 시도 후 성공 여부를 확인하는 방식도 고려
if (window.history.length > 1) {
window.history.back();
} else {
navigate('/', { replace: true });
}
};혹은 더 안전하게 항상 대체 옵션을 제공하는 방식도 있습니다. 2️⃣ 버튼 접근성 버튼에 <button aria-label="이전 페이지로 돌아가기">
<ChevronLeft />
</button>성능 & 구조3️⃣ 라우트 순서 App.tsx에서 catch-all 라우트( 4️⃣ React.FC 타입 const NotFound: React.FC = () => { ... }현대적인 React 패턴을 따르시면 더 좋습니다: const NotFound: React.FC<{}> = () => { ... }
// 또는
const NotFound = (): React.ReactElement => { ... }🎯 Review Effort🎯 2 (Simple) | ⏱️ ~12 minutes 🎉 축하시
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@src/components/owner/menuFormModal.tsx`:
- Around line 42-52: The handleSubmit currently does Number(formData.price)
which yields 0 for empty string and NaN for invalid input; add validation in
handleSubmit to trim and check formData.price is non-empty and that parsedPrice
= Number(formData.price) (or parseFloat) isFinite and not NaN before calling
onSubmit, and if invalid prevent submission (do not call onSubmit/onClose) and
surface an error (e.g., set a local validation state or call a provided onError)
so the user can correct it; keep using editingMenu?.id, editingMenu?.isActive
and editingMenu?.isSoldOut when building the payload passed to onSubmit.
In `@src/components/owner/menuManagement.tsx`:
- Around line 83-88: deleteCategory currently removes the category but leaves
menus pointing to the deleted category id, creating orphaned menus; update
deleteCategory to also update menus by calling setMenus and mapping over
previous menus to replace any menu.category === id with 'ALL' (or the default
category id used by the app), then proceed to setCategories and
setActiveCategory('ALL') as before; reference deleteCategory, setMenus,
setCategories, and setActiveCategory when making this change.
- Around line 106-120: The click handler is unsafely casting dynamic category
IDs to CategoryType which can break at runtime; update the active category state
and related signatures to accept both known CategoryType values and dynamic
string IDs (e.g., change the activeCategory/state setter type from CategoryType
to string | CategoryType and adjust any consumers such as the filtering logic),
ensure cat.id is typed as string | CategoryType where categories are built
(including handleAddCategory which generates "CAT_${Date.now()}"), and remove
the cast in setActiveCategory so you call setActiveCategory(cat.id) with correct
types throughout (affecting functions: setActiveCategory, handleAddCategory,
categories.map, and any filters that read activeCategory).
In `@src/components/owner/ownerHeader.tsx`:
- Line 1: Add an explicit React import so the React.FC type is available: update
the top of ownerHeader.tsx to import React (e.g., import React from 'react'; or
import type { FC } from 'react' and update the component signature accordingly)
to ensure the React.FC type used in the ownerHeader component is resolved at
compile time.
In `@src/components/owner/storeSettings.tsx`:
- Around line 214-222: The toggle button for the no-show policy uses the wrong
state in its accessibility attribute: change the aria-checked reference from
sameDayBooking to noShowPolicy so the switch reflects
setNoShowPolicy/noShowPolicy correctly (update the button that calls
setNoShowPolicy and the aria-checked prop to use noShowPolicy instead of
sameDayBooking) to ensure screen readers receive the actual toggle state.
In `@src/components/owner/tableCreateModal.tsx`:
- Around line 24-39: The cols/rows inputs allow 0 or negative values; add native
min="1" to both number inputs and update their onChange handlers (cols ->
setCols, rows -> setRows) to parse, clamp to >=1 (e.g., Math.max(1,
Number(...))) to ensure internal state never becomes <1; additionally, before
calling onConfirm in the modal confirm handler, validate that cols and rows are
>=1 and short-circuit (or surface a validation error) if not, so an invalid
table is never created.
In `@src/components/owner/tableDashboard.tsx`:
- Line 276: In the TableDashboard component replace the invalid Tailwind class
token in the cancel/delete button JSX (currently written as bg-[`#FF6B6B`]) with
the correct syntax bg-[`#FF6B6B`] and remove the placeholder "..." token,
replacing it with the actual utility classes you want (e.g., padding, rounded,
hover/focus states like p-2 rounded-md hover:opacity-90 focus:outline-none) so
the button renders with proper styling; locate the JSX button that renders <X
size={14}/> to update its className.
- Around line 84-91: The startEditingCapacity helper captures
originalMinCapacity/originalMaxCapacity but is never used; locate where code
calls updateTable(id, { isEditingCapacity: true }) and replace that call with
startEditingCapacity(id) so the originalMinCapacity and originalMaxCapacity are
saved via getTableData before toggling edit mode; alternatively, if you prefer
to keep the inline call, update that site to also set originalMinCapacity:
table.minCapacity and originalMaxCapacity: table.maxCapacity by calling
getTableData(id) first (use function names getTableData, updateTable, and
startEditingCapacity to find the relevant spots).
🧹 Nitpick comments (17)
src/pages/myPage/storePage.tsx (1)
109-188: 빈 상태(empty state) 처리 고려해주세요현재
shops배열이 하드코딩되어 있지만, 추후 API 연동 시 가게 목록이 비어있을 때의 UX가 필요해요. 코딩 가이드라인에서도 로딩/에러/empty 상태 확인을 권장하고 있어요.이슈
#45체크리스트에 "예외처리: 로딩 / 에러 / 빈 화면"이 포함되어 있으니, 해당 작업 시 함께 처리하시면 될 것 같아요!💡 빈 상태 처리 예시
{/* 가게 리스트 */} <div className="space-y-4 mb-8"> + {shops.length === 0 ? ( + <div className="text-center py-12 text-gray-500"> + <Store size={48} className="mx-auto mb-4 text-gray-300" /> + <p>등록된 가게가 없습니다.</p> + <p className="text-sm mt-1">새 가게를 등록해보세요!</p> + </div> + ) : ( {shops.map((shop) => ( // ... existing code ))} + )} </div>src/components/owner/menuFormModal.tsx (2)
4-10:any타입 사용으로 타입 안전성이 떨어져요.
onSubmit과editingMenu에any타입을 사용하면 컴파일 타임에 타입 오류를 잡기 어렵고, 유지보수 시 실수가 발생하기 쉬워요. 명확한 타입 정의를 권장드려요.♻️ 타입 정의 예시
+interface MenuData { + id: string; + name: string; + category: string; + price: number; + description: string; + isActive: boolean; + isSoldOut: boolean; +} + interface MenuFormModalProps { isOpen: boolean; onClose: () => void; - onSubmit: (menuData: any) => void; + onSubmit: (menuData: MenuData) => void; categories: { id: string; label: string }[]; - editingMenu?: any; // 수정 시 전달받을 데이터 + editingMenu?: MenuData | null; }
54-64: 모달 접근성(a11y) 개선이 필요해요.스크린 리더 사용자를 위해 모달에
role="dialog"와aria-modal,aria-labelledby속성을 추가하면 좋겠어요. 닫기 버튼에도aria-label이 있으면 더 좋아요.♿ 접근성 개선 예시
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"> - <div className="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200 border border-gray-100"> + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm" role="presentation"> + <div + role="dialog" + aria-modal="true" + aria-labelledby="menu-form-title" + className="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200 border border-gray-100" + > <div className="flex justify-between items-center px-8 py-6 border-b border-gray-50"> - <h3 className="text-xl text-gray-900"> + <h3 id="menu-form-title" className="text-xl text-gray-900"> {editingMenu ? '메뉴 수정' : '새 메뉴 등록'} </h3> - <button onClick={onClose} className="p-2 text-gray-400 hover:text-gray-600 transition-colors"> + <button onClick={onClose} aria-label="닫기" className="p-2 text-gray-400 hover:text-gray-600 transition-colors">src/components/owner/menuManagement.tsx (1)
122-173: 메뉴가 없을 때 빈 화면(empty state) UX가 없어요.
filteredMenus가 비어있을 때 사용자에게 아무것도 보여주지 않으면 혼란스러울 수 있어요. 빈 상태 안내 메시지를 추가하면 UX가 개선돼요.✨ Empty state 추가 예시
{/* 메뉴 카드 그리드 */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12"> - {filteredMenus.map(menu => ( + {filteredMenus.length === 0 ? ( + <div className="col-span-full text-center py-12 text-gray-400"> + 등록된 메뉴가 없습니다. 메뉴를 추가해 주세요. + </div> + ) : filteredMenus.map(menu => ( <div key={menu.id}As per coding guidelines:
src/pages/**: 로딩/에러/empty 상태 UX 확인.src/components/owner/tableDetailModal.tsx (4)
9-9: 사용되지 않는 props와 state가 있어요.
onManageReservationprop과setTableImageUrlsetter가 선언만 되고 사용되지 않아요. 향후 구현 예정이 아니라면 제거하거나, 예정이라면 TODO 주석을 남겨두면 좋겠어요.🧹 정리 예시
interface Props { tableNumber: number; onClose: () => void; breakTimes: BreakTime[]; - onManageReservation?: () => void; + // TODO: 예약 관리 기능 추가 시 사용 + // onManageReservation?: () => void; }또는 사용하지 않는다면:
- const [tableImageUrl, setTableImageUrl] = useState<string | null>(null); + const tableImageUrl: string | null = null; // 이미지 업로드 기능 추가 시 state로 변경Also applies to: 91-91
45-49: 시간 문자열 비교 방식이 취약할 수 있어요.
time >= bt.start && time < bt.end같은 문자열 비교는 "09:00" < "9:00" 같은 포맷 불일치 시 잘못된 결과를 줄 수 있어요. 현재 코드에서는padStart로 포맷팅하고 있어서 괜찮지만, 외부에서 받는breakTimes포맷이 다를 경우 문제가 될 수 있어요.🛡️ 더 안전한 비교 방식
const timeToMinutes = (time: string): number => { const [h, m] = time.split(':').map(Number); return h * 60 + m; }; const isBreakTime = (time: string, breakTimes: BreakTime[]) => { const timeMinutes = timeToMinutes(time); return breakTimes.some( (bt) => timeMinutes >= timeToMinutes(bt.start) && timeMinutes < timeToMinutes(bt.end) ); };
94-98: 모달 접근성(a11y) 개선이 필요해요.
role="dialog",aria-modal="true",aria-labelledby속성을 추가하면 스크린 리더 사용자가 모달임을 인식할 수 있어요.♿ 접근성 개선 예시
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4" onClick={onClose}> - <div className="bg-white w-full max-w-2xl rounded-lg shadow-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200" + <div + role="dialog" + aria-modal="true" + aria-labelledby="table-detail-title" + className="bg-white w-full max-w-2xl rounded-lg shadow-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200" onClick={(e)=>e.stopPropagation()}>그리고 헤더의 h3에 id 추가:
- <h3 className="text-lg text-gray-900"> + <h3 id="table-detail-title" className="text-lg text-gray-900">
209-214: 비활성화된 날짜 버튼의 cursor 스타일이 중복 적용돼요.
disabled상태에서cursor-not-allowed를 적용하고 있지만, 같은 className에cursor-pointer도 함께 있어요. CSS 특성상 나중에 선언된 것이 우선하지만, 명확하게 분리하는 게 좋아요.✨ 스타일 정리
- <button key={day} disabled={isPast} onClick={() => { setSelectedFullDate(dateObj); setStep('SLOTS'); }} className={`cursor-pointer h-14 rounded-xl border-2 flex flex-col items-center justify-center font-bold transition-all ${isPast ? 'bg-gray-50 border-gray-50 text-gray-300 cursor-not-allowed' : isTodayFlag ? 'bg-blue-50 border-gray-200 text-black shadow-lg hover:border-blue-300' : 'bg-white border-gray-100 text-gray-700 hover:border-blue-300 hover:bg-blue-50'}`}> + <button key={day} disabled={isPast} onClick={() => { setSelectedFullDate(dateObj); setStep('SLOTS'); }} className={`h-14 rounded-xl border-2 flex flex-col items-center justify-center font-bold transition-all ${isPast ? 'bg-gray-50 border-gray-50 text-gray-300 cursor-not-allowed' : isTodayFlag ? 'bg-blue-50 border-gray-200 text-black shadow-lg hover:border-blue-300 cursor-pointer' : 'bg-white border-gray-100 text-gray-700 hover:border-blue-300 hover:bg-blue-50 cursor-pointer'}`}>src/pages/NotFound.tsx (1)
33-39: "이전으로" 버튼의 엣지 케이스를 고려해볼 수 있어요.사용자가 404 페이지를 직접 URL로 접근한 경우(히스토리가 없는 경우)
navigate(-1)이 브라우저 기본 동작에 따라 아무 일도 안 하거나 예상치 못한 페이지로 이동할 수 있어요. 큰 문제는 아니지만 참고하세요.🛡️ 히스토리 체크 예시 (선택사항)
+ const handleGoBack = () => { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate('/'); + } + }; + return ( // ... <button - onClick={() => navigate(-1)} + onClick={handleGoBack} className="flex items-center justify-center gap-2 px-6 py-3 border border-gray-300 rounded-xl bg-white text-gray-700 font-medium hover:bg-gray-50 transition-all active:scale-95" >src/components/owner/ownerHeader.tsx (1)
25-42: 탭 네비게이션 접근성을 개선할 수 있어요.탭 UI에
role="tablist",role="tab",aria-selected속성을 추가하면 스크린 리더 사용자가 탭 네비게이션임을 인식할 수 있어요.♿ 접근성 개선 예시
- <nav className="flex gap-5 pt-4 px-5"> + <nav role="tablist" className="flex gap-5 pt-4 px-5"> {tabs.map(tab => ( <button key={tab.key} + role="tab" + aria-selected={activeTab === tab.key} onClick={() => onChangeTab(tab.key)} className={`pb-4 px-2 text-md transition-all relative ${ activeTab === tab.key ? 'text-blue-600' : 'text-gray-900 hover:text-gray-900' }`} >As per coding guidelines:
src/components/**: 접근성(aria) 체크.src/pages/ownerPage/ownerPage.tsx (2)
71-81: 중복된 조건문 제거 필요
activeTab === 'settings'와activeTab === 'menu'조건이 외부 블록과 내부에서 중복 체크되고 있어요. 불필요한 코드입니다.♻️ 중복 조건 제거 제안
{activeTab === 'dashboard' && <TableDashboard />} {activeTab === 'settings' && ( <div className="max-w-7xl mx-auto text-gray-500"> - {activeTab === 'settings' && <StoreSettings />} + <StoreSettings /> </div> )} {activeTab === 'menu' && ( <div className="max-w-7xl mx-auto text-gray-500"> - {activeTab === 'menu' && <MenuManagement />} + <MenuManagement /> </div> )}
23-65: 탭 접근성(A11y) 개선 권장탭 네비게이션에 적절한 ARIA 속성이 없어서 스크린 리더 사용자가 탭 구조를 인식하기 어려워요.
role="tablist",role="tab",aria-selected등을 추가하면 좋겠습니다.♿ 접근성 개선 예시
- <nav className="flex gap-5 pt-4 px-5"> + <nav className="flex gap-5 pt-4 px-5" role="tablist" aria-label="가게 관리 탭"> <button onClick={() => setActiveTab('dashboard')} + role="tab" + aria-selected={activeTab === 'dashboard'} className={`pb-4 px-2 text-md transition-all relative ${ activeTab === 'dashboard' ? 'text-blue-600' : 'text-gray-900 hover:text-gray-900' }`} >나머지 탭 버튼들에도 동일하게
role="tab"과aria-selected속성을 추가해 주세요.As per coding guidelines:
src/components/**: 컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크.src/components/owner/storeSettings.tsx (1)
39-97: 입력 필드와 라벨 연결 권장
<label>과<input>이htmlFor/id로 연결되어 있지 않아요. 라벨 클릭 시 입력 필드에 포커스가 가지 않고, 접근성 측면에서도 개선이 필요합니다. 현재는 동작에 큰 문제는 없지만, 향후 개선하면 좋겠어요.♿ 예시
<div> - <label className={labelStyle}>가게 이름</label> + <label htmlFor="storeName" className={labelStyle}>가게 이름</label> <input + id="storeName" type="text" value={storeName}src/components/owner/tableCreateModal.tsx (1)
18-18: 닫기 버튼 접근성 개선닫기 버튼에
aria-label이 없어서 스크린 리더 사용자가 버튼의 용도를 알기 어려워요.♿ 접근성 속성 추가
- <button onClick={onClose} className="absolute right-6 top-6 text-gray-400"><X /></button> + <button onClick={onClose} aria-label="닫기" className="absolute right-6 top-6 text-gray-400"><X /></button>src/components/owner/tableDashboard.tsx (2)
334-343: 영업시간 하드코딩됨
BreakTimeModal에openTime과closeTime이 하드코딩되어 있어요.StoreSettings에서 설정한 영업시간과 동기화되지 않으면 사용자 혼란이 생길 수 있습니다. 향후 상태 관리(Context나 전역 상태)로 연동하는 것을 고려해 주세요.
22-22: 들여쓰기 일관성Line 22의
breakTimes상태 선언이 다른 상태들과 들여쓰기가 다릅니다. 코드 일관성을 위해 정렬해 주세요.♻️ 들여쓰기 수정
const [isBreakModalOpen, setIsBreakModalOpen] = useState(false); -const [breakTimes, setBreakTimes] = useState<BreakTime[]>([]); + const [breakTimes, setBreakTimes] = useState<BreakTime[]>([]); const [tableData, setTableData] = useState<Record<number, TableInfo>>({});src/components/owner/BreakTimeModal.tsx (1)
32-34: 닫기 버튼 접근성 개선닫기 버튼에
aria-label이 없어서 스크린 리더 사용자에게 버튼 용도가 전달되지 않아요.♿ 접근성 속성 추가
- <button onClick={onClose} className="absolute right-4 top-4 hover:text-gray-500 cursor-pointer"> + <button onClick={onClose} aria-label="닫기" className="absolute right-4 top-4 hover:text-gray-500 cursor-pointer"> <X /> </button>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/pages/NotFound.tsx`:
- Around line 33-39: The Back button currently calls navigate(-1) directly in
the NotFound component, which can misbehave when there is no prior history;
update the button's onClick handler to check browser history (e.g.,
window.history.length) or navigation capability before calling navigate(-1) and
provide a safe fallback route (e.g., navigate('/') or navigate('/home')) so
users who land directly on the 404 are routed into the app instead of leaving
it.
🧹 Nitpick comments (1)
src/pages/NotFound.tsx (1)
13-14: 접근성: 움직임 민감도(motion sensitivity)를 고려해주세요.
animate-pulse애니메이션이 계속 반복되는데, 전정기관 장애가 있거나 움직임에 민감한 사용자에게 불편을 줄 수 있어요.prefers-reduced-motion미디어 쿼리를 고려하면 좋을 것 같습니다.♻️ Tailwind에서 motion-reduce 유틸리티 활용 예시
- <div className="absolute inset-0 flex items-center justify-center animate-pulse"> + <div className="absolute inset-0 flex items-center justify-center animate-pulse motion-reduce:animate-none">이렇게 하면 사용자가 시스템 설정에서 "움직임 줄이기"를 활성화한 경우 애니메이션이 비활성화됩니다.
jjjsun
left a comment
There was a problem hiding this comment.
에러페이지 잘 구성해주신것같아요! 좋습니다! 아래 코멘트 남긴것만 추가해주시면 될것같아요!
…tFoundPage notFoundPage
💡 개요
notFoundPage UI 구현
🔢 관련 이슈 링크
💻 작업내용
-notFoundPage UI 구현
-이전으로, 홈으로 버튼 구현
📌 변경사항PR
🤔 추가 논의하고 싶은 내용
✅ 체크리스트
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선