- Backend: NestJS + Prisma 7 (MariaDB adapter) scaffold - PrismaService with @prisma/adapter-mariadb driver - SistersService: SSH 상태 체크 with graceful fallback - HealthController: GET /health - 시드 스크립트: 자매 4명 초기 데이터 - 테스트 5/5 pass - Frontend: Next.js 16 + styled-components - styled-components SSR registry (next.config 컴파일러) - 다크 테마 글로벌 스타일 + 테마 토큰 - SisterCard: glassmorphism 상태 카드 (온라인 pulse 애니메이션) - StatusBadge: 상태 표시 컴포넌트 - Sidebar: 접이식 네비게이션 - 대시보드 메인 페이지 (API 미연결 시 mock 데이터 fallback) - build 성공 확인 - DB credential 이랑이 대기 중
143 lines
3.3 KiB
TypeScript
143 lines
3.3 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState } from 'react';
|
||
import styled from 'styled-components';
|
||
import Link from 'next/link';
|
||
import { usePathname } from 'next/navigation';
|
||
|
||
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] = useState(false);
|
||
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>
|
||
);
|
||
}
|