-
Notifications
You must be signed in to change notification settings - Fork 3
Header, Drawer 리팩토링 #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Header, Drawer 리팩토링 #288
Conversation
Walkthrough관리자/사용자 헤더와 레이아웃 분기를 리팩터링하고 공통 Header 및 메뉴 컴포넌트(컨텍스트·트리거·메뉴·아이템 등), NavigationItems 및 클릭-아웃사이드 훅을 추가했으며 로그아웃 흐름에 쿠키/인증 리셋 호출을 통합했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as 사용자
participant H as Header (공통)
participant MC as MenuContainer
participant MT as MenuTrigger
participant M as Menu
participant MI as MenuItem
U->>MT: 메뉴 열기(클릭/마우스다운)
MT->>MC: setOpen(true)
MC-->>M: open=true, contentId 제공
M-->>U: 메뉴 렌더링(role="menu")
U->>MI: 항목 선택
MI->>MC: setOpen(false)
MI-->>U: 링크/동작 수행 (외부면 target="_blank")
sequenceDiagram
autonumber
actor Admin as 관리자
participant AH as AdminHeader
participant Store as Auth/Club 스토어
participant Cookie as useCookie
participant Router as next/navigation
Admin->>AH: 로그아웃 클릭
AH->>Store: resetAuth()
AH->>Store: resetClub()
AH->>Cookie: resetCookie()
AH->>Router: push("/login")
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 24
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (22)
src/app/providers.tsx
(1 hunks)src/components/layout/AdminHeader.tsx
(0 hunks)src/components/layout/Header/Header.tsx
(1 hunks)src/components/layout/Header/Menu.tsx
(1 hunks)src/components/layout/Header/MenuContext.tsx
(1 hunks)src/components/layout/Header/MenuItem.tsx
(1 hunks)src/components/layout/Header/MenuTrigger.tsx
(1 hunks)src/components/layout/Header/NavigationItem.tsx
(1 hunks)src/components/layout/Header/index.ts
(1 hunks)src/components/layout/Header/menu-context.ts
(1 hunks)src/components/layout/Header/useMenuCtx.ts
(1 hunks)src/components/layout/Layout.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(0 hunks)src/components/layout/header-kit/AdminHeader.tsx
(1 hunks)src/components/layout/header-kit/Logo.tsx
(1 hunks)src/components/layout/header-kit/NavigationItems.tsx
(1 hunks)src/components/layout/header-kit/UserHeader.tsx
(1 hunks)src/components/layout/header-kit/index.ts
(1 hunks)src/components/layout/index.ts
(1 hunks)src/components/layout/index.tsx
(0 hunks)src/constants/navItems.ts
(1 hunks)src/hooks/common/useClickOutside.ts
(1 hunks)
💤 Files with no reviewable changes (3)
- src/components/layout/AdminHeader.tsx
- src/components/layout/index.tsx
- src/components/layout/UserHeader.tsx
🧰 Additional context used
🧬 Code graph analysis (11)
src/components/layout/Layout.tsx (2)
src/components/layout/header-kit/AdminHeader.tsx (1)
AdminHeader
(11-45)src/components/layout/Footer.tsx (1)
Footer
(1-21)
src/components/layout/Header/Header.tsx (2)
src/components/layout/Header/MenuItem.tsx (1)
Props
(13-21)src/components/layout/Header/index.ts (1)
Header
(1-1)
src/components/layout/header-kit/UserHeader.tsx (3)
src/components/layout/header-kit/Logo.tsx (1)
Logo
(9-22)src/components/layout/header-kit/NavigationItems.tsx (1)
NavigationItems
(21-137)src/components/common/Drawer.tsx (1)
Drawer
(11-57)
src/components/layout/header-kit/AdminHeader.tsx (3)
src/store/auth.ts (1)
useAuthStore
(16-24)src/store/club.ts (1)
useClubStore
(12-27)src/app/_api/useCookie.ts (1)
useCookie
(5-30)
src/components/layout/Header/MenuContext.tsx (1)
src/components/layout/Header/menu-context.ts (1)
MenuContext
(10-10)
src/components/layout/Header/Menu.tsx (2)
src/components/layout/Header/useMenuCtx.ts (1)
useMenuCtx
(7-11)src/hooks/common/useClickOutside.ts (1)
useClickOutside
(9-24)
src/components/layout/Header/NavigationItem.tsx (2)
src/components/layout/Header/MenuItem.tsx (1)
Props
(13-21)src/components/layout/Header/index.ts (1)
NavigationItem
(2-2)
src/components/layout/Header/MenuTrigger.tsx (1)
src/components/layout/Header/useMenuCtx.ts (1)
useMenuCtx
(7-11)
src/components/layout/Header/MenuItem.tsx (1)
src/components/layout/Header/useMenuCtx.ts (1)
useMenuCtx
(7-11)
src/components/layout/header-kit/NavigationItems.tsx (6)
src/components/layout/Header/MenuItem.tsx (2)
Props
(13-21)MenuItem
(23-71)src/constants/navItems.ts (1)
navItems
(1-44)src/components/layout/Header/NavigationItem.tsx (1)
NavigationItem
(19-59)src/components/layout/Header/MenuContext.tsx (1)
MenuContainer
(16-33)src/components/layout/Header/MenuTrigger.tsx (1)
MenuTrigger
(13-39)src/components/layout/Header/Menu.tsx (1)
Menu
(15-41)
src/components/layout/Header/useMenuCtx.ts (1)
src/components/layout/Header/menu-context.ts (1)
MenuContext
(10-10)
🪛 Biome (2.1.2)
src/components/layout/header-kit/UserHeader.tsx
[error] 46-47: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form
element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🔇 Additional comments (5)
src/components/layout/index.ts (1)
1-1
: 배럴(export) 정리 LGTM
Layout
의 named export 노출 방식 일관성 좋습니다.src/components/layout/Header/index.ts (1)
1-6
: 바렐 익스포트 구성 적절헤더 관련 컴포넌트 집약 익스포트 좋아요. 사용처 단순화에 도움 됩니다.
src/constants/navItems.ts (1)
39-41
: 인스타그램 링크 업데이트 확인데이터 스키마와 사용처(이미지 옵셔널 처리)와도 일치합니다. 변경 사항 문제없습니다.
배포 환경에서 외부 링크가 새 탭으로 열리는지 확인해 주세요(컴포넌트에서 target/rel 처리 이미 포함).
src/components/layout/Header/Menu.tsx (1)
33-36
: className 순서/내용은 OK센터 포지셔닝과 최소 너비 지정이 의도대로 보입니다.
src/components/layout/header-kit/NavigationItems.tsx (1)
64-65
: Tailwind text-md 사용 확인 필요기본 Tailwind에는 text-md가 없습니다. 설정에 존재하는지 확인해 주세요. 없다면 text-base 등으로 교체해야 합니다.
import { MenuContext } from './menu-context'; | ||
|
||
export function useMenuCtx() { | ||
const ctx = useContext(MenuContext); | ||
if (!ctx) throw new Error('MenuContext is not provided'); | ||
return ctx; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
반환 타입 명시로 타입 안정성 강화
반환 타입을 MenuCtx
로 명시하면 사용처에서 null 체크 없이 안전하게 사용할 수 있습니다.
-import { MenuContext } from './menu-context';
+import { MenuContext, type MenuCtx } from './menu-context';
-export function useMenuCtx() {
+export function useMenuCtx(): MenuCtx {
const ctx = useContext(MenuContext);
if (!ctx) throw new Error('MenuContext is not provided');
return ctx;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
import { MenuContext } from './menu-context'; | |
export function useMenuCtx() { | |
const ctx = useContext(MenuContext); | |
if (!ctx) throw new Error('MenuContext is not provided'); | |
return ctx; | |
} | |
import { MenuContext, type MenuCtx } from './menu-context'; | |
export function useMenuCtx(): MenuCtx { | |
const ctx = useContext(MenuContext); | |
if (!ctx) throw new Error('MenuContext is not provided'); | |
return ctx; | |
} |
🤖 Prompt for AI Agents
In src/components/layout/Header/useMenuCtx.ts around lines 5-11, the hook
currently returns an untyped value; import the MenuCtx type from
'./menu-context' and annotate the hook's return type as MenuCtx (e.g. export
function useMenuCtx(): MenuCtx). Also call useContext with the generic type
useContext<MenuCtx>(MenuContext) so TypeScript knows the context shape; keep the
existing runtime null check/throw to satisfy non-null at runtime.
src/components/layout/Layout.tsx
Outdated
export function Layout({ children }: LayoutProps) { | ||
const host = window.location.hostname; | ||
const sub = host.split('.')[0]; | ||
const isAdminHost = sub === 'admin'; | ||
|
||
const path = window.location.pathname; | ||
const isLoginPage = path.endsWith('/login'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버/클라이언트 경계 위반: window 직접 접근 및 'use client' 누락
이 컴포넌트는 렌더 중 window
를 참조하고 있으나 파일 상단에 'use client'가 없습니다. App Router에서 Client 컴포넌트 없이 window
접근은 빌드/런타임 오류를 유발합니다.
다음과 같이 수정을 제안합니다:
- 상단에 'use client' 추가
- 경로 판별은
usePathname
사용 - 호스트 판별은
typeof window !== 'undefined'
가드 추가
+ 'use client';
+
+import { usePathname } from 'next/navigation';
import Footer from './Footer';
import { UserHeader } from './header-kit';
import AdminHeader from './header-kit/AdminHeader';
type LayoutProps = { children: React.ReactNode };
export function Layout({ children }: LayoutProps) {
- const host = window.location.hostname;
- const sub = host.split('.')[0];
+ const host = typeof window !== 'undefined' ? window.location.hostname : '';
+ const sub = host ? host.split('.')[0] : '';
const isAdminHost = sub === 'admin';
- const path = window.location.pathname;
- const isLoginPage = path.endsWith('/login');
+ const pathname = usePathname();
+ const isLoginPage = pathname?.endsWith('/login') ?? false;
return (
<>
{isAdminHost ? <AdminHeader /> : <UserHeader />}
<main className="flex min-h-screen w-full flex-col items-center bg-white text-gray-800">
- <div className="flex w-full max-w-6xl flex-col px-6 pt-22 md:px-16 md:pt-26">
+ <div className="flex w-full max-w-6xl flex-col px-6 pt-16 md:px-16">
{children}
</div>
</main>
{!isLoginPage && <Footer />}
</>
);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export function Layout({ children }: LayoutProps) { | |
const host = window.location.hostname; | |
const sub = host.split('.')[0]; | |
const isAdminHost = sub === 'admin'; | |
const path = window.location.pathname; | |
const isLoginPage = path.endsWith('/login'); | |
'use client'; | |
import { usePathname } from 'next/navigation'; | |
import Footer from './Footer'; | |
import { UserHeader } from './header-kit'; | |
import AdminHeader from './header-kit/AdminHeader'; | |
type LayoutProps = { children: React.ReactNode }; | |
export function Layout({ children }: LayoutProps) { | |
const host = typeof window !== 'undefined' ? window.location.hostname : ''; | |
const sub = host ? host.split('.')[0] : ''; | |
const isAdminHost = sub === 'admin'; | |
const pathname = usePathname(); | |
const isLoginPage = pathname?.endsWith('/login') ?? false; | |
return ( | |
<> | |
{isAdminHost ? <AdminHeader /> : <UserHeader />} | |
<main className="flex min-h-screen w-full flex-col items-center bg-white text-gray-800"> | |
<div className="flex w-full max-w-6xl flex-col px-6 pt-16 md:px-16"> | |
{children} | |
</div> | |
</main> | |
{!isLoginPage && <Footer />} | |
</> | |
); | |
} |
🤖 Prompt for AI Agents
In src/components/layout/Layout.tsx around lines 7 to 14, the component reads
window.location directly but lacks 'use client' and should use client-safe APIs;
add "use client" at the top of the file, import usePathname from next/navigation
and replace window.location.pathname usage with usePathname(), and guard host
logic with typeof window !== 'undefined' (e.g., compute host/sub only when
window is defined, falling back to a safe default) so no server-side access to
window occurs.
type UseClickOutsideProps<T extends HTMLElement = HTMLElement> = { | ||
ref: RefObject<T | null>; | ||
handler: () => void; | ||
enabled?: boolean; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
모바일/포인터 입력 지원 및 안전 가드 추가 제안
pointer 이벤트와 target 가드를 통해 모바일 터치와 펜 입력을 포함하고, handler에 이벤트를 전달하도록 개선하면 재사용성이 올라갑니다. passive 옵션으로 스크롤 성능도 확보할 수 있습니다.
-type UseClickOutsideProps<T extends HTMLElement = HTMLElement> = {
- ref: RefObject<T | null>;
- handler: () => void;
- enabled?: boolean;
-};
+type UseClickOutsideProps<T extends HTMLElement = HTMLElement> = {
+ ref: RefObject<T | null>;
+ handler: (event?: MouseEvent | TouchEvent | PointerEvent) => void;
+ enabled?: boolean;
+};
useEffect(() => {
if (!enabled) return;
- const listener = (event: MouseEvent) => {
- if (ref.current && !ref.current.contains(event.target as Node)) {
- handler();
- }
- };
- document.addEventListener('mousedown', listener);
- return () => document.removeEventListener('mousedown', listener);
+ const listener = (event: PointerEvent) => {
+ const target = event.target as Node | null;
+ if (!ref.current || !target) return;
+ if (!ref.current.contains(target)) {
+ handler(event);
+ }
+ };
+ document.addEventListener('pointerdown', listener, { passive: true });
+ return () => document.removeEventListener('pointerdown', listener);
}, [ref, handler, enabled]);
Also applies to: 14-23
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/components/layout/Layout.tsx (2)
26-28
: Tailwind spacing 토큰(pt-22 / md:pt-26) 재검토 필요프로젝트 Tailwind 설정에
22
,26
간격이 정의돼 있지 않아 해당 클래스가 적용되지 않습니다. 헤더가 고정(h-16
)이라 콘텐츠가 겹치는 문제가 계속됩니다. 존재하는 토큰(예:pt-16
)으로 교체하거나 Tailwindtheme.extend.spacing
에 값을 추가해 주세요.
9-16
:Layout
는 클라이언트 컴포넌트로 전환하고 라우팅 정보를 안전하게 읽어야 합니다.현재 파일에
'use client'
가 없어서 서버 컴포넌트로 렌더링되며,window
값은 빈 문자열로 평가됩니다. 그 결과 admin 서브도메인에서도UserHeader
가 렌더링되고,/login
경로에서도 Footer가 노출되는 등 런타임 동작이 깨집니다. 클라이언트 전용으로 전환하고usePathname()
/클라이언트 상태 등을 통해 호스트·경로를 안전하게 판별해 주세요.+'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; export function Layout({ children }: LayoutProps) { - const host = typeof window !== 'undefined' ? window.location.hostname : ''; - const sub = host.split('.')[0]; - const isAdminHost = sub === 'admin'; - - const path = typeof window !== 'undefined' ? window.location.pathname : ''; - const isLoginPage = path.endsWith('/login'); + const pathname = usePathname(); + const [host, setHost] = useState(''); + + useEffect(() => { + setHost(window.location.hostname); + }, []); + + const isAdminHost = host.split('.')[0] === 'admin'; + const isLoginPage = pathname?.endsWith('/login') ?? false;src/components/layout/NavigationItems.tsx (2)
98-114
:NavigationItem
에 허용되지 않은 props(target/rel) 전달
NavigationItem
내부에서 외부 링크를 감지해target
/rel
을 이미 설정합니다. 지금처럼 직접 전달하면 TS 타입 오류가 발생합니다. 조건부 스프레드를 제거해 주세요.<NavigationItem key={category} href={item.href} onClick={onItemClick} - {...(isExternal - ? { target: '_blank', rel: 'noopener noreferrer' } - : {})} >
74-81
: 모바일 아이콘 이미지에 의미 있는 대체 텍스트 제공
alt="icon"
은 스크린리더에 정보를 주지 못합니다. 항목 라벨을 활용하거나 장식용이면alt=""
로 처리해 주세요.- {item.image && ( + {item.image && ( <Image src={item.image} width={24} height={24} - alt="icon" + alt={item.content ?? category} className="mr-2" /> )}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
src/components/layout/AdminHeader.tsx
(1 hunks)src/components/layout/Layout.tsx
(1 hunks)src/components/layout/NavigationItems.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/layout/Layout.tsx (3)
src/components/layout/AdminHeader.tsx (1)
AdminHeader
(11-45)src/components/layout/UserHeader.tsx (1)
UserHeader
(10-96)src/components/layout/Footer.tsx (1)
Footer
(1-21)
src/components/layout/UserHeader.tsx (2)
src/lib/utils.ts (1)
cn
(4-6)src/components/layout/NavigationItems.tsx (1)
NavigationItems
(25-142)
src/components/layout/NavigationItems.tsx (1)
src/constants/navItems.ts (1)
navItems
(1-44)
src/components/layout/AdminHeader.tsx (5)
src/store/auth.ts (1)
useAuthStore
(16-24)src/store/club.ts (1)
useClubStore
(12-27)src/app/_api/useCookie.ts (1)
useCookie
(5-30)src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/Header/NavigationItem.tsx (1)
NavigationItem
(19-59)
🪛 Biome (2.1.2)
src/components/layout/UserHeader.tsx
[error] 73-74: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form
element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/components/layout/UserHeader.tsx (1)
72-74
: 모바일 메뉴 버튼에type="button"
을 명시해 주세요Line 72의
<button>
이 기본값인submit
상태라 헤더가 폼 안에 놓일 경우 의도치 않게 폼이 제출될 수 있습니다. lint도 같은 문제를 지적하고 있으니type="button"
을 추가해 주세요.- <button onClick={openModal} aria-label="Open navigation drawer"> + <button + type="button" + onClick={openModal} + aria-label="Open navigation drawer" + >src/components/layout/Header/MenuTrigger.tsx (1)
26-28
: 키보드 접근성을 위해onClick
으로 변경해 주세요Line 26에서
onMouseDown
만 사용하면 Enter/Space 등 키보드 입력으로 메뉴를 열 수 없습니다.onClick
으로 교체해 주세요.- onMouseDown={() => { - setOpen(!open); - }} + onClick={() => { + setOpen(!open); + }}src/components/layout/NavigationItems.tsx (1)
74-80
: 아이콘alt
텍스트를 의미 있게 수정해 주세요Line 79에서
alt="icon"
으로 고정돼 있어 스크린 리더가 항목을 식별할 수 없습니다. 항목 라벨이나 내용으로 대체 텍스트를 제공하거나 정말 장식용이라면 빈 문자열을 사용해 주세요.- alt="icon" + alt={item.content ?? category}src/components/layout/Header/MenuItem.tsx (1)
24-47
: Link 분기에서rel
누락과onClick
미호출을 해결해 주세요Line 24-47 구간에서
rel
이 Props에 정의돼 있지 않아NavigationItems
에서 넘기는 값이 타입 에러를 일으키고, 동시에 링크 분기에서onClick
콜백이 실행되지 않아onItemClick
등 상위 동작이 막히고 있습니다. 또한target="_blank"
일 때rel="noopener noreferrer"
가 빠져 보안상 취약합니다. 아래처럼 Props와 Link 분기를 수정해 주세요.export type Props = { children: ReactNode; onClick?: () => void; href?: string; target?: string; + rel?: string; icon?: IconName; className?: string; disabled?: boolean; }; export function MenuItem({ children, onClick, href, target, + rel, icon, className, disabled, }: Props) { const { setOpen } = useMenuCtx(); @@ if (href) { return ( <Link href={href} target={target} + rel={rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined)} className={common} role="menuitem" - onClick={() => setOpen(false)} + onClick={(event) => { + if (disabled) { + event.preventDefault(); + return; + } + onClick?.(); + setOpen(false); + }} > {icon && <Icon name={icon} size={16} />} {children} </Link> );
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
src/components/layout/Header/MenuItem.tsx
(1 hunks)src/components/layout/Header/MenuTrigger.tsx
(1 hunks)src/components/layout/NavigationItems.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(1 hunks)src/components/layout/index.tsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/components/layout/NavigationItems.tsx (3)
src/components/layout/Header/MenuItem.tsx (2)
Props
(13-21)MenuItem
(23-71)src/constants/navItems.ts (1)
navItems
(1-44)src/components/layout/Header/MenuTrigger.tsx (1)
MenuTrigger
(13-39)
src/components/layout/Header/MenuTrigger.tsx (2)
src/components/layout/Header/index.ts (1)
MenuTrigger
(5-5)src/components/layout/Header/useMenuCtx.ts (1)
useMenuCtx
(7-11)
src/components/layout/UserHeader.tsx (3)
src/lib/utils.ts (1)
cn
(4-6)src/components/layout/NavigationItems.tsx (1)
NavigationItems
(25-142)src/components/common/Drawer.tsx (1)
Drawer
(11-57)
src/components/layout/Header/MenuItem.tsx (2)
src/components/layout/Header/index.ts (1)
MenuItem
(6-6)src/components/layout/Header/useMenuCtx.ts (1)
useMenuCtx
(7-11)
src/components/layout/index.tsx (2)
src/components/layout/AdminHeader.tsx (1)
AdminHeader
(11-45)src/components/layout/UserHeader.tsx (1)
UserHeader
(10-102)
🪛 Biome (2.1.2)
src/components/layout/UserHeader.tsx
[error] 72-73: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form
element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/components/layout/Header/Header.tsx (1)
18-23
: 이전 리뷰 제안 확인 필요: 접근성을 위한 role="banner" 추가이전 리뷰에서 스크린리더 경험 개선을 위해 header 요소에
role="banner"
추가를 제안했습니다. 해당 제안이 아직 반영되지 않았습니다.다음 diff를 적용하여 접근성을 개선할 수 있습니다:
<header + role="banner" className={cn( 'fixed top-0 left-0 z-10 w-full border-b border-gray-200 bg-white px-6 md:z-20', className, )} >
src/components/layout/Header/NavigationItem.tsx (3)
11-17
: 이전 리뷰 제안 확인 필요: onClick 타입 개선이전 리뷰에서 onClick 제네릭을
MouseEventHandler<HTMLElement>
로 통일하여 타입 캐스팅을 제거하자는 제안이 있었으나 아직 반영되지 않았습니다.
32-42
: 이전 리뷰 제안 확인 필요: 외부 링크 판별 로직 공통화이전 리뷰에서 동일한 정규식이 여러 파일에서 반복되므로 공통 유틸(
lib/url.ts
의isExternalLink
)로 추출하자는 제안이 있었으나 아직 반영되지 않았습니다.
49-56
: 이전 리뷰 이슈 확인 필요: 버튼의 aria-current 제거이전 리뷰에서 버튼 요소에
aria-current
속성을 사용하는 것은 부적절하므로 제거해야 한다는 지적이 있었으나 아직 수정되지 않았습니다.aria-current
는 링크 요소에만 사용하는 것이 적절합니다.src/components/layout/UserHeader.tsx (1)
61-63
: 이전 리뷰 이슈 확인 필요: 모바일 메뉴 버튼에 type="button" 추가이전 리뷰와 정적 분석 도구(Biome)에서 모두 지적한 이슈가 아직 해결되지 않았습니다.
<button>
요소의 기본 타입은submit
이므로, 폼 내부에 위치할 경우 예기치 않은 폼 제출이 발생할 수 있습니다.다음 diff를 적용해주세요:
- <button onClick={openModal} aria-label="Open navigation drawer"> + <button + type="button" + onClick={openModal} + aria-label="Open navigation drawer" + > <Icon name="list" size={29} color="black" /> </button>
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (5)
package.json
(1 hunks)src/components/layout/AdminHeader.tsx
(1 hunks)src/components/layout/Header/Header.tsx
(1 hunks)src/components/layout/Header/NavigationItem.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/layout/UserHeader.tsx (3)
src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/NavigationItems.tsx (1)
NavigationItems
(25-142)src/components/common/Drawer.tsx (1)
Drawer
(11-57)
src/components/layout/Header/NavigationItem.tsx (1)
src/components/layout/Header/MenuItem.tsx (1)
Props
(13-21)
src/components/layout/Header/Header.tsx (1)
src/components/layout/Header/MenuItem.tsx (1)
Props
(13-21)
src/components/layout/AdminHeader.tsx (5)
src/store/auth.ts (1)
useAuthStore
(16-24)src/store/club.ts (1)
useClubStore
(12-27)src/app/_api/useCookie.ts (1)
useCookie
(5-30)src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/Header/NavigationItem.tsx (1)
NavigationItem
(19-59)
🪛 Biome (2.1.2)
src/components/layout/UserHeader.tsx
[error] 61-62: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form
element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🔇 Additional comments (3)
src/components/layout/UserHeader.tsx (1)
65-86
: LGTM! usePortal을 활용한 모바일 드로어 구현
usePortal
훅을 사용하여 모달 상태를 관리하고,Drawer
컴포넌트로 모바일 내비게이션을 렌더링하는 구조가 깔끔합니다.NavigationItems
에onItemClick={closeModal}
을 전달하여 메뉴 항목 클릭 시 드로어가 닫히도록 한 점도 좋습니다.src/components/layout/AdminHeader.tsx (2)
3-3
: LGTM! Next.js 15 마이그레이션 올바르게 적용
next/router
에서next/navigation
으로의 변경은 Next.js 15의 권장 패턴입니다.usePathname
훅을 사용하여 경로 확인 로직이 올바르게 업데이트되었습니다.
38-42
: 로그아웃 NavigationItem의 접근성 확인
NavigationItem
컴포넌트가href
없이onClick
만 전달되면<button>
요소로 렌더링됩니다.NavigationItem.tsx
구현을 확인한 결과, 버튼은type="button"
을 명시하고 있어 폼 제출 문제는 없습니다. 다만, 해당 컴포넌트에는aria-current
가 버튼에도 적용되는 접근성 이슈가 있으니 NavigationItem.tsx 파일의 리뷰 코멘트를 참고해주세요.
const handleLogout = () => { | ||
resetAuth(); | ||
resetClub(); | ||
resetCookie(); | ||
router.push('/login'); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
로그아웃 흐름에 에러 핸들링 추가 권장
resetAuth
, resetClub
, resetCookie
호출이 동기적으로 실행되고 있으나, 쿠키 삭제나 스토어 리셋 중 예외가 발생할 경우를 대비한 에러 핸들링이 없습니다. 사용자 경험을 위해 try-catch로 에러를 처리하고, 실패 시에도 로그인 페이지로 이동하도록 보장하는 것이 좋습니다.
다음과 같이 수정할 수 있습니다:
const handleLogout = () => {
- resetAuth();
- resetClub();
- resetCookie();
- router.push('/login');
+ try {
+ resetAuth();
+ resetClub();
+ resetCookie();
+ } catch (error) {
+ console.error('Logout error:', error);
+ } finally {
+ router.push('/login');
+ }
};
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const handleLogout = () => { | |
resetAuth(); | |
resetClub(); | |
resetCookie(); | |
router.push('/login'); | |
}; | |
const handleLogout = () => { | |
try { | |
resetAuth(); | |
resetClub(); | |
resetCookie(); | |
} catch (error) { | |
console.error('Logout error:', error); | |
} finally { | |
router.push('/login'); | |
} | |
}; |
🤖 Prompt for AI Agents
In src/components/layout/AdminHeader.tsx around lines 19 to 24, the logout flow
calls resetAuth(), resetClub(), resetCookie() without error handling; wrap these
calls in a try-catch so any exception during cookie deletion or store resets is
caught, log or handle the error (e.g., console.error or a logger), and ensure
router.push('/login') is executed in a finally block or after the catch so
navigation to the login page always occurs even if resets fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/components/layout/index.tsx (1)
10-17
: SSR에서 호스트 감지가 실패합니다.서버 렌더에서는
window
가 존재하지 않아host
가 공백으로 남고, 최초 SSR 결과가 항상 사용자 헤더가 되어 관리자 호스트에서도 하이드레이션 시 DOM이 뒤바뀝니다. Next의headers()
등 서버 API로 호스트 값을 계산하거나, 클라이언트 컴포넌트로 전환해 초기 렌더와 클라이언트 렌더를 일치시켜 주세요.+import { headers } from 'next/headers'; + export default function Layout({ children }: Props) { - const host = typeof window !== 'undefined' ? window.location.hostname : ''; + const host = headers().get('host') ?? ''; const sub = host.split('.')[0];
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
src/components/layout/AdminHeader.tsx
(1 hunks)src/components/layout/Footer.tsx
(1 hunks)src/components/layout/Header/NavigationItem.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(2 hunks)src/components/layout/index.tsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/layout/index.tsx (2)
src/components/layout/AdminHeader.tsx (1)
AdminHeader
(12-46)src/components/layout/UserHeader.tsx (1)
UserHeader
(9-95)
src/components/layout/UserHeader.tsx (3)
src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/NavigationItems.tsx (1)
NavigationItems
(25-142)src/components/common/Drawer.tsx (1)
Drawer
(11-57)
src/components/layout/Header/NavigationItem.tsx (2)
src/components/layout/Header/MenuItem.tsx (1)
Props
(13-21)src/components/layout/Header/index.ts (1)
NavigationItem
(2-2)
src/components/layout/AdminHeader.tsx (5)
src/store/auth.ts (1)
useAuthStore
(16-24)src/store/club.ts (1)
useClubStore
(12-27)src/app/_api/useCookie.ts (1)
useCookie
(5-30)src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/Header/NavigationItem.tsx (1)
NavigationItem
(19-54)
🪛 Biome (2.1.2)
src/components/layout/Footer.tsx
[error] 21-21: The elements with this role can be changed to the following elements:
For examples and more information, see WAI-ARIA Roles
(lint/a11y/useSemanticElements)
🔇 Additional comments (7)
src/components/layout/Footer.tsx (7)
1-5
: 클라이언트 컴포넌트 전환 확인 필요이 컴포넌트가 'use client'로 변경되어 클라이언트 측에서만 실행됩니다.
usePathname
훅 사용을 위해 필요한 변경이지만, 이로 인해 서버 사이드 렌더링 시 푸터가 초기 HTML에 포함되지 않습니다. SEO 및 초기 렌더링 성능에 영향을 줄 수 있습니다.다음 사항을 확인해 주세요:
- 푸터가 SEO에 중요한 콘텐츠를 포함하는지 여부
- 초기 로딩 시 레이아웃 시프트(layout shift) 발생 가능성
16-16
: 동적 연도 계산 승인매년 수동으로 업데이트할 필요 없이 저작권 연도를 자동으로 갱신하는 좋은 구현입니다.
28-36
: 외부 링크 보안 속성 승인
target="_blank"
와 함께rel="noopener noreferrer"
속성을 사용하고prefetch={false}
를 설정하여 보안과 성능을 고려한 좋은 구현입니다.
38-40
: 동적 저작권 표시 승인템플릿 리터럴을 사용한 동적 연도 표시로 매년 자동 갱신되는 저작권 문구가 올바르게 구현되었습니다.
42-50
: 시맨틱 HTML 사용 승인이메일 주소에
<address>
요소와mailto
링크를 사용하여 시맨틱하고 접근성이 좋은 마크업을 작성했습니다.
9-9
: 기본 내보내기 함수명 변경은 import에 영향 없음
export default function FooterGate()
로 변경해도import Footer from './Footer'
는 모듈의 default export를 받아오기 때문에 빌드 오류가 발생하지 않습니다.Likely an incorrect or invalid review comment.
7-14
: 경로 매칭 로직 검증 완료:/login
로 끝나는 라우트는 현재/admin/login
한 곳뿐입니다
해당 조건은 오직src/app/admin/login/page.tsx
경로에서만 푸터를 숨기므로, 다른 로그인 페이지가 없다면 의도된 동작입니다.
<Flex | ||
as="footer" | ||
role="contentinfo" | ||
dir="row" | ||
justifyContent="center" | ||
alignItems="stretch" | ||
className="mt-14 w-full bg-gray-50 md:mt-20" | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
중복된 role 속성 제거 필요
as="footer"
로 시맨틱 <footer>
요소를 렌더링하는 경우, role="contentinfo"
는 중복됩니다. <footer>
요소는 이미 암묵적으로 contentinfo 역할을 가지고 있습니다.
다음 diff를 적용하여 중복 속성을 제거하세요:
<Flex
as="footer"
- role="contentinfo"
dir="row"
justifyContent="center"
alignItems="stretch"
className="mt-14 w-full bg-gray-50 md:mt-20"
>
Based on static analysis hints
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<Flex | |
as="footer" | |
role="contentinfo" | |
dir="row" | |
justifyContent="center" | |
alignItems="stretch" | |
className="mt-14 w-full bg-gray-50 md:mt-20" | |
> | |
<Flex | |
as="footer" | |
dir="row" | |
justifyContent="center" | |
alignItems="stretch" | |
className="mt-14 w-full bg-gray-50 md:mt-20" | |
> |
🧰 Tools
🪛 Biome (2.1.2)
[error] 21-21: The elements with this role can be changed to the following elements:
For examples and more information, see WAI-ARIA Roles
(lint/a11y/useSemanticElements)
🤖 Prompt for AI Agents
In src/components/layout/Footer.tsx around lines 19 to 26, remove the redundant
role="contentinfo" when rendering as="footer" (the native <footer> already has
that implicit role); update the JSX to omit the role prop (or conditionally set
it only when `as` is not "footer") so the rendered footer element no longer has
a duplicate role attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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/components/layout/UserHeader.tsx (1)
23-32
: 로고 렌더링의 중복을 제거하세요.로고 Image 컴포넌트가 세 곳에서 반복됩니다(데스크탑 헤더, 모바일 헤더, Drawer 내부). 유지보수성과 일관성을 위해 재사용 가능한
Logo
컴포넌트로 추출하는 것을 권장합니다.예시:
function Logo({ variant = 'default', className = '' }: { variant?: 'default' | 'mobile'; className?: string }) { const src = variant === 'mobile' ? '/m_logo.png' : '/logo.png'; const width = variant === 'mobile' ? 'w-30' : 'w-40 md:w-44'; return ( <Image src={src} width={1544} height={380} priority alt="ddingdong" className={cn(width, className)} /> ); }그런 다음 각 위치에서 다음과 같이 사용할 수 있습니다:
<Link href="/" className="inline-block"> <Logo /> </Link>Also applies to: 52-61, 72-81
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/components/layout/AdminHeader.tsx
(1 hunks)src/components/layout/UserHeader.tsx
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/layout/UserHeader.tsx (3)
src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/NavigationItems.tsx (1)
NavigationItems
(25-142)src/components/common/Drawer.tsx (1)
Drawer
(11-57)
src/components/layout/AdminHeader.tsx (5)
src/store/auth.ts (1)
useAuthStore
(16-24)src/store/club.ts (1)
useClubStore
(12-27)src/app/_api/useCookie.ts (1)
useCookie
(5-30)src/components/layout/Header/Header.tsx (1)
Header
(16-29)src/components/layout/Header/NavigationItem.tsx (1)
NavigationItem
(19-54)
🔇 Additional comments (3)
src/components/layout/UserHeader.tsx (3)
45-45
: 모바일 헤더의 z-index 재정의를 검증하세요.모바일 헤더에
z-0
를 명시적으로 설정하여Header
컴포넌트의 기본z-10
을 재정의하고 있습니다. 이는 Drawer가 헤더 위에 표시되도록 하기 위한 의도로 보이지만, 다른 UI 요소(예: 토스트, 모달 등)와의 스택 컨텍스트 충돌을 일으킬 수 있습니다. 모바일 레이아웃에서 z-index 계층 구조가 올바른지 확인해 주세요.
70-70
: ‘ddingdong-design-system’ 패키지에서 Drawer API 위치 및 정의 직접 확인 필요
‘ddingdong-design-system’ 모듈이 node_modules 하위에 보이지 않아 자동 검증이 불가능합니다. 패키지 위치(로컬 패키지, 모노레포 등)를 확인하고, 해당 소스나 타입 선언에서 Drawer 컴포넌트에isOpen
과onClose
prop이 정의되어 있는지 검증해 주세요.
10-11
:usePortal
훅 API 사용 수동 검증 필요
isOpen
,openModal
,closeModal
의 시그니처와 동작이 패키지 문서나 타입 정의에서 예상대로인지 로컬 환경에서 확인해 주세요.
const curPath = router.pathname; | ||
const isLoginPage = curPath.endsWith('login'); | ||
const pathname = usePathname(); | ||
const isLoginPage = pathname.endsWith('/login'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그인 페이지 감지 로직이 너무 포괄적입니다.
pathname.endsWith('/login')
은 /admin/login
, /user/login
, /any/path/login
등 모든 /login
으로 끝나는 경로와 매칭됩니다. 관리자 헤더의 경우 정확한 경로 매칭을 사용하는 것이 더 안전합니다.
다음과 같이 정확한 경로 매칭으로 수정하세요:
- const isLoginPage = pathname.endsWith('/login');
+ const isLoginPage = pathname === '/admin/login';
또는 관리자 영역의 여러 로그인 경로를 지원해야 한다면:
- const isLoginPage = pathname.endsWith('/login');
+ const isLoginPage = pathname === '/admin/login' || pathname === '/login';
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const isLoginPage = pathname.endsWith('/login'); | |
const isLoginPage = pathname === '/admin/login'; |
🤖 Prompt for AI Agents
In src/components/layout/AdminHeader.tsx around line 17, the current
pathname.endsWith('/login') is too broad and matches any path ending with
/login; replace it with an exact admin path check (e.g. pathname ===
'/admin/login') or, if multiple admin login routes must be supported, define an
array of allowed admin login paths and set isLoginPage =
allowedAdminLoginPaths.includes(pathname) so only the intended admin login
routes match.
)} | ||
</div> | ||
</header> | ||
<Header className="px-3 md:px-13"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
비표준 Tailwind 클래스 사용 확인 필요
px-13
은 Tailwind CSS의 기본 간격 스케일에 포함되지 않습니다. 프로젝트의 Tailwind 설정에서 커스텀으로 확장된 것인지 확인이 필요합니다. 만약 의도하지 않은 사용이라면 px-12
또는 px-14
같은 표준 클래스로 교체하세요.
다음 스크립트로 Tailwind 설정을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Tailwind 설정 파일에서 커스텀 spacing 확장 여부 확인
fd -e js -e ts -e cjs -e mjs 'tailwind.config' --exec cat {}
Length of output: 1938
src/components/layout/AdminHeader.tsx:28의 px-13
클래스 수정 필요
현재 Tailwind 설정에 px-13
이 정의되어 있지 않아 스타일이 적용되지 않습니다. px-12
또는 px-14
등 기본 스케일 중 의도한 값으로 교체하세요.
🤖 Prompt for AI Agents
src/components/layout/AdminHeader.tsx around line 28: the className uses "px-13"
which is not defined in the Tailwind config so padding isn't applied; replace
"px-13" with a valid Tailwind spacing class such as "px-12" or "px-14" (choose
the one matching the intended layout) and commit the change so the header
padding uses the defined scale.
<Icon | ||
name="close" | ||
color="white" | ||
size={18} | ||
onClick={closeModal} | ||
className="cursor-pointer" | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Icon 클릭 요소의 접근성을 개선하세요.
Icon
컴포넌트를 onClick
핸들러로 직접 클릭 가능하게 만들고 있습니다. Icon
이 내부적으로 버튼 역할을 하지 않는다면, 키보드 탐색과 스크린 리더 사용자가 이 요소에 접근할 수 없습니다. 접근성을 위해 적절한 aria-label
이 있는 button
요소로 감싸주세요.
다음 diff를 적용하여 접근성을 개선하세요:
- <Icon
- name="close"
- color="white"
- size={18}
- onClick={closeModal}
- className="cursor-pointer"
- />
+ <button
+ type="button"
+ onClick={closeModal}
+ aria-label="Close navigation drawer"
+ className="cursor-pointer"
+ >
+ <Icon
+ name="close"
+ color="white"
+ size={18}
+ />
+ </button>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<Icon | |
name="close" | |
color="white" | |
size={18} | |
onClick={closeModal} | |
className="cursor-pointer" | |
/> | |
<button | |
type="button" | |
onClick={closeModal} | |
aria-label="Close navigation drawer" | |
className="cursor-pointer" | |
> | |
<Icon | |
name="close" | |
color="white" | |
size={18} | |
/> | |
</button> |
🤖 Prompt for AI Agents
In src/components/layout/UserHeader.tsx around lines 82 to 88, the Icon is
clickable but not keyboard/screen-reader accessible; wrap the Icon inside a
semantic <button type="button"> with an appropriate aria-label (e.g., "Close"),
move the onClick handler and cursor/focus classes to that button, and remove the
direct onClick from the Icon so keyboard users can activate it and screen
readers receive a proper name; also ensure the button has visible focus styles
and any existing Icon props remain unchanged.
🔥 연관 이슈
🚀 작업 내용
useOutsideClick
훅을 적용했습니다.Layout.tsx
에 기존 헤더 컴포넌트를 작업한 헤더 컴포넌트를 적용하여 반영했습니다.🤔 고민했던 내용
navigationItems
컴포넌트로 추출하여 같은 데이터를 공유하도록 처리했습니다💬 리뷰 중점사항
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Chores