Files
hanarang-dashboard/frontend/components/common/Sidebar.tsx
narang-ai 52732e88b9 feat(sprint-002): projects + task ledger + activity feed
Sprint 001 non-blocking 해결:
- N6: npm audit high 패키지 업데이트 (@nestjs/config, @nestjs/cli)
- S1: CORS origin 화이트리스트 (CORS_ORIGINS env)
- S3: DATABASE_URL 미설정 시 즉시 throw
- N2: SSH_KEY_PATH 미설정 시 즉시 throw (하드코딩 fallback 제거)
- N3: API 응답에서 내부 IP 제거
- N7: SshService 에러 메시지 마스킹 (IP 노출 차단)
- N4: theme.ts 색상 SisterCard/StatusBadge에 실제 적용
- N5: Sidebar-MainContent margin 연동 (SidebarContext)
- N9: LayoutShell Client 컴포넌트로 분리

Sprint 002 본문:
- TASK-004: GiteaService (org repo 목록, PR 조회)
- TASK-004: GET /api/projects → Gitea + DB 병합
- TASK-004: GET /api/projects/:id → 상세 + open PRs
- TASK-005: GET /api/projects/:id/tasks → Sprint/Task Ledger
- TASK-005: POST /api/projects/:id/sprints → Sprint 생성
- TASK-005: PATCH /api/tasks/:id → Task 상태 업데이트 + ActivityLog
- TASK-006: GET /api/activity, /api/projects/:id/activity
- TASK-007: /projects/[id] 상세 페이지 (SprintAccordion + TaskTable + 활동탭)
- TASK-007: 메인 대시보드에 프로젝트 섹션 + 활동 피드 추가
- 테스트 11/11 pass
2026-04-04 11:34:04 +09:00

144 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useSidebar } from '@/lib/SidebarContext';
const MENU_ITEMS = [
{ href: '/', icon: '⬛', label: '대시보드' },
{ href: '/projects', icon: '📁', label: '프로젝트' },
{ href: '/sisters', icon: '🦊', label: '자매' },
{ href: '/org', icon: '🏢', label: '조직도' },
{ href: '/settings', icon: '⚙️', label: '설정' },
];
const ADMIN_ITEMS = [
{ href: '/admin', icon: '🔧', label: '관리자' },
];
const Wrapper = styled.aside<{ $collapsed: boolean }>`
width: ${({ $collapsed }) => ($collapsed ? '60px' : '220px')};
height: 100vh;
background: rgba(13, 17, 23, 0.95);
backdrop-filter: blur(10px);
border-right: 1px solid rgba(240, 246, 252, 0.1);
display: flex;
flex-direction: column;
position: fixed;
left: 0;
top: 0;
z-index: 100;
transition: width 0.2s ease;
overflow: hidden;
`;
const LogoArea = styled.div`
padding: 20px 16px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid rgba(240, 246, 252, 0.08);
min-height: 64px;
`;
const LogoText = styled.span`
font-size: 15px;
font-weight: 700;
color: #E6EDF3;
white-space: nowrap;
`;
const CollapseBtn = styled.button`
margin-left: auto;
background: none;
border: none;
color: #8B949E;
cursor: pointer;
font-size: 16px;
padding: 4px;
border-radius: 4px;
flex-shrink: 0;
&:hover { color: #E6EDF3; background: rgba(240, 246, 252, 0.06); }
`;
const Nav = styled.nav`
flex: 1;
padding: 12px 8px;
display: flex;
flex-direction: column;
gap: 2px;
`;
const NavItem = styled(Link)<{ $active: boolean }>`
display: flex;
align-items: center;
gap: 10px;
padding: 10px 10px;
border-radius: 8px;
font-size: 14px;
color: ${({ $active }) => ($active ? '#E6EDF3' : '#8B949E')};
background: ${({ $active }) => ($active ? 'rgba(88, 166, 255, 0.12)' : 'transparent')};
transition: background 0.15s, color 0.15s;
white-space: nowrap;
overflow: hidden;
&:hover {
background: rgba(240, 246, 252, 0.06);
color: #E6EDF3;
}
`;
const NavIcon = styled.span`
font-size: 16px;
flex-shrink: 0;
`;
const Divider = styled.div`
height: 1px;
background: rgba(240, 246, 252, 0.08);
margin: 8px 8px;
`;
const AdminSection = styled.div`
padding: 8px;
`;
export default function Sidebar() {
const { collapsed, setCollapsed } = useSidebar();
const pathname = usePathname();
return (
<Wrapper $collapsed={collapsed}>
<LogoArea>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🦊</span>
{!collapsed && <LogoText></LogoText>}
<CollapseBtn onClick={() => setCollapsed(!collapsed)}>
{collapsed ? '' : ''}
</CollapseBtn>
</LogoArea>
<Nav>
{MENU_ITEMS.map((item) => (
<NavItem key={item.href} href={item.href} $active={pathname === item.href}>
<NavIcon>{item.icon}</NavIcon>
{!collapsed && item.label}
</NavItem>
))}
</Nav>
<Divider />
<AdminSection>
{ADMIN_ITEMS.map((item) => (
<NavItem key={item.href} href={item.href} $active={pathname === item.href}>
<NavIcon>{item.icon}</NavIcon>
{!collapsed && item.label}
</NavItem>
))}
</AdminSection>
</Wrapper>
);
}