diff --git a/src/_nav.tsx b/src/_nav.tsx index 11548bf..4a360d3 100644 --- a/src/_nav.tsx +++ b/src/_nav.tsx @@ -1,21 +1,8 @@ import React from 'react' import CIcon from '@coreui/icons-react' import { - cilBell, - cilBrowser, - cilBoatAlt, - cilCalculator, - cilChartPie, - cilCursor, - cilDescription, - cilDrop, - cilExternalLink, - cilNotes, - cilPencil, - cilPuzzle, cilSpeedometer, cilStar, - cilMenu, } from '@coreui/icons' import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react' @@ -32,29 +19,7 @@ const _nav = [ }, { component: CNavTitle, - name: 'Theme', - }, - { - component: CNavItem, - name: 'Colors', - to: '/theme/colors', - icon: , - }, - { - component: CNavItem, - name: 'Typography', - to: '/theme/typography', - icon: , - }, - { - component: CNavTitle, - name: 'Admin', - }, - { - component: CNavItem, - name: 'Menu Management', - to: '/admin/menu', - icon: , + name: 'Menu', }, { component: CNavTitle, diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index d313728..58fa854 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -1,10 +1,13 @@ -import React from 'react' +import React, { useEffect, useState, useMemo } from 'react' import { NavLink } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'src/store' import { CCloseButton, + CNavGroup, + CNavItem, + CNavTitle, CSidebar, CSidebarBrand, CSidebarFooter, @@ -12,6 +15,8 @@ import { CSidebarToggler, } from '@coreui/react' import CIcon from '@coreui/icons-react' +import * as icons from '@coreui/icons' +import { cilMinus } from '@coreui/icons' import { AppSidebarNav } from 'src/components/AppSidebarNav' @@ -20,11 +25,98 @@ import { sygnet } from 'src/assets/brand/sygnet' // sidebar nav config import navigation from 'src/_nav' +import { getAdminMenuTree, AdminMenuTree } from 'src/services/adminMenuService' + +// 아이콘 이름으로 아이콘 컴포넌트 가져오기 (최상위 메뉴용) +const getIconByName = (iconName: string) => { + const icon = (icons as Record)[iconName] + if (icon) { + return + } + // 아이콘이 없으면 기본 아이콘 표시 + return +} + +// 하위 메뉴용 아이콘 (깊이에 따른 들여쓰기 포함) +const getChildIcon = (iconName: string, depth: number) => { + const icon = (icons as Record)[iconName] + const iconElement = icon ? ( + + ) : ( + + ) + // 깊이에 따라 들여쓰기 증가 (2단계: 1rem, 3단계: 2rem, ...) + const paddingLeft = `${depth}rem` + return ( + + {iconElement} + + ) +} + +// API 응답을 CoreUI 네비게이션 형식으로 변환 +const convertMenuTreeToNav = (menuTree: AdminMenuTree[], depth = 0): any[] => { + return menuTree.map((menu) => { + // 깊이가 0이면 최상위, 1 이상이면 하위 메뉴 + const icon = depth === 0 ? getIconByName(menu.iconName) : getChildIcon(menu.iconName, depth) + + if (menu.childMenuList && menu.childMenuList.length > 0) { + // 하위 메뉴가 있는 경우 CNavGroup 사용 + return { + component: CNavGroup, + name: menu.menuName, + icon: icon, + items: convertMenuTreeToNav(menu.childMenuList, depth + 1), + } + } else { + // 하위 메뉴가 없는 경우 CNavItem 사용 + const navItem: any = { + component: CNavItem, + name: menu.menuName, + icon: icon, + indent: depth > 0, + } + // URL이 있는 경우에만 to 속성 추가 + if (menu.menuUrl) { + navItem.to = menu.menuUrl + } + return navItem + } + }) +} const AppSidebar = () => { const dispatch = useDispatch() const unfoldable = useSelector((state: RootState) => state.sidebarUnfoldable) const sidebarShow = useSelector((state: RootState) => state.sidebarShow) + const [dynamicMenus, setDynamicMenus] = useState([]) + + // API에서 메뉴 트리 조회 + useEffect(() => { + const fetchMenuTree = async () => { + try { + const menuTree = await getAdminMenuTree() + setDynamicMenus(menuTree) + } catch (error) { + console.error('메뉴 트리 조회 실패:', error) + } + } + fetchMenuTree() + }, []) + + // 동적 메뉴를 포함한 전체 네비게이션 생성 + const fullNavigation = useMemo(() => { + const navItems = [...navigation] + // 'Menu' 타이틀 바로 다음에 동적 메뉴 삽입 + const menuTitleIndex = navItems.findIndex( + (item) => item.name === 'Menu' && item.component === CNavTitle + ) + if (menuTitleIndex !== -1 && dynamicMenus.length > 0) { + const dynamicNavItems = convertMenuTreeToNav(dynamicMenus) + navItems.splice(menuTitleIndex + 1, 0, ...dynamicNavItems) + } + return navItems + }, [dynamicMenus]) return ( { onClick={() => dispatch({ type: 'set', sidebarShow: false })} /> - + dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })} diff --git a/src/components/AppSidebarNav.tsx b/src/components/AppSidebarNav.tsx index 01b347a..96852ff 100644 --- a/src/components/AppSidebarNav.tsx +++ b/src/components/AppSidebarNav.tsx @@ -32,14 +32,9 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => { const navLink = (name?: string, icon?: React.ReactNode, badge?: Badge, indent = false) => { return ( <> - {icon - ? icon - : indent && ( - - - - )} - {name && name} + {icon && icon} + {indent && !icon && } + {name && name} {badge && ( {badge.text} @@ -54,17 +49,13 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => { const Component = component return ( - {rest.to || rest.href ? ( - - {navLink(name, icon, badge, indent)} - - ) : ( - navLink(name, icon, badge, indent) - )} + + {navLink(name, icon, badge, indent)} + ) } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 17a5ef9..bea520f 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,16 +1,11 @@ import React from 'react' const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard')) -const Colors = React.lazy(() => import('src/views/theme/colors/Colors')) -const Typography = React.lazy(() => import('src/views/theme/typography/Typography')) const AdminMenuManagement = React.lazy(() => import('src/views/admin/AdminMenuManagement')) const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, - { path: '/theme', name: 'Theme', element: Colors, exact: true }, - { path: '/theme/colors', name: 'Colors', element: Colors }, - { path: '/theme/typography', name: 'Typography', element: Typography }, { path: '/admin/menu', name: 'Admin Menu Management', element: AdminMenuManagement }, ] diff --git a/src/services/adminMemberService.ts b/src/services/adminMemberService.ts new file mode 100644 index 0000000..8c8777a --- /dev/null +++ b/src/services/adminMemberService.ts @@ -0,0 +1,42 @@ +import axios from 'src/axios/axios'; + +// 회원 정보 인터페이스 +export interface AdminMember { + memberId: string; + memberName?: string; + email?: string; + password?: string; + // 필요한 경우 추가 필드 정의 +} + +// API 응답 인터페이스 +export interface AdminMemberResponse { + resultCode: string; + resultMessage: string; + resultData?: any; +} + +// 회원 추가 +export const addAdminMember = async (member: AdminMember): Promise => { + const response = await axios.post('/admin/member/add', member); + return response.data; +}; + +// 회원 수정 +export const updateAdminMember = async (member: AdminMember): Promise => { + const response = await axios.post('/admin/member/update', member); + return response.data; +}; + +// 회원 상세 조회 +export const getAdminMember = async (memberId: string): Promise => { + const response = await axios.get(`/admin/member/${memberId}`); + return response.data.resultData; +}; + +// 회원 삭제 +export const deleteAdminMember = async (memberId: string): Promise => { + const response = await axios.post('/admin/member/delete', { memberId }); + return response.data; +}; + diff --git a/src/services/adminMenuService.ts b/src/services/adminMenuService.ts index d528ff1..6070575 100644 --- a/src/services/adminMenuService.ts +++ b/src/services/adminMenuService.ts @@ -11,6 +11,18 @@ export interface AdminMenu { level: number; } +// 트리 구조 메뉴 인터페이스 +export interface AdminMenuTree { + adminMenuSeq: number; + parentSeq: number; + menuOrder: number; + menuName: string; + iconName: string; + menuUrl: string; + level: number; + childMenuList: AdminMenuTree[] | null; +} + // 페이징 정보 인터페이스 export interface PageInfo { pageNum: number; @@ -91,3 +103,9 @@ export const deleteAdminMenu = async (adminMenuSeq: number): Promise('/admin/menu/delete', { adminMenuSeq }); return response.data; }; + +// 어드민 메뉴 트리 조회 (로그인 회원 레벨 기준) +export const getAdminMenuTree = async (): Promise => { + const response = await axios.get('/admin/menu/tree'); + return response.data.resultData || []; +}; diff --git a/src/views/admin/AdminMenuManagement.tsx b/src/views/admin/AdminMenuManagement.tsx index 3383d6d..20496ea 100644 --- a/src/views/admin/AdminMenuManagement.tsx +++ b/src/views/admin/AdminMenuManagement.tsx @@ -642,7 +642,7 @@ const AdminMenuManagement: React.FC = () => { {/* 추가/수정 모달 */} - { setModalVisible(false); setIconPickerVisible(false); }}> + { setModalVisible(false); setIconPickerVisible(false); }} backdrop="static"> {isEditMode ? '메뉴 수정' : '메뉴 추가'} @@ -785,7 +785,7 @@ const AdminMenuManagement: React.FC = () => { {/* 삭제 확인 모달 */} - setDeleteModalVisible(false)}> + setDeleteModalVisible(false)} backdrop="static"> 메뉴 삭제 diff --git a/src/views/theme/colors/Colors.tsx b/src/views/theme/colors/Colors.tsx deleted file mode 100644 index ef844e1..0000000 --- a/src/views/theme/colors/Colors.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useEffect, useState, createRef } from 'react' -import PropTypes from 'prop-types' -import classNames from 'classnames' -import { CRow, CCol, CCard, CCardHeader, CCardBody } from '@coreui/react' -import { rgbToHex } from '@coreui/utils' -import { DocsLink } from 'src/components' - -const ThemeView = () => { - const [color, setColor] = useState('rgb(255, 255, 255)') - const ref = createRef() - - useEffect(() => { - const el = ref.current.parentNode.firstChild - const varColor = window.getComputedStyle(el).getPropertyValue('background-color') - setColor(varColor) - }, [ref]) - - return ( - - - - - - - - - - - -
HEX:{rgbToHex(color)}
RGB:{color}
- ) -} - -const ThemeColor = ({ className, children }) => { - const classes = classNames(className, 'theme-color w-75 rounded mb-3') - return ( - -
- {children} - -
- ) -} - -ThemeColor.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -} - -const Colors = () => { - return ( - <> - - - Theme colors - - - - - -
Brand Primary Color
-
- -
Brand Secondary Color
-
- -
Brand Success Color
-
- -
Brand Danger Color
-
- -
Brand Warning Color
-
- -
Brand Info Color
-
- -
Brand Light Color
-
- -
Brand Dark Color
-
-
-
-
- - ) -} - -export default Colors diff --git a/src/views/theme/typography/Typography.tsx b/src/views/theme/typography/Typography.tsx deleted file mode 100644 index 1cae4f6..0000000 --- a/src/views/theme/typography/Typography.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import React from 'react' -import { CCard, CCardHeader, CCardBody } from '@coreui/react' -import { DocsLink } from 'src/components' - -const Typography = () => { - return ( - <> - - - Headings - - - -

- Documentation and examples for Bootstrap typography, including global settings, - headings, body text, lists, and more. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HeadingExample
-

- <h1></h1> -

-
- h1. Bootstrap heading -
-

- <h2></h2> -

-
- h2. Bootstrap heading -
-

- <h3></h3> -

-
- h3. Bootstrap heading -
-

- <h4></h4> -

-
- h4. Bootstrap heading -
-

- <h5></h5> -

-
- h5. Bootstrap heading -
-

- <h6></h6> -

-
- h6. Bootstrap heading -
-
-
- - Headings - -

- .h1 through - .h6 - classes are also available, for when you want to match the font styling of a heading but - cannot use the associated HTML element. -

-
-

h1. Bootstrap heading

-

h2. Bootstrap heading

-

h3. Bootstrap heading

-

h4. Bootstrap heading

-

h5. Bootstrap heading

-

h6. Bootstrap heading

-
-
-
- -
Display headings
-
-

- Traditional heading elements are designed to work best in the meat of your page content. - When you need a heading to stand out, consider using a display heading - —a larger, slightly more opinionated heading style. -

-
- - - - - - - - - - - - - - - -
- Display 1 -
- Display 2 -
- Display 3 -
- Display 4 -
-
-
-
- - Inline text elements - -

- Traditional heading elements are designed to work best in the meat of your page content. - When you need a heading to stand out, consider using a display heading - —a larger, slightly more opinionated heading style. -

-
-

- You can use the mark tag to highlight text. -

-

- This line of text is meant to be treated as deleted text. -

-

- This line of text is meant to be treated as no longer accurate. -

-

- This line of text is meant to be treated as an addition to the document. -

-

- This line of text will render as underlined -

-

- This line of text is meant to be treated as fine print. -

-

- This line rendered as bold text. -

-

- This line rendered as italicized text. -

-
-
-
- - Description list alignment - -

- Align terms and descriptions horizontally by using our grid system’s predefined classes - (or semantic mixins). For longer terms, you can optionally add a{' '} - .text-truncate class to truncate the text - with an ellipsis. -

-
-
-
Description lists
-
A description list is perfect for defining terms.
- -
Euismod
-
-

- Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit. -

-

Donec id elit non mi porta gravida at eget metus.

-
- -
Malesuada porta
-
Etiam porta sem malesuada magna mollis euismod.
- -
Truncated term is truncated
-
- Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut - fermentum massa justo sit amet risus. -
- -
Nesting
-
-
-
Nested definition list
-
- Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc. -
-
-
-
-
-
-
- - ) -} - -export default Typography