From 25e13d7b01b80c8c4df890044c62051ae946c63a Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Thu, 16 Jul 2026 16:24:40 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20negodata/front:=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=EB=89=B4=EC=96=BC=201=EC=B0=A8=20?= =?UTF-8?q?=E2=80=94=20=EC=9D=B8=EB=94=94=EA=B3=A0=20=EC=BD=98=EC=86=94=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=C2=B7=E2=8C=98K=20=ED=81=AC=EB=A1=AC=C2=B7?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C/=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8/=EC=95=8C=EB=A6=BC=ED=95=A8=20=EA=B0=9C=ED=8E=B8,=20?= =?UTF-8?q?=ED=88=B4=EB=B0=94=20=ED=86=B5=EC=9D=BC,=20=EC=8B=9C=ED=8A=B8?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../front/src/components/layout/Layout.tsx | 263 +++++++++++++---- .../src/components/layout/PageToolbar.tsx | 19 +- negodata/front/src/components/ui/card.tsx | 2 +- .../front/src/components/ui/data-table.tsx | 5 +- negodata/front/src/components/ui/sheet.tsx | 13 +- .../dashboard/components/ActionWidget.tsx | 72 ----- .../dashboard/components/DashboardHero.tsx | 19 +- .../dashboard/components/DeadlineWidget.tsx | 28 -- .../components/EmailUnsentWidget.tsx | 27 -- .../dashboard/components/KpiStrip.tsx | 25 ++ .../features/dashboard/components/KpiTile.tsx | 39 --- .../dashboard/components/RefWidget.tsx | 37 --- .../dashboard/components/ScopeSection.tsx | 45 ++- .../dashboard/components/WorkTable.tsx | 82 +++++ .../components/QuotationDetailSheet/index.tsx | 12 +- negodata/front/src/pages/cards.tsx | 94 +++--- negodata/front/src/pages/login.tsx | 148 ++++++---- negodata/front/src/pages/notifications.tsx | 279 +++++++++++++----- negodata/front/src/pages/quotation.tsx | 35 +-- negodata/front/src/tokens.css | 141 ++++----- 20 files changed, 805 insertions(+), 580 deletions(-) delete mode 100644 negodata/front/src/features/dashboard/components/ActionWidget.tsx delete mode 100644 negodata/front/src/features/dashboard/components/DeadlineWidget.tsx delete mode 100644 negodata/front/src/features/dashboard/components/EmailUnsentWidget.tsx create mode 100644 negodata/front/src/features/dashboard/components/KpiStrip.tsx delete mode 100644 negodata/front/src/features/dashboard/components/KpiTile.tsx delete mode 100644 negodata/front/src/features/dashboard/components/RefWidget.tsx create mode 100644 negodata/front/src/features/dashboard/components/WorkTable.tsx diff --git a/negodata/front/src/components/layout/Layout.tsx b/negodata/front/src/components/layout/Layout.tsx index e9589ab..eef9f3c 100644 --- a/negodata/front/src/components/layout/Layout.tsx +++ b/negodata/front/src/components/layout/Layout.tsx @@ -1,10 +1,11 @@ -import { useState, type ReactNode, type ElementType } from 'react'; +import { useEffect, useState, type ReactNode, type ElementType } from 'react'; import { PageType } from '@/types'; import { useAuth } from '@/features/auth/useAuth'; import { ProfileSheet } from '@/features/auth/components/ProfileSheet'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Typography } from '@/components/ui/typography'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; import { NotificationBell } from './NotificationBell'; import { @@ -17,6 +18,7 @@ import { FileSpreadsheet, Layers, LogOut, + Search, Sun, Moon, Building, @@ -34,17 +36,34 @@ interface LayoutProps { type SidebarUser = ReturnType['user']; +type MenuItem = { type: PageType; label: string; icon: ElementType; id: string; ownerOnly?: boolean }; + // ownerOnly 항목은 최고관리자에게만 노출된다(렌더 시 user.role 로 필터). -const menuItems: { type: PageType; label: string; icon: ElementType; id: string; ownerOnly?: boolean }[] = [ - { type: 'DASHBOARD', label: '대시보드', icon: LayoutDashboard, id: 'sidebar-dashboard' }, - { type: 'STATISTICS', label: '통계', icon: BarChart3, id: 'sidebar-statistics' }, - { type: 'PRODUCTS', label: '상품관리', icon: Briefcase, id: 'sidebar-products' }, - { type: 'PARTNERS', label: '협력사관리', icon: Users, id: 'sidebar-partners' }, - { type: 'QUOTATION', label: '견적관리', icon: FileSpreadsheet, id: 'sidebar-quotation' }, - { type: 'CARDS', label: '협상카드관리', icon: Layers, id: 'sidebar-cards' }, - { type: 'MEMBERS', label: '회원관리', icon: UserCog, id: 'sidebar-members', ownerOnly: true }, +// 그룹 라벨은 사이드바 섹션 헤더로 노출(접힘 상태에선 숨김). +const menuGroups: { label?: string; items: MenuItem[] }[] = [ + { + items: [ + { type: 'DASHBOARD', label: '대시보드', icon: LayoutDashboard, id: 'sidebar-dashboard' }, + { type: 'STATISTICS', label: '통계', icon: BarChart3, id: 'sidebar-statistics' }, + ], + }, + { + label: '업무', + items: [ + { type: 'PRODUCTS', label: '상품관리', icon: Briefcase, id: 'sidebar-products' }, + { type: 'PARTNERS', label: '협력사관리', icon: Users, id: 'sidebar-partners' }, + { type: 'QUOTATION', label: '견적관리', icon: FileSpreadsheet, id: 'sidebar-quotation' }, + { type: 'CARDS', label: '협상카드관리', icon: Layers, id: 'sidebar-cards' }, + ], + }, + { + label: '관리', + items: [{ type: 'MEMBERS', label: '회원관리', icon: UserCog, id: 'sidebar-members', ownerOnly: true }], + }, ]; +const menuItems: MenuItem[] = menuGroups.flatMap((g) => g.items); + const pageLabelMap: Record = { DASHBOARD: '대시보드', STATISTICS: '통계', @@ -65,10 +84,25 @@ export default function Layout({ children, currentPage, setPage, onLogout }: Lay const [isSidebarOpen, setIsSidebarOpen] = useState(true); // 데스크톱 접기 토글 const [isMobileOpen, setIsMobileOpen] = useState(false); // 모바일 드로어 열림 const [isProfileOpen, setIsProfileOpen] = useState(false); // 내 정보 수정 시트 + const [isCmdOpen, setIsCmdOpen] = useState(false); // ⌘K 빠른 이동 // 사이드바 펼침 여부(라벨/프로필 노출 기준). 모바일 드로어는 항상 펼친 상태로 본다. const expanded = isMobileOpen || isSidebarOpen; + const visibleItems = menuItems.filter((item) => !item.ownerOnly || user?.role === '최고관리자'); + + // ⌘K / Ctrl+K 로 빠른 이동 열기 + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + setIsCmdOpen((v) => !v); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); + const toggleDarkMode = () => { setIsDark(!isDark); if (!isDark) { @@ -94,7 +128,7 @@ export default function Layout({ children, currentPage, setPage, onLogout }: Lay {/* Main Sidebar */}