왼쪽메뉴에 어드민 메뉴의 트리구조 리스트 api 값을 사용하도록 기능 추가

This commit is contained in:
2026-01-20 21:14:36 +09:00
parent c544c2375f
commit d448a20d1d
9 changed files with 167 additions and 384 deletions

View File

@@ -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: <CIcon icon={cilDrop} customClassName="nav-icon" />,
},
{
component: CNavItem,
name: 'Typography',
to: '/theme/typography',
icon: <CIcon icon={cilPencil} customClassName="nav-icon" />,
},
{
component: CNavTitle,
name: 'Admin',
},
{
component: CNavItem,
name: 'Menu Management',
to: '/admin/menu',
icon: <CIcon icon={cilMenu} customClassName="nav-icon" />,
name: 'Menu',
},
{
component: CNavTitle,

View File

@@ -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<string, unknown>)[iconName]
if (icon) {
return <CIcon icon={icon as string[]} customClassName="nav-icon" />
}
// 아이콘이 없으면 기본 아이콘 표시
return <CIcon icon={cilMinus} customClassName="nav-icon" />
}
// 하위 메뉴용 아이콘 (깊이에 따른 들여쓰기 포함)
const getChildIcon = (iconName: string, depth: number) => {
const icon = (icons as Record<string, unknown>)[iconName]
const iconElement = icon ? (
<CIcon icon={icon as string[]} style={{ width: '1.25rem', height: '1.25rem' }} />
) : (
<CIcon icon={cilMinus} style={{ width: '1.25rem', height: '1.25rem' }} />
)
// 깊이에 따라 들여쓰기 증가 (2단계: 1rem, 3단계: 2rem, ...)
const paddingLeft = `${depth}rem`
return (
<span className="nav-icon" style={{ paddingLeft }}>
{iconElement}
</span>
)
}
// 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<AdminMenuTree[]>([])
// 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 (
<CSidebar
@@ -48,7 +140,7 @@ const AppSidebar = () => {
onClick={() => dispatch({ type: 'set', sidebarShow: false })}
/>
</CSidebarHeader>
<AppSidebarNav items={navigation} />
<AppSidebarNav items={fullNavigation} />
<CSidebarFooter className="border-top d-none d-lg-flex">
<CSidebarToggler
onClick={() => dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })}

View File

@@ -32,14 +32,9 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
const navLink = (name?: string, icon?: React.ReactNode, badge?: Badge, indent = false) => {
return (
<>
{icon
? icon
: indent && (
<span className="nav-icon">
<span className="nav-icon-bullet"></span>
</span>
)}
{name && name}
{icon && icon}
{indent && !icon && <span style={{ marginLeft: '1rem' }} />}
<span style={{ whiteSpace: 'nowrap' }}>{name && name}</span>
{badge && (
<CBadge color={badge.color} className="ms-auto" size="sm">
{badge.text}
@@ -54,17 +49,13 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
const Component = component
return (
<Component as="div" key={index}>
{rest.to || rest.href ? (
<CNavLink
{...(rest.to && { as: NavLink })}
{...(rest.href && { target: '_blank', rel: 'noopener noreferrer' })}
{...rest}
>
{navLink(name, icon, badge, indent)}
</CNavLink>
) : (
navLink(name, icon, badge, indent)
)}
<CNavLink
{...(rest.to && { as: NavLink })}
{...(rest.href && { target: '_blank', rel: 'noopener noreferrer' })}
{...rest}
>
{navLink(name, icon, badge, indent)}
</CNavLink>
</Component>
)
}

View File

@@ -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 },
]

View File

@@ -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<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/add', member);
return response.data;
};
// 회원 수정
export const updateAdminMember = async (member: AdminMember): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/update', member);
return response.data;
};
// 회원 상세 조회
export const getAdminMember = async (memberId: string): Promise<AdminMember> => {
const response = await axios.get<AdminMemberResponse>(`/admin/member/${memberId}`);
return response.data.resultData;
};
// 회원 삭제
export const deleteAdminMember = async (memberId: string): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/delete', { memberId });
return response.data;
};

View File

