Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough이번 PR은 UI 아이콘과 레이아웃 태그의 일관된 정리 작업입니다. PNG 기반 아이콘들을 SVG 컴포넌트로 교체하고 하단 네비게이션의 활성화 로직을 pathname 기반으로 전환했으며, 여러 레이아웃/페이지에서 DOM 태그(예: div ↔ main, section ↔ div) 및 래핑 구조를 수정했습니다. 마이페이지의 통계 타일을 클릭 가능한 요소로 변환하고 요약 관련 뒤로가기 아이콘에 호버 스타일을 추가했으며, 로딩 컴포넌트에 접근성(role/aria) 속성과 레이아웃 변경을 적용했습니다. 인증 흐름 판정 조건은 간소화되었습니다. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(private)/summary/bookmarks/page.tsx (1)
108-122: 훅 레벨의 정규화를 믿고 컴포넌트에서 불필요한 타입 체크 제거
useBookmarkedSummaryList훅이 이미keywords: string[]로 정규화하므로, 컴포넌트 레벨의typeof summary.keywords === 'string'체크는 불필요합니다. 훅의 타입 계약을 신뢰하고 중복 방어 로직을 제거하세요.- typeof summary.keywords === 'string' ? [summary.keywords] : summary.keywords; + summary.keywords훅에서 이미 JSON.parse를 포함한 방어적 파싱을 수행(useSummaryList.ts, useBookmarkedSummaryList.ts 참고)하고 있으므로, 컴포넌트는 정규화된 데이터를 받는다고 가정할 수 있습니다. 이렇게 하면 데이터 정규화 책임이 명확해지고 컴포넌트 로직도 단순화됩니다.
🤖 Fix all issues with AI agents
In `@src/app/`(private)/mypage/page.tsx:
- Around line 153-170: The two buttons that call handleSummary and
handleBookmarks (rendering summaryCount/bookmarkCount with statsLoading) are
missing an explicit type attribute and will default to type="submit" inside
forms; update both <button> elements to include type="button" to prevent
accidental form submission (add type="button" to the button rendering
summaryCount and the one rendering bookmarkCount).
In `@src/app/layout.tsx`:
- Around line 45-48: The root layout is missing the semantic <main> landmark
causing WCAG failures; update the Providers component in layout.tsx so that the
rendered children from {children} are wrapped in a single <main> element (keep
GlobalComponents outside the <main>), ensuring every page inherits exactly one
main landmark; check components named Providers and GlobalComponents when making
the change and verify other nested layouts/pages don’t inject additional <main>
elements.
🧹 Nitpick comments (4)
src/app/(private)/summary/_components/SummarySuccessPage.tsx (1)
62-64: 백 버튼 접근성 개선 권장SVG 아이콘으로 전환은 좋은 변경입니다. 다만 키보드 사용자를 위한 포커스 상태 스타일링이 없습니다.
focus:outline또는focus-visible:ring클래스 추가를 권장합니다.♻️ 포커스 스타일링 추가 제안
- <button type="button" onClick={() => router.push('/summary')} className="ml-4"> + <button + type="button" + onClick={() => router.push('/summary')} + aria-label="뒤로가기" + className="ml-4 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500" + >src/components/layout/bottom-navigation/index.tsx (2)
20-113: 네비게이션 아이템 구조 반복 - 선택적 리팩토링 제안각 네비게이션 아이템이 동일한 패턴(버튼 → 아이콘 → 라벨)을 따르고 있습니다. 현재도 읽기 쉽지만, 향후 아이템 추가/수정 시 유지보수성을 위해 설정 배열과 매핑 방식으로 리팩토링을 고려해볼 수 있습니다.
♻️ 설정 기반 리팩토링 예시
const navItems = [ { path: '/', label: '홈', icon: Home }, { path: '/recommend', label: '추천', icon: Recommend }, { path: '/summary', label: '요약', icon: Summary }, { path: '/mypage', label: 'MY', icon: My }, ]; // 상담 버튼은 특수 스타일이므로 별도 처리 {navItems.map(({ path, label, icon: Icon }) => ( <li key={path}> <button type="button" onClick={() => router.push(path)} className="flex flex-col items-center gap-1"> <Icon className={`h-5 w-5 transition-colors ${isActive(path) ? 'text-primary-500' : 'text-gray-600'}`} /> <span className={`text-xs ${isActive(path) ? 'text-primary-500 font-semibold' : 'text-gray-600'}`}> {label} </span> </button> </li> ))}
105-111: 라벨 일관성 확인 필요다른 탭들은 한글 라벨(홈, 추천, 상담, 요약)을 사용하는데, 마지막 탭만 영어 "MY"로 되어 있습니다. 의도된 브랜딩이라면 무시해도 되지만, 일관성을 위해 "마이" 또는 "마이페이지"로 변경을 고려해보세요.
src/app/(private)/summary/bookmarks/page.tsx (1)
24-101: 헤더 컴포넌트 중복 - 리팩토링 권장동일한 헤더 구조가 4번 반복됩니다(로딩, 인증 에러, 목록 에러, 정상 렌더). 이는 DRY 원칙 위반이며, 헤더 스타일 변경 시 4곳을 모두 수정해야 합니다.
공통 헤더 컴포넌트를 추출하거나, 조건부 렌더링 구조를 개선하는 것을 권장합니다.
♻️ 헤더 컴포넌트 추출 제안
// 헤더 컴포넌트 추출 const BookmarkHeader = () => { const router = useRouter(); return ( <div className="relative flex h-14 items-center bg-white"> <button type="button" aria-label="뒤로가기" onClick={() => router.push('/mypage')} className="ml-4 flex items-center" > <GoBack className="hover:text-primary-500" /> </button> <p className="absolute left-1/2 -translate-x-1/2 text-sm font-semibold">북마크 상담</p> </div> ); }; // 본문에서 사용 export default function BookmarkedSummaryPage() { // ... hooks ... // 공통 레이아웃으로 감싸기 const renderContent = () => { if (meLoading || listLoading) { return <div className="py-12 text-center text-sm text-gray-400">북마크 불러오는 중...</div>; } if (meError || !Number.isFinite(userId)) { return <div className="py-12 text-center text-sm text-gray-400">로그인이 필요합니다</div>; } // ... etc }; return ( <> <BookmarkHeader /> {renderContent()} <BottomNav /> </> ); }
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(private)/layout.tsx (1)
9-35: 코드 중복:buildCookieHeader와callMe함수 추출 권장
buildCookieHeader와callMe함수가 다음 파일들에서 중복되어 있습니다:
src/app/(private)/layout.tsxsrc/app/(public)/layout.tsxsrc/app/(policy)/policy/page.tsx
src/lib/auth/server.ts에 이미 유사한 유틸리티가 존재하는 것으로 보입니다. DRY 원칙에 따라 공통 유틸리티로 추출하면 유지보수성이 향상됩니다.♻️ 공통 유틸리티 추출 제안
src/lib/auth/server.ts에 통합:// src/lib/auth/server.ts import { cookies } from 'next/headers'; const API = process.env.NEXT_PUBLIC_API_BASE_URL; export async function buildCookieHeader(): Promise<string> { const store = await cookies(); return store .getAll() .map(({ name, value }) => `${name}=${value}`) .join('; '); } export async function callMe(cookieHeader: string): Promise<Response | null> { if (!API) return null; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(`${API}/api/auth/me`, { method: 'GET', headers: cookieHeader ? { cookie: cookieHeader } : {}, cache: 'no-store', signal: controller.signal, }); return res; } catch (e) { console.error('[auth] callMe failed:', e); return null; } finally { clearTimeout(timeoutId); } }각 레이아웃에서 import하여 사용:
import { buildCookieHeader, callMe } from '@/lib/auth/server';
🤖 Fix all issues with AI agents
In `@src/app/`(policy)/policy/page.tsx:
- Around line 67-70: The page currently uses a nested <main> which violates
HTML5 (root layout already renders <main>), so replace the outer <main> in the
policy page with a neutral container (e.g., <div> or <section>) preserving the
existing className and children; update the element that wraps PolicyHeader,
PolicyAgree, and PolicyView in the component rendering logic (the block that
currently reads main className="min-h-dvh bg-[`#FBF8FB`] px-4 pt-6") to a
div/section to avoid nested main elements, or alternatively adjust the top-level
layout to not always emit <main> and ensure only one semantic main exists per
document.
In `@src/components/loading/index.tsx`:
- Around line 6-9: The outer container div in the Loading component (the div
with className "bg-primary-100 flex h-96 w-full ...") is missing ARIA
attributes; update that element in src/components/loading/index.tsx to include
role="status", aria-live="polite", and aria-busy="true" (matching
src/components/ui/fallback/LoadingComponent.tsx) so screen readers announce the
loading state for the Image/ loadingPng content.
- Around line 11-14: The container div in the loading component (the <div
className="flex items-center justify-center gap-4"> in
src/components/loading/index.tsx) is missing accessibility attributes; update
that div to include role="status", aria-live="polite", and aria-busy="true"
(same approach used in LoadingComponent.tsx) so screen readers are notified of
the loading state.
🧹 Nitpick comments (8)
src/app/(private)/mypage/page.tsx (5)
140-149: 접근성: Moono 아이콘에 대체 텍스트 또는aria-hidden속성 추가 권장
<Moono />컴포넌트가 프로필 아바타로 사용되고 있습니다. 스크린 리더 사용자를 위해 의미 있는 이미지라면aria-label을, 순수 장식용이라면aria-hidden="true"를 추가하는 것이 좋습니다.🔧 수정 제안
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/70 shadow-md"> - <Moono /> + <Moono aria-hidden="true" /> </div>또는 의미 있는 아이콘인 경우:
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/70 shadow-md"> - <Moono /> + <Moono role="img" aria-label="프로필 아바타" /> </div>
153-172: 접근성: 버튼에 hover/focus 상태 스타일 추가 권장버튼이
type="button"과 클릭 핸들러가 잘 추가되어 있습니다. 다만 시각적 피드백을 위해hover와focus상태의 스타일을 추가하면 사용자 경험이 향상됩니다. 키보드 내비게이션 시 포커스 상태가 명확히 보여야 접근성 기준을 충족합니다.🔧 수정 제안
<button type="button" onClick={handleSummary} - className="rounded-2xl bg-white/70 px-4 py-3 text-center" + className="rounded-2xl bg-white/70 px-4 py-3 text-center transition-colors hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-pink-300" ><button type="button" onClick={handleBookmarks} - className="rounded-2xl bg-white/70 px-4 py-3 text-center" + className="rounded-2xl bg-white/70 px-4 py-3 text-center transition-colors hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-purple-300" >
100-109: UX 개선: 미구현 설정 항목에 대한 처리 필요"테마 변경"과 "언어 설정" 항목에
onClick핸들러가 없어noop이 할당됩니다. 사용자가 클릭하면 아무 반응이 없어 혼란스러울 수 있습니다. 다음 중 하나의 방법을 권장드립니다:
- 기능 준비 중임을 알리는 토스트 표시
- 비활성화 스타일 적용 (
disabled상태)- 해당 항목을 임시로 숨김 처리
🔧 수정 제안 (토스트 방식)
+ const handleNotReady = () => { + dispatch(toastActions.show({ text: '준비 중인 기능입니다.', variant: 'info' })); + }; const 설정Rows = [ { label: '테마 변경', icon: <ThemeIcon width="18px" height="18px" />, + onClick: handleNotReady, }, { label: '언어 설정', icon: <GroupIcon width="18px" height="18px" />, + onClick: handleNotReady, }, ];
92-98: 유지보수성: 변수명 일관성 검토한국어 변수명(
상담Rows,설정Rows,지원Rows)이 사용되고 있습니다. 팀 컨벤션에 따라 괜찮을 수 있으나, 국제화나 협업 확장 시 영문 변수명(consultationRows,settingsRows,supportRows)이 권장됩니다. 현재 팀 컨벤션을 따르고 있다면 무시하셔도 됩니다.
45-45: 가독성:noop함수명 개선 권장
noop이라는 이름은 일반적이지만 의도를 명확히 하기 위해handleNotImplemented등으로 변경하면 코드 리뷰 시 의도 파악이 더 쉬워집니다.- const noop = () => {}; + const handleNotImplemented = () => {};src/app/(public)/oauth/callback/[provider]/page.tsx (2)
93-97: 접근성 개선 필요: 로딩 상태 ARIA 속성 누락레이아웃 구조 변경은 PR의 일관성 있는 변경 방향과 부합합니다. 다만, 로딩 상태의 접근성을 개선할 수 있습니다.
src/components/ui/fallback/LoadingComponent.tsx에는role="status",aria-live="polite",aria-busy="true"속성이 있지만, 여기서 사용하는Loading컴포넌트에는 이러한 접근성 속성이 없습니다. 스크린 리더 사용자가 로딩 상태를 인지할 수 있도록 개선을 권장합니다.♿ 접근성 개선 제안
return ( - <div className="flex min-h-screen flex-col items-center justify-center p-12"> + <div + className="flex min-h-screen flex-col items-center justify-center p-12" + role="status" + aria-live="polite" + aria-busy="true" + > <Loading /> - <p className="text-sm text-gray-500">로그인 처리 중입니다...</p> + <p className="text-sm text-gray-500" aria-label="로그인 처리 중"> + 로그인 처리 중입니다... + </p> </div> );
54-90: 비동기 작업 중 컴포넌트 언마운트 처리 고려OAuth 로그인 로직은 전반적으로 잘 구현되어 있습니다. 다만, 비동기 작업 진행 중 사용자가 페이지를 이탈하면 언마운트된 컴포넌트에서 상태 업데이트가 시도될 수 있습니다.
현재 코드에서는
store.dispatch를 사용하므로 React 컴포넌트 상태가 아닌 Redux 상태를 업데이트하여 이 문제가 크게 영향을 미치지 않습니다. 그러나 향후 유지보수성을 위해 클린업 패턴을 고려해 볼 수 있습니다.🛡️ 방어적 코딩 패턴 (선택적)
useEffect(() => { if (called.current) return; called.current = true; + let isMounted = true; // ... validation logic ... (async () => { try { const res = await apiClient.post(`/api/auth/login/${provider}`, null, { params: { code }, }); // ... token handling ... + if (!isMounted) return; + store.dispatch(authActions.setAccessToken(accessToken)); // ... rest of logic ... } catch (e) { + if (!isMounted) return; console.error('OAuth 로그인 중 에러 발생:', e); // ... error handling ... } })(); + + return () => { + isMounted = false; + }; }, [router, searchParams, params?.provider]);src/app/(public)/layout.tsx (1)
14-31:clearTimeout호출 위치 개선 권장
clearTimeout이try와catch블록 모두에서 호출되고 있습니다.finally블록을 사용하면 코드 중복을 줄이고 타임아웃 정리가 보장됩니다.참고로
src/app/(private)/layout.tsx와src/app/(policy)/policy/page.tsx의callMe함수는 이미finally패턴을 사용하고 있어 일관성이 필요합니다.♻️ finally 블록 사용 제안
async function callMe(cookieHeader: string) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(`${API}/api/auth/me`, { method: 'GET', headers: cookieHeader ? { cookie: cookieHeader } : {}, cache: 'no-store', signal: controller.signal, }); - clearTimeout(timeoutId); return response; } catch (error) { - clearTimeout(timeoutId); console.error('Auth check failed:', error); return null; + } finally { + clearTimeout(timeoutId); } }
Key Changes
작업 내역
네비바의 아이콘을 SVG로 대체했습니다.
네비바의 중앙 아이콘의 그림자에 색상을 입혔습니다.
네비바 아이콘을 누르면 해당 페이지로 이동했을 때 색이 칠해집니다.
마이페이지에서 등급 삭제 및 프로필 사진 부분을 변경했습니다.
마이페이지 상담 횟수, 북마크 상담 수에 리다이렉트를 추가했습니다.
close: [URECA-77] Feat: 네비바 수정 #73
📸 스크린샷
💬 공유사항 to 리뷰어
비고
PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일
접근성
UI 변경
✏️ Tip: You can customize this high-level summary in your review settings.