All checks were successful
build-and-push / build (push) Successful in 32s
- 대시보드: 내게 배정된 작업을 상태별 JIRA 보드로, 프로젝트별 색상 카드(/my/tasks) - 캘린더 페이지(/calendar): 월 그리드, 분류(프로젝트/기타/개인)별 색 일정 추가·수정·삭제 - 메일 리스트 메모칸 위에 AI 자동 요약 표시 - projectColor() 헬퍼, 사이드바 CalendarDays 아이콘, 라우트 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
import { NavLink } from "react-router-dom";
|
|
import {
|
|
LayoutDashboard, Clock, FolderKanban, Coins, CheckSquare, Calculator,
|
|
Wallet, Users, Settings, FolderCog, Inbox, UserCircle, ClipboardList, Database, CalendarDays,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getNav, getApprovals } from "@/lib/api";
|
|
import { useAuth } from "@/context/Auth";
|
|
import { SpinLogo } from "./SpinLogo";
|
|
import { WorkStatusMenu } from "./WorkStatusMenu";
|
|
import { classNames } from "@/lib/format";
|
|
import type { NavItem } from "@/types";
|
|
|
|
export const ICONS: Record<string, LucideIcon> = {
|
|
LayoutDashboard, Clock, FolderKanban, Coins, CheckSquare, Calculator, Wallet, Users, Settings, FolderCog,
|
|
Inbox, UserCircle, ClipboardList, Database, CalendarDays,
|
|
};
|
|
|
|
export function Sidebar({ collapsed = false, className }: { collapsed?: boolean; className?: string }) {
|
|
const { isAdmin } = useAuth();
|
|
const navQ = useQuery({ queryKey: ["nav"], queryFn: getNav, staleTime: 5 * 60_000 });
|
|
const apprQ = useQuery({ queryKey: ["approvals-count"], queryFn: getApprovals, enabled: isAdmin, staleTime: 60_000 });
|
|
const approvalCount = (apprQ.data?.leave.length ?? 0) + (apprQ.data?.overtime.length ?? 0);
|
|
|
|
const items = navQ.data ?? [];
|
|
const sections = Array.from(new Set(items.map((i) => i.section)));
|
|
|
|
return (
|
|
<aside
|
|
className={classNames(
|
|
"shrink-0 bg-navy-sidebar text-white flex flex-col h-screen sticky top-0 transition-[width] duration-200 ease-out",
|
|
collapsed ? "w-[68px]" : "w-60",
|
|
className
|
|
)}
|
|
>
|
|
<div className={classNames("pt-6 pb-4", collapsed ? "px-0 flex justify-center" : "px-5")}>
|
|
<SpinLogo variant="light" markOnly={collapsed} />
|
|
</div>
|
|
|
|
{/* 출근/퇴근/휴식/미팅/이동 빠른 상태 설정 (로고 바로 아래) */}
|
|
<div className={classNames("pb-3", collapsed ? "px-2" : "px-5")}>
|
|
<WorkStatusMenu collapsed={collapsed} />
|
|
</div>
|
|
|
|
<nav className={classNames("flex-1 overflow-y-auto overflow-x-hidden pb-4", collapsed ? "px-2 space-y-2" : "px-3 space-y-4")}>
|
|
{sections.map((section, si) => (
|
|
<div key={section}>
|
|
{collapsed
|
|
? si > 0 && <div className="mx-2 my-2 h-px bg-white/10" />
|
|
: <div className="px-3 text-[10px] uppercase tracking-widest text-white/35 mb-1.5">{section}</div>}
|
|
<div className="space-y-0.5">
|
|
{items.filter((i) => i.section === section).map((item: NavItem) => {
|
|
const Icon = ICONS[item.icon] ?? LayoutDashboard;
|
|
return (
|
|
<NavLink
|
|
key={item.key}
|
|
to={item.path}
|
|
end={item.path === "/"}
|
|
title={collapsed ? item.label : undefined}
|
|
className={({ isActive }) =>
|
|
classNames(
|
|
"relative flex items-center rounded-control text-sm font-medium transition-colors",
|
|
collapsed ? "justify-center h-10" : "gap-3 px-3 py-2",
|
|
isActive ? "bg-white/10 text-white" : "text-white/60 hover:text-white hover:bg-white/5"
|
|
)
|
|
}
|
|
>
|
|
<Icon size={18} strokeWidth={2} />
|
|
{!collapsed && <span className="flex-1">{item.label}</span>}
|
|
{item.key === "approvals" && approvalCount > 0 && (
|
|
collapsed ? (
|
|
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-[#C99A2E]" />
|
|
) : (
|
|
<span className="font-num text-[11px] font-bold bg-[#C99A2E] text-[#1A1305] rounded-pill min-w-[20px] h-5 px-1.5 flex items-center justify-center">
|
|
{approvalCount}
|
|
</span>
|
|
)
|
|
)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{!collapsed && (
|
|
<div className="px-5 py-4 border-t border-white/10 flex flex-col items-center">
|
|
<div className="text-[10px] uppercase tracking-widest text-white/40 mb-2 text-center">Powered by</div>
|
|
{/* Logo sits directly on the rail (sidebar shares the logo's #03143F
|
|
navy), cropped 1px left / 3px bottom and scaled up. Centered. */}
|
|
<div className="overflow-hidden mx-auto" style={{ width: 143, height: 37 }}>
|
|
<img
|
|
src="/special-partners.jpg"
|
|
alt="Special Partners"
|
|
className="block"
|
|
style={{ height: 40, width: "auto", maxWidth: "none", marginLeft: -1 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|