@@ -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<AdminMenuRe
const response = await axios.post<AdminMenuResponse>('/admin/menu/delete', { adminMenuSeq });
return response.data;
};
// 어드민 메뉴 트리 조회 (로그인 회원 레벨 기준)
export const getAdminMenuTree = async (): Promise<AdminMenuTree[]> => {
const response = await axios.get<AdminMenuResponse>('/admin/menu/tree');
return response.data.resultData || [];
};

View File

@@ -642,7 +642,7 @@ const AdminMenuManagement: React.FC = () => {
</CCard>
{/* 추가/수정 모달 */}
<CModal visible={modalVisible} onClose={() => { setModalVisible(false); setIconPickerVisible(false); }}>
<CModal visible={modalVisible} onClose={() => { setModalVisible(false); setIconPickerVisible(false); }} backdrop="static">
<CModalHeader>
<CModalTitle>{isEditMode ? '메뉴 수정' : '메뉴 추가'}</CModalTitle>
</CModalHeader>
@@ -785,7 +785,7 @@ const AdminMenuManagement: React.FC = () => {
</CModal>
{/* 삭제 확인 모달 */}
<CModal visible={deleteModalVisible} onClose={() => setDeleteModalVisible(false)}>
<CModal visible={deleteModalVisible} onClose={() => setDeleteModalVisible(false)} backdrop="static">
<CModalHeader>
<CModalTitle> </CModalTitle>
</CModalHeader>

View File

@@ -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 (
<table className="table w-100" ref={ref}>
<tbody>
<tr>
<td className="text-body-secondary">HEX:</td>
<td className="font-weight-bold">{rgbToHex(color)}</td>
</tr>
<tr>
<td className="text-body-secondary">RGB:</td>
<td className="font-weight-bold">{color}</td>
</tr>
</tbody>
</table>
)
}
const ThemeColor = ({ className, children }) => {
const classes = classNames(className, 'theme-color w-75 rounded mb-3')
return (
<CCol xs={12} sm={6} md={4} xl={2} className="mb-4">
<div className={classes} style={{ paddingTop: '75%' }}></div>
{children}
<ThemeView />
</CCol>
)
}
ThemeColor.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
}
const Colors = () => {
return (
<>
<CCard className="mb-4">
<CCardHeader>
Theme colors
<DocsLink href="https://coreui.io/docs/utilities/colors/" />
</CCardHeader>
<CCardBody>
<CRow>
<ThemeColor className="bg-primary">
<h6>Brand Primary Color</h6>
</ThemeColor>
<ThemeColor className="bg-secondary">
<h6>Brand Secondary Color</h6>
</ThemeColor>
<ThemeColor className="bg-success">
<h6>Brand Success Color</h6>
</ThemeColor>
<ThemeColor className="bg-danger">
<h6>Brand Danger Color</h6>
</ThemeColor>
<ThemeColor className="bg-warning">
<h6>Brand Warning Color</h6>
</ThemeColor>
<ThemeColor className="bg-info">
<h6>Brand Info Color</h6>
</ThemeColor>
<ThemeColor className="bg-light">
<h6>Brand Light Color</h6>
</ThemeColor>
<ThemeColor className="bg-dark">
<h6>Brand Dark Color</h6>
</ThemeColor>
</CRow>
</CCardBody>
</CCard>
</>
)
}
export default Colors

View File

