이 글은 Claude Code로 요약하였습니다
Figma 디자인 분석 → 컴포넌트 구조 결정
↓
GNB 개발 + Storybook
↓
AccordionMenu 구현 (내부 전용)
↓
LeftPanel 개발 + Storybook
↓
AdminLayout 조합
↓
CLAUDE.md 업데이트
Figma MCP로 관리자 레이아웃 디자인을 분석한 결과, 세 개의 컴포넌트와 하나의 내부 전용 헬퍼로 구조를 결정했다.
src/shared/components/layout/
GNB.tsx / GNB.module.css / GNB.stories.tsx
LeftPanel.tsx / LeftPanel.module.css / LeftPanel.stories.tsx
AdminLayout.tsx / AdminLayout.module.css / AdminLayout.stories.tsx
AccordionMenu.tsx / AccordionMenu.module.css ← 내부 전용 (export 안 함)
index.ts
반응형 breakpoint:
SVG 로고 추출 불가 문제: Figma MCP의 get_design_context는 복잡한 벡터를 래스터 이미지 URL로 반환하기 때문에 SVG를 직접 추출할 수 없다. DefaultLogo 플레이스홀더를 사용하고, 실제 SVG는 Figma에서 수동으로 내보내야 한다.
GNB는 justify-content: space-between의 flex 컨테이너로, 좌·중앙·우 세 영역을 CSS 미디어 쿼리만으로 반응형 처리한다.
| 구간 | 좌측 | 중앙 | 우측 |
|---|---|---|---|
| PC (≥1200px) | 햄버거 + 로고 | — | "안녕하세요," + 이름님 + 알림 |
| TAB (800–1199px) | 햄버거 + 로고 | — | 이름님 + 알림 |
| MO (≤799px) | 햄버거 | pageName | 알림 |
중앙 pageName은 position: absolute; left: 50%; transform: translateX(-50%)로 배치해 좌우 영역 너비와 무관하게 항상 정중앙에 고정된다.
// isPanelOpen 값으로 fold/unfold 아이콘 전환
<IconButton
icon={<i className={isPanelOpen ? 'ri-menu-fold-line' : 'ri-menu-unfold-line'} />}
onClick={onMenuToggle}
/>
함수 props(onMenuToggle, onProfileClick 등)는 Storybook Controls 패널에 노출되면 노이즈가 된다. argTypes로 숨긴다.
const meta: Meta<typeof GNB> = {
component: GNB,
argTypes: {
onMenuToggle: { table: { disable: true } },
onProfileClick: { table: { disable: true } },
onNotificationClick: { table: { disable: true } },
},
};
LeftPanel의 "모집 공고", "메세지", "동아리명" 섹션이 공통적으로 펼침/접힘 동작이 필요해서 추출한 내부 컴포넌트다. layout/index.ts에서 export하지 않는다.
기존 Menu 컴포넌트의 showArrowIcon + active prop을 활용해 트리거를 구성한다. active=true이면 화살표가 아래를 가리키고, false이면 오른쪽을 가리킨다.
export function AccordionMenu({ label, menuIcon, defaultOpen = false, triggerClassName, children }) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className={styles.root}>
<Menu menuIcon={menuIcon} showArrowIcon active={isOpen}
onClick={() => setIsOpen(v => !v)} className={triggerClassName}>
{label}
</Menu>
{isOpen && <div className={styles.subList}>{children}</div>}
</div>
);
}
triggerClassName을 외부에서 주입받아 동아리명 트리거에만 font-weight: 600을 적용한다.
두 breakpoint에서 슬라이드 방식이 다르다.
PC (≥1200px) — width 기반 in-flow:
패널이 flex 자식 요소로 flow에 포함된다. width: 0에서 width: 280px로 트랜지션하면서 콘텐츠 영역이 자연스럽게 밀려난다.
gap 대신 패널의 margin-right를 함께 트랜지션시키는 이유: gap은 width가 0이 되어도 사라지지 않기 때문이다.
@media (min-width: 1200px) {
.panel {
width: 0;
padding: 0;
margin-right: 0;
transition: width 0.25s ease, padding 0.25s ease, margin-right 0.25s ease;
}
.panelOpen {
width: 280px;
padding: 8px;
margin-right: 20px;
}
}
TAB/MO (<1200px) — translateX 기반 fixed overlay:
패널이 position: fixed로 flow에서 빠져나온다. translateX(-100%)에서 translateX(0)으로 트랜지션한다.
@media (max-width: 1199px) {
.panel {
position: fixed;
top: 0; left: 0;
height: 100dvh;
transform: translateX(-100%);
transition: transform 0.25s ease, box-shadow 0.25s ease;
z-index: 100;
}
.panelOpen {
transform: translateX(0);
box-shadow: var(--shadow-modal);
}
}
백드롭은 TAB/MO에서만 display: block으로 전환되고, 클릭 시 onClose를 호출한다.
activeMenu 값에 따라 해당 아코디언을 마운트 시 자동으로 열어둔다.
const isRecruitmentActive = (key?: string) =>
!!key && (key === 'recruitment-new' || key.startsWith('recruitment-'));
<AccordionMenu defaultOpen={isRecruitmentActive(activeMenu)}>
기본 args만으로는 메뉴 클릭 시 활성 상태가 바뀌는 걸 Storybook에서 확인할 수 없다. meta의 render에 useState를 써서 모든 스토리가 실제 인터랙션을 갖도록 한다.
const meta: Meta<typeof LeftPanel> = {
component: LeftPanel,
render: (args) => {
const [activeMenu, setActiveMenu] = useState(args.activeMenu);
return <LeftPanel {...args} activeMenu={activeMenu} onNavigate={setActiveMenu} />;
},
};
GNB + LeftPanel + 콘텐츠 영역을 조합하는 최상위 레이아웃. isPanelOpen 상태를 내부에서 관리해 GNB의 햄버거 버튼과 LeftPanel의 닫기 버튼/백드롭을 연동한다.
export function AdminLayout({ children, ...props }) {
const [isPanelOpen, setIsPanelOpen] = useState(true);
return (
<div className={styles.layout}>
<GNB isPanelOpen={isPanelOpen} onMenuToggle={() => setIsPanelOpen(v => !v)} {...gnbProps} />
<div className={styles.body}>
<LeftPanel isOpen={isPanelOpen} onClose={() => setIsPanelOpen(false)} {...panelProps} />
<main className={styles.content}>{children}</main>
</div>
</div>
);
}
isPanelOpen은 PC에서도 동작한다 — true이면 280px width로 슬라이드 인, false이면 width 0으로 슬라이드 아웃되어 콘텐츠 영역이 전체 너비를 차지한다.
구현 완료 후 src/shared/components/CLAUDE.md를 실제 구현과 일치하도록 업데이트했다. props 목록, 메뉴 key 테이블, 반응형 동작, isPanelOpen 연동 방식을 정확히 반영했다.