@@ -1,229 +0,0 @@
import React from 'react'
import { CCard, CCardHeader, CCardBody } from '@coreui/react'
import { DocsLink } from 'src/components'
const Typography = () => {
return (
<>
<CCard className="mb-4">
<CCardHeader>
Headings
<DocsLink href="https://coreui.io/docs/content/typography/" />
</CCardHeader>
<CCardBody>
<p>
Documentation and examples for Bootstrap typography, including global settings,
headings, body text, lists, and more.
</p>
<table className="table">
<thead>
<tr>
<th>Heading</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h1&gt;&lt;/h1&gt;</code>
</p>
</td>
<td>
<span className="h1">h1. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h2&gt;&lt;/h2&gt;</code>
</p>
</td>
<td>
<span className="h2">h2. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h3&gt;&lt;/h3&gt;</code>
</p>
</td>
<td>
<span className="h3">h3. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h4&gt;&lt;/h4&gt;</code>
</p>
</td>
<td>
<span className="h4">h4. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h5&gt;&lt;/h5&gt;</code>
</p>
</td>
<td>
<span className="h5">h5. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h6&gt;&lt;/h6&gt;</code>
</p>
</td>
<td>
<span className="h6">h6. Bootstrap heading</span>
</td>
</tr>
</tbody>
</table>
</CCardBody>
</CCard>
<CCard className="mb-4">
<CCardHeader>Headings</CCardHeader>
<CCardBody>
<p>
<code className="highlighter-rouge">.h1</code> through
<code className="highlighter-rouge">.h6</code>
classes are also available, for when you want to match the font styling of a heading but
cannot use the associated HTML element.
</p>
<div className="bd-example">
<p className="h1">h1. Bootstrap heading</p>
<p className="h2">h2. Bootstrap heading</p>
<p className="h3">h3. Bootstrap heading</p>
<p className="h4">h4. Bootstrap heading</p>
<p className="h5">h5. Bootstrap heading</p>
<p className="h6">h6. Bootstrap heading</p>
</div>
</CCardBody>
</CCard>
<CCard className="mb-4">
<div className="card-header">Display headings</div>
<div className="card-body">
<p>
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 <strong>display heading</strong>
a larger, slightly more opinionated heading style.
</p>
<div className="bd-example bd-example-type">
<table className="table">
<tbody>
<tr>
<td>
<span className="display-1">Display 1</span>
</td>
</tr>
<tr>
<td>
<span className="display-2">Display 2</span>
</td>
</tr>
<tr>
<td>
<span className="display-3">Display 3</span>
</td>
</tr>
<tr>
<td>
<span className="display-4">Display 4</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</CCard>
<CCard className="mb-4">
<CCardHeader>Inline text elements</CCardHeader>
<CCardBody>
<p>
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 <strong>display heading</strong>
a larger, slightly more opinionated heading style.
</p>
<div className="bd-example">
<p>
You can use the mark tag to <mark>highlight</mark> text.
</p>
<p>
<del>This line of text is meant to be treated as deleted text.</del>
</p>
<p>
<s>This line of text is meant to be treated as no longer accurate.</s>
</p>
<p>
<ins>This line of text is meant to be treated as an addition to the document.</ins>
</p>
<p>
<u>This line of text will render as underlined</u>
</p>
<p>
<small>This line of text is meant to be treated as fine print.</small>
</p>
<p>
<strong>This line rendered as bold text.</strong>
</p>
<p>
<em>This line rendered as italicized text.</em>
</p>
</div>
</CCardBody>
</CCard>
<CCard className="mb-4">
<CCardHeader>Description list alignment</CCardHeader>
<CCardBody>
<p>
Align terms and descriptions horizontally by using our grid systems predefined classes
(or semantic mixins). For longer terms, you can optionally add a{' '}
<code className="highlighter-rouge">.text-truncate</code> class to truncate the text
with an ellipsis.
</p>
<div className="bd-example">
<dl className="row">
<dt className="col-sm-3">Description lists</dt>
<dd className="col-sm-9">A description list is perfect for defining terms.</dd>
<dt className="col-sm-3">Euismod</dt>
<dd className="col-sm-9">
<p>
Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit.
</p>
<p>Donec id elit non mi porta gravida at eget metus.</p>
</dd>
<dt className="col-sm-3">Malesuada porta</dt>
<dd className="col-sm-9">Etiam porta sem malesuada magna mollis euismod.</dd>
<dt className="col-sm-3 text-truncate">Truncated term is truncated</dt>
<dd className="col-sm-9">
Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut
fermentum massa justo sit amet risus.
</dd>
<dt className="col-sm-3">Nesting</dt>
<dd className="col-sm-9">
<dl className="row">
<dt className="col-sm-4">Nested definition list</dt>
<dd className="col-sm-8">
Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc.
</dd>
</dl>
</dd>
</dl>
</div>
</CCardBody>
</CCard>
</>
)
}
export default Typography