조민수 조민수 05-20
권한관리 > 관리자별메뉴관리, 자주 쓰이는 메커니즘 function [util] 화
@5042f14f2a967d7d3a85c296add7f3095458cee5
eslint.config.js
--- eslint.config.js
+++ eslint.config.js
@@ -19,5 +19,8 @@
       ecmaVersion: 2020,
       globals: globals.browser,
     },
+    rules: {
+      '@typescript-eslint/no-unused-vars': 'off',
+    }
   },
 ])
src/App.tsx
--- src/App.tsx
+++ src/App.tsx
@@ -1,7 +1,6 @@
 import { useEffect, useState } from 'react';
 import { UserLayout } from './user/UserLayout';
 import { UserListPage } from './user/UserListPage';
-import {AdminLayout} from "./admin/layout/AdminLayout.tsx";
 import {AdminRoute} from "./admin/route/AdminRoute.tsx";
 import {ToastContainer} from "react-toastify";
 
@@ -55,9 +54,7 @@
       </div>
 
       {skin === 'admin' ? (
-        <AdminLayout>
-          <AdminRoute />
-        </AdminLayout>
+        <AdminRoute />
       ) : (
         <UserLayout>
           <UserListPage />
 
src/admin/component/table/SortableHeaderCell.tsx (added)
+++ src/admin/component/table/SortableHeaderCell.tsx
@@ -0,0 +1,29 @@
+import type {ReactNode} from "react";
+
+type SortableHeaderCellProps = {
+    field: string;
+    active: boolean;
+    icon: ReactNode;
+    onSort: (field: string) => void;
+    children: ReactNode;
+};
+
+export const SortableHeaderCell = ({
+                                       field,
+                                       active,
+                                       icon,
+                                       onSort,
+                                       children,
+                                   }: SortableHeaderCellProps) => {
+    return (
+        <th scope="col">
+            {children}
+            <button
+                className={`sort sortBtn ${active ? 'active' : ''}`}
+                onClick={() => onSort(field)}
+            >
+                {icon}
+            </button>
+        </th>
+    );
+};
 
src/admin/component/table/getTableRowNumber.ts (added)
+++ src/admin/component/table/getTableRowNumber.ts
@@ -0,0 +1,18 @@
+import type {SearchParams} from "../../../type/searchParams.ts";
+
+type GetTableRowNumberParams = {
+    searchParams: Pick<SearchParams, 'searchSortOrd' | 'pageUnit'>;
+    totalItems: number;
+    currentPage: number;
+    index: number;
+};
+
+export const getTableRowNumber = ({
+                                      searchParams,
+                                      totalItems,
+                                      currentPage,
+                                      index,
+                                  }: GetTableRowNumberParams) =>
+    searchParams.searchSortOrd === 'DESC'
+        ? totalItems - (currentPage - 1) * searchParams.pageUnit - index
+        : (currentPage - 1) * searchParams.pageUnit + (index + 1);
src/admin/feature/board/article/components/BoardArticleListTable.tsx
--- src/admin/feature/board/article/components/BoardArticleListTable.tsx
+++ src/admin/feature/board/article/components/BoardArticleListTable.tsx
@@ -34,7 +34,6 @@
                                 searchParams={params}
                                 totalItems={pagination.totalItems}
                                 currentPage={pagination.currentPage}
-                                totalPages={pagination.totalPages}
                                 checked={check.isChecked(item.nttId)}
                                 onCheck={check.onCheck}
                             />
src/admin/feature/board/article/components/BoardArticleListTableHeader.tsx
--- src/admin/feature/board/article/components/BoardArticleListTableHeader.tsx
+++ src/admin/feature/board/article/components/BoardArticleListTableHeader.tsx
@@ -1,5 +1,7 @@
 import type {BoardArticleSearchParams} from "../type/boardArticle.types.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
 
 type BoardArticleListTableHeaderProps = {
     params: BoardArticleSearchParams;
@@ -16,31 +18,7 @@
                                                 indeterminate,
                                                 onCheckAll,
                                             }: BoardArticleListTableHeaderProps) => {
-    const handleSort = (field: string) => {
-        const nextOrder =
-            params.searchSortCnd === field &&
-            params.searchSortOrd === 'ASC'
-                ? 'DESC'
-                : 'ASC';
-
-        onChange({
-            ...params,
-            searchSortCnd: field,
-            searchSortOrd: nextOrder,
-            pageIndex: 1,
-        });
-    };
-
-    const getSortIcon = (field: string) => {
-
-        if (params.searchSortCnd !== field) {
-            return '-';
-        }
-
-        return params.searchSortOrd === 'ASC'
-            ? '▲'
-            : '▼';
-    };
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
 
 
     return (
@@ -77,15 +55,15 @@
                 <th scope={"col"}>
                     공개여부
                 </th>
-                <th scope="col">작성자</th>
-                <th scope="col">작성일
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'FRST_REGIST_PNTTM' ? 'active' : ''}`}
-                        onClick={() => handleSort('FRST_REGIST_PNTTM')}
-                    >
-                        {getSortIcon('FRST_REGIST_PNTTM')}
-                    </button>
-                </th>
+                <th scope="col">???</th>
+                <SortableHeaderCell
+                    field="FRST_REGIST_PNTTM"
+                    active={isSorted('FRST_REGIST_PNTTM')}
+                    icon={getSortIcon('FRST_REGIST_PNTTM')}
+                    onSort={handleSort}
+                >
+                    ???
+                </SortableHeaderCell>
                 <th scope="col">조회수</th>
             </tr>
             </thead>
src/admin/feature/board/article/components/BoardArticleListTableRow.tsx
--- src/admin/feature/board/article/components/BoardArticleListTableRow.tsx
+++ src/admin/feature/board/article/components/BoardArticleListTableRow.tsx
@@ -1,6 +1,7 @@
 import type {BoardArticleListItem} from "../type/boardArticle.types.ts";
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
 
 type BoardArticleListTableRowProps = {
     item: BoardArticleListItem
@@ -8,7 +9,6 @@
     searchParams: SearchParams
     totalItems: number
     currentPage: number
-    totalPages: number
     checked: boolean
     onCheck: (id: string, checked: boolean) => void
 }
@@ -19,14 +19,17 @@
                                              searchParams,
                                              totalItems,
                                              currentPage,
-                                             totalPages,
                                              checked,
                                              onCheck,
                                          }: BoardArticleListTableRowProps) => {
 
-    const rowNumber = searchParams.searchSortOrd === 'DESC'
-        ? totalItems - (currentPage - 1) * totalPages - index
-        : (currentPage - 1) * totalPages + (index + 1)
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index,
+    });
+
     return (
         <tr>
             <td>
src/admin/feature/board/master/components/BoardListTable.tsx
--- src/admin/feature/board/master/components/BoardListTable.tsx
+++ src/admin/feature/board/master/components/BoardListTable.tsx
@@ -42,7 +42,6 @@
                             searchParams={params}
                             totalItems={pagination.totalItems}
                             currentPage={pagination.currentPage}
-                            totalPages={pagination.totalPages}
                             checked={check.isChecked(item.bbsId)}
                             onCheck={check.onCheck}
                             onDetail={rowActions.onDetail}
src/admin/feature/board/master/components/BoardListTableHeader.tsx
--- src/admin/feature/board/master/components/BoardListTableHeader.tsx
+++ src/admin/feature/board/master/components/BoardListTableHeader.tsx
@@ -1,5 +1,7 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
 
 interface BoardListTableHeaderProps {
     params: SearchParams;
@@ -16,32 +18,7 @@
                                          indeterminate,
                                          onCheckAll
                                      }: BoardListTableHeaderProps) {
-    const handleSort = (field: string) => {
-
-        const nextOrder =
-            params.searchSortCnd === field &&
-            params.searchSortOrd === 'ASC'
-                ? 'DESC'
-                : 'ASC';
-
-        onChange({
-            ...params,
-            searchSortCnd: field,
-            searchSortOrd: nextOrder,
-            pageIndex: 1,
-        });
-    };
-
-    const getSortIcon = (field: string) => {
-
-        if (params.searchSortCnd !== field) {
-            return '-';
-        }
-
-        return params.searchSortOrd === 'ASC'
-            ? '▲'
-            : '▼';
-    };
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
 
     return (
         <>
@@ -69,60 +46,54 @@
                     />
                 </th>
                 <th>번호</th>
-                <th>
+                <SortableHeaderCell
+                    field="BBS_NM"
+                    active={isSorted('BBS_NM')}
+                    icon={getSortIcon('BBS_NM')}
+                    onSort={handleSort}
+                >
                     게시판명
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'BBS_NM' ? 'active' : ''}`}
-                        onClick={() => handleSort('BBS_NM')}
-                    >
-                        {getSortIcon('BBS_NM')}
-                    </button>
-                </th>
-                <th>
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="MENU_NM"
+                    active={isSorted('MENU_NM')}
+                    icon={getSortIcon('MENU_NM')}
+                    onSort={handleSort}
+                >
                     연결 메뉴
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'MENU_NM' ? 'active' : ''}`}
-                        onClick={() => handleSort('MENU_NM')}
-                    >
-                        {getSortIcon('MENU_NM')}
-                    </button>
-                </th>
-                <th>
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="TOTCNT"
+                    active={isSorted('TOTCNT')}
+                    icon={getSortIcon('TOTCNT')}
+                    onSort={handleSort}
+                >
                     댓글 / 글수
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'TOTCNT' ? 'active' : ''}`}
-                        onClick={() => handleSort('TOTCNT')}
-                    >
-                        {getSortIcon('TOTCNT')}
-                    </button>
-                </th>
-                <th>
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="BBS_TY_CODE_NM"
+                    active={isSorted('BBS_TY_CODE_NM')}
+                    icon={getSortIcon('BBS_TY_CODE_NM')}
+                    onSort={handleSort}
+                >
                     게시판유형
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'BBS_TY_CODE_NM' ? 'active' : ''}`}
-                        onClick={() => handleSort('BBS_TY_CODE_NM')}
-                    >
-                        {getSortIcon('BBS_TY_CODE_NM')}
-                    </button>
-                </th>
-                <th>
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="FRST_REGIST_PNTTM"
+                    active={isSorted('FRST_REGIST_PNTTM')}
+                    icon={getSortIcon('FRST_REGIST_PNTTM')}
+                    onSort={handleSort}
+                >
                     생성일
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'FRST_REGIST_PNTTM' ? 'active' : ''}`}
-                        onClick={() => handleSort('FRST_REGIST_PNTTM')}
-                    >
-                        {getSortIcon('FRST_REGIST_PNTTM')}
-                    </button>
-                </th>
-                <th>
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="USE_AT"
+                    active={isSorted('USE_AT')}
+                    icon={getSortIcon('USE_AT')}
+                    onSort={handleSort}
+                >
                     사용여부
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'USE_AT' ? 'active' : ''}`}
-                        onClick={() => handleSort('USE_AT')}
-                    >
-                        {getSortIcon('USE_AT')}
-                    </button>
-                </th>
+                </SortableHeaderCell>
                 <th>게시판 관리</th>
             </tr>
             </thead>
src/admin/feature/board/master/components/BoardListTableRow.tsx
--- src/admin/feature/board/master/components/BoardListTableRow.tsx
+++ src/admin/feature/board/master/components/BoardListTableRow.tsx
@@ -1,6 +1,7 @@
 import type {BoardListItem} from "../type/boardMaster.types.ts";
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
 
 interface BoardListTableRowProps {
     item: BoardListItem
@@ -8,7 +9,6 @@
     searchParams: SearchParams
     totalItems: number
     currentPage: number
-    totalPages: number
     checked: boolean
     onCheck: (id: string, checked: boolean) => void
     onDetail: (bbsId: string) => void
@@ -22,16 +22,19 @@
                                       searchParams,
                                       totalItems,
                                       currentPage,
-                                      totalPages,
                                       checked,
                                       onCheck,
                                       onDetail,
                                       onArticleList,
                                       onPreview
                                   }: BoardListTableRowProps) {
-    const rowNumber = searchParams.searchSortOrd === 'DESC'
-        ? totalItems - (currentPage - 1) * totalPages - index
-        : (currentPage - 1) * totalPages + (index + 1)
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index,
+    });
+
     const bbsId = item.bbsId;
 
     return (
src/admin/feature/role/author/components/AuthorListTable.tsx
--- src/admin/feature/role/author/components/AuthorListTable.tsx
+++ src/admin/feature/role/author/components/AuthorListTable.tsx
@@ -41,7 +41,6 @@
                             searchParams={params}
                             totalItems={pagination.totalItems}
                             currentPage={pagination.currentPage}
-                            totalPages={pagination.totalPages}
                             checked={check.isChecked(item.authorCode)}
                             onCheck={check.onCheck}
                             onDetail={rowActions.onDetail}
src/admin/feature/role/author/components/AuthorListTableHeader.tsx
--- src/admin/feature/role/author/components/AuthorListTableHeader.tsx
+++ src/admin/feature/role/author/components/AuthorListTableHeader.tsx
@@ -1,5 +1,7 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
 
 interface AuthorListTableHeaderProps {
     params: SearchParams;
@@ -16,32 +18,7 @@
                                           indeterminate,
                                           onCheckAll
                                       }: AuthorListTableHeaderProps) => {
-    const handleSort = (field: string) => {
-
-        const nextOrder =
-            params.searchSortCnd === field &&
-            params.searchSortOrd === 'ASC'
-                ? 'DESC'
-                : 'ASC';
-
-        onChange({
-            ...params,
-            searchSortCnd: field,
-            searchSortOrd: nextOrder,
-            pageIndex: 1,
-        });
-    };
-
-    const getSortIcon = (field: string) => {
-
-        if (params.searchSortCnd !== field) {
-            return '-';
-        }
-
-        return params.searchSortOrd === 'ASC'
-            ? '▲'
-            : '▼';
-    };
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
 
     return (
         <>
@@ -65,46 +42,41 @@
                         onChange={onCheckAll}
                     />
                 </th>
-                <th scope="col">번호
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'SORT_NUM' ? 'active' : ''}`}
-                        onClick={() => handleSort('SORT_NUM')}
-                    >
-                        {getSortIcon('SORT_NUM')}
-                    </button>
-                </th>
-                <th scope="col">권한명
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'AUTHOR_NM' ? 'active' : ''}`}
-                        onClick={() => handleSort('AUTHOR_NM')}
-                    >
-                        {getSortIcon('AUTHOR_NM')}
-                    </button>
-                </th>
-                <th scope="col">권한코드
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'AUTHOR_CODE' ? 'active' : ''}`}
-                        onClick={() => handleSort('AUTHOR_CODE')}
-                    >
-                        {getSortIcon('AUTHOR_CODE')}
-                    </button>
-                </th>
-                <th scope="col">설명
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'AUTHOR_DC' ? 'active' : ''}`}
-                        onClick={() => handleSort('AUTHOR_DC')}
-                    >
-                        {getSortIcon('AUTHOR_DC')}
-                    </button>
-                </th>
-                <th scope="col">등록일자
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'AUTHOR_CREAT_DE' ? 'active' : ''}`}
-                        onClick={() => handleSort('AUTHOR_CREAT_DE')}
-                    >
-                        {getSortIcon('AUTHOR_CREAT_DE')}
-                    </button>
-                </th>
+                <SortableHeaderCell
+                    field="SORT_NUM"
+                    active={isSorted('SORT_NUM')}
+                    icon={getSortIcon('SORT_NUM')}
+                    onSort={handleSort}
+                >번호
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="AUTHOR_NM"
+                    active={isSorted('AUTHOR_NM')}
+                    icon={getSortIcon('AUTHOR_NM')}
+                    onSort={handleSort}
+                >권한명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="AUTHOR_CODE"
+                    active={isSorted('AUTHOR_CODE')}
+                    icon={getSortIcon('AUTHOR_CODE')}
+                    onSort={handleSort}
+                >권한코드
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="AUTHOR_DC"
+                    active={isSorted('AUTHOR_DC')}
+                    icon={getSortIcon('AUTHOR_DC')}
+                    onSort={handleSort}
+                >설명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="AUTHOR_CREAT_DE"
+                    active={isSorted('AUTHOR_CREAT_DE')}
+                    icon={getSortIcon('AUTHOR_CREAT_DE')}
+                    onSort={handleSort}
+                >등록일자
+                </SortableHeaderCell>
                 <th scope="col">롤 정보</th>
             </tr>
             </thead>
src/admin/feature/role/author/components/AuthorListTableRow.tsx
--- src/admin/feature/role/author/components/AuthorListTableRow.tsx
+++ src/admin/feature/role/author/components/AuthorListTableRow.tsx
@@ -1,6 +1,7 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import type {AuthorListItem} from "../type/author.types.ts";
 import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
 
 interface AuthorListTableRowProps {
     item: AuthorListItem
@@ -8,7 +9,6 @@
     searchParams: SearchParams
     totalItems: number
     currentPage: number
-    totalPages: number
     checked: boolean
     onCheck: (id: string, checked: boolean) => void
     onDetail: (authorCode: string) => void
@@ -22,15 +22,17 @@
                                        searchParams,
                                        totalItems,
                                        currentPage,
-                                       totalPages,
                                        checked,
                                        onCheck,
                                        onDetail,
                                        onRoleMove
                                    }: AuthorListTableRowProps) => {
-    const rowNumber = searchParams.searchSortOrd === 'DESC'
-        ? totalItems - (currentPage - 1) * totalPages - index
-        : (currentPage - 1) * totalPages + (index + 1)
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index,
+    });
 
     const authorCode = item.authorCode;
 
@@ -70,4 +72,4 @@
             </td>
         </tr>
     );
-};
(No newline at end of file)
+};
src/admin/feature/role/authorGroup/api/authorGroupApi.ts
--- src/admin/feature/role/authorGroup/api/authorGroupApi.ts
+++ src/admin/feature/role/authorGroup/api/authorGroupApi.ts
@@ -1,6 +1,16 @@
 import {apiClient} from "../../../../../api/apiClient.ts";
-import type {AuthorGroupSearchParams} from "../type/authorGroup.types.ts";
+import type {
+    AuthorGroupExtraData,
+    AuthorGroupListItem,
+    AuthorGroupSearchParams,
+    UpdateAuthorGroupRequest
+} from "../type/authorGroup.types.ts";
+import type {PageResponse} from "../../../../../type/pageResponse.ts";
 
 export async function fetchAuthorGroupList(params: AuthorGroupSearchParams) {
-    return apiClient.get(`/sec/rgm/list.do`, params);
+    return apiClient.get<PageResponse<AuthorGroupListItem, AuthorGroupExtraData>>(`/sec/rgm/list.do`, params);
+}
+
+export async function fetchUpdateAuthorGroup(params: UpdateAuthorGroupRequest) {
+    return apiClient.post(`/sec/rgm/EgovAuthorUpdate.do`, params);
 }
 
src/admin/feature/role/authorGroup/component/AuthorGroupListHeader.tsx (added)
+++ src/admin/feature/role/authorGroup/component/AuthorGroupListHeader.tsx
@@ -0,0 +1,66 @@
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
+
+interface AuthorGroupListTableProps {
+    params: SearchParams;
+    onChange: (params: SearchParams) => void;
+}
+
+export const AuthorGroupListHeader = ({
+                                          params,
+                                          onChange
+
+                                      }: AuthorGroupListTableProps) => {
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
+
+    return (
+        <>
+            <colgroup>
+                <col style={{width: "6%"}}/>
+                <col style={{width: "25%"}}/>
+                <col style={{width: "25%"}}/>
+                <col style={{width: "25%"}}/>
+                <col style={{width: "120px"}}/>
+            </colgroup>
+            <thead>
+            <tr>
+                <SortableHeaderCell
+                    field="ESNTL_ID"
+                    active={isSorted('ESNTL_ID')}
+                    onSort={handleSort}
+                    icon={getSortIcon('ESNTL_ID')}
+                >번호</SortableHeaderCell>
+                <SortableHeaderCell
+                    field="USER_ID"
+                    active={isSorted('USER_ID')}
+                    onSort={handleSort}
+                    icon={getSortIcon('USER_ID')}
+                >아이디</SortableHeaderCell>
+
+                <SortableHeaderCell
+                    field="USER_NM"
+                    active={isSorted('USER_NM')}
+                    onSort={handleSort}
+                    icon={getSortIcon('USER_NM')}
+                >관리자명</SortableHeaderCell>
+
+                <SortableHeaderCell
+                    field="AUTHOR_CODE"
+                    active={isSorted('AUTHOR_CODE')}
+                    onSort={handleSort}
+                    icon={getSortIcon('AUTHOR_CODE')}
+                >권한</SortableHeaderCell>
+
+                <SortableHeaderCell
+                    field="REG_YN"
+                    active={isSorted('REG_YN')}
+                    onSort={handleSort}
+                    icon={getSortIcon('REG_YN')}
+                >등록여부</SortableHeaderCell>
+            </tr>
+            </thead>
+
+        </>
+    )
+}(No newline at end of file)
 
src/admin/feature/role/authorGroup/component/AuthorGrouptListTable.tsx (added)
+++ src/admin/feature/role/authorGroup/component/AuthorGrouptListTable.tsx
@@ -0,0 +1,41 @@
+import type {ListTableModel, RowActionsModel} from "../../../../../type/viewModel.ts";
+import type {AuthorGroupListItem} from "../type/authorGroup.types.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {AuthorGroupListHeader} from "./AuthorGroupListHeader.tsx";
+import {EmptyRow} from "../../../../component/EmptyRow.tsx";
+import {AuthorGroupListTableRow} from "./AuthorGrouptListTableRow.tsx";
+import type {AuthorListItem} from "../../author/type/author.types.ts";
+
+type AuthorGroupListTableProps =
+    ListTableModel<AuthorGroupListItem, SearchParams, AuthorListItem[]>
+    & RowActionsModel<{ onEdit: (authorCode: string) => void }>;
+
+export const AuthorGroupListTable = ({
+                                         items,
+                                         params,
+                                         onChange,
+                                         pagination,
+                                         rowActions,
+                                         extraData
+                                     }: AuthorGroupListTableProps) => {
+    return (
+        <div className="table table_type_cols">
+            <table>
+                <AuthorGroupListHeader params={params}
+                                       onChange={onChange}/>
+                <tbody>
+                {items.length > 0 ?
+                    items.map((item, index) => (
+                        <AuthorGroupListTableRow key={index} index={index} item={item} extraData={extraData ?? []}
+                                                 searchParams={params} {...pagination}
+                                                 {...rowActions}
+                        />
+                    ))
+
+                    :
+                    (<EmptyRow colSpan={5}/>)}
+                </tbody>
+            </table>
+        </div>
+    );
+}
 
src/admin/feature/role/authorGroup/component/AuthorGrouptListTableRow.tsx (added)
+++ src/admin/feature/role/authorGroup/component/AuthorGrouptListTableRow.tsx
@@ -0,0 +1,51 @@
+import type {AuthorGroupListItem} from "../type/authorGroup.types.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
+import type {AuthorListItem} from "../../author/type/author.types.ts";
+
+interface AuthorGroupListTableRowProps {
+    item: AuthorGroupListItem
+    index: number
+    searchParams: SearchParams
+    totalItems: number;
+    currentPage: number;
+    onEdit: (uniqId:string, authorCode: string) => void;
+    extraData: AuthorListItem[];
+}
+
+
+export const AuthorGroupListTableRow = ({
+                                            item,
+                                            index,
+                                            searchParams,
+                                            totalItems,
+                                            currentPage,
+                                            onEdit,
+                                            extraData
+                                        }: AuthorGroupListTableRowProps) => {
+    const rowNumber = getTableRowNumber({searchParams, totalItems, currentPage, index});
+
+    return (
+        <tr>
+            <td>{rowNumber}</td>
+            <td>
+                {item.userId}
+            </td>
+            <td>{item.userNm}</td>
+            <td>
+                <select
+                    className="select"
+                    value={item.authorCode}
+                    onChange={(event) => onEdit(item.uniqId, event.target.value)}
+                >
+                    {extraData.map((author) => (
+                        <option key={author.authorCode} value={author.authorCode}>
+                            {author.authorNm}
+                        </option>
+                    ))}
+                </select>
+            </td>
+            <td>{item.regYn}</td>
+        </tr>
+    );
+};
 
src/admin/feature/role/authorGroup/hook/mutation/useUpdateAuthorGroup.ts (added)
+++ src/admin/feature/role/authorGroup/hook/mutation/useUpdateAuthorGroup.ts
@@ -0,0 +1,15 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {fetchUpdateAuthorGroup} from "../../api/authorGroupApi.ts";
+
+export function useUpdateAuthorGroup () {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: fetchUpdateAuthorGroup,
+        onSuccess: () => {
+            queryClient.invalidateQueries({
+                queryKey:["authorGroupList"]
+            })
+        }
+    })
+}(No newline at end of file)
src/admin/feature/role/authorGroup/hook/page/useAuthorGroupListPage.ts
--- src/admin/feature/role/authorGroup/hook/page/useAuthorGroupListPage.ts
+++ src/admin/feature/role/authorGroup/hook/page/useAuthorGroupListPage.ts
@@ -1,0 +1,125 @@
+import type {
+    HeaderModel,
+    ListTableModel,
+    PaginationModel,
+    RowActionsModel,
+    SearchModel,
+    StatusModel
+} from "../../../../../../type/viewModel.ts";
+import type {AuthorGroupListItem, AuthorGroupSearchParams} from "../../type/authorGroup.types.ts";
+import {useState} from "react";
+import {useAuthorGroupList} from "../query/useAuthorGroupList.ts";
+import type {AuthorListItem} from "../../../author/type/author.types.ts";
+import {useUpdateAuthorGroup} from "../mutation/useUpdateAuthorGroup.ts";
+import {toast} from "react-toastify";
+
+const initSearchParam: AuthorGroupSearchParams = {
+    pageIndex: 1,
+    pageUnit: 10,
+    searchCnd: "0",
+    searchKeyword: "",
+    searchSortCnd: "",
+    searchSortOrd: "",
+};
+
+type AuthorGroupListRowActions = {
+    onEdit: (uniqId: string, authorCode: string) => void;
+}
+
+type AuthorGroupListPageModel = {
+    header: HeaderModel;
+    status: StatusModel;
+    search: SearchModel<AuthorGroupSearchParams>;
+    table: ListTableModel<AuthorGroupListItem, AuthorGroupSearchParams, AuthorListItem[]> & RowActionsModel<AuthorGroupListRowActions>;
+    pagination: PaginationModel;
+}
+
+const pageSizeOptions = [
+    {value: '10', label: '10줄'},
+    {value: '20', label: '20줄'},
+    {value: '30', label: '30줄'},
+]
+
+
+const searchOptions = [
+    {value: '', label: '전체'},
+    {value: '0', label: '아이디'},
+    {value: '1', label: '관리자명'},
+]
+
+
+export const useAuthorGroupListPage = (): AuthorGroupListPageModel => {
+    const [searchParams, setSearchParams] = useState({...initSearchParam});
+    const {
+        extraData,
+        currentPage,
+        totalPages,
+        list,
+        totalItems,
+        isLoading,
+        size,
+        error
+    } = useAuthorGroupList(searchParams);
+    const title = "관리자별권한관리";
+    const breadcrumb = [
+        {label: '권한관리'},
+        {label: title}
+    ];
+
+    const {mutateAsync: fetchUpdateAuthorGroup} = useUpdateAuthorGroup();
+
+    const handlePageChange = (pageIndex: number) => {
+        setSearchParams((prev) => ({...prev, pageIndex}));
+    }
+
+    const handleUpdate = async (uniqId: string, authorCode: string) => {
+        await toast.promise(
+            fetchUpdateAuthorGroup({uniqId, authorCode}),
+            {
+                pending: '변경 처리 중...',
+                success: '변경 완료',
+                error: '변경 실패'
+            }
+        )
+    }
+    return {
+        header: {
+            title,
+            breadcrumb
+        },
+        status: {
+            isLoading,
+            error,
+            successMessage: '관리자별권한관리를 조회하였습니다.'
+        },
+        search: {
+            totalItems,
+            searchParams,
+            onChange: setSearchParams,
+            pageSizeOptions,
+            searchOptions
+        },
+        table: {
+            items: list,
+            extraData: extraData?.authorManageList ?? [],
+            params: searchParams,
+            onChange: setSearchParams,
+            pagination: {
+                totalItems,
+                currentPage,
+                totalPages,
+            },
+            rowActions: {
+                onEdit: handleUpdate,
+            }
+        },
+        pagination: {
+            totalItems,
+            totalPages,
+            currentPage,
+            size,
+            onPageChange: handlePageChange,
+        }
+    }
+
+}
src/admin/feature/role/authorGroup/hook/query/useAuthorGroupList.ts
--- src/admin/feature/role/authorGroup/hook/query/useAuthorGroupList.ts
+++ src/admin/feature/role/authorGroup/hook/query/useAuthorGroupList.ts
@@ -1,11 +1,14 @@
 import type {AuthorGroupSearchParams} from "../../type/authorGroup.types.ts";
 import {keepPreviousData, useQuery} from "@tanstack/react-query";
 import {fetchAuthorGroupList} from "../../api/authorGroupApi.ts";
+import {createPageQueryResult} from "../../../../../../type/pageResponse.ts";
 
 export function useAuthorGroupList(searchParams: AuthorGroupSearchParams) {
-    return useQuery({
-        queryKey: ['authorGroupList', searchParams],
+    const query = useQuery({
+        queryKey: ["authorGroupList"],
         queryFn: () => fetchAuthorGroupList(searchParams),
         placeholderData: keepPreviousData
-    });
+    })
+
+    return createPageQueryResult(query);
 }
(No newline at end of file)
 
src/admin/feature/role/authorGroup/page/AuthorGroupListPage.tsx (added)
+++ src/admin/feature/role/authorGroup/page/AuthorGroupListPage.tsx
@@ -0,0 +1,21 @@
+import {useAuthorGroupListPage} from "../hook/page/useAuthorGroupListPage.ts";
+import {PageHeader} from "../../../../component/PageHeader.tsx";
+import {ListSearchForm} from "../../../../component/ListSearchForm.tsx";
+import {Pagination} from "../../../../component/pagination/Pagination.tsx";
+import {AuthorGroupListTable} from "../component/AuthorGrouptListTable.tsx";
+import {useLoadingToast} from "../../../../hook/useLoadingToast.ts";
+
+export const AuthorGroupListPage = () => {
+    const {header, status, search, table, pagination} = useAuthorGroupListPage();
+
+    useLoadingToast(status);
+
+    return (
+        <>
+            <PageHeader {...header}/>
+            <ListSearchForm {...search} totalLabel={"관리"}/>
+            <AuthorGroupListTable {...table}/>
+            <Pagination {...pagination}/>
+        </>
+    );
+}(No newline at end of file)
src/admin/feature/role/authorGroup/type/authorGroup.types.ts
--- src/admin/feature/role/authorGroup/type/authorGroup.types.ts
+++ src/admin/feature/role/authorGroup/type/authorGroup.types.ts
@@ -1,4 +1,24 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
+import type {AuthorListItem} from "../../author/type/author.types.ts";
 
 export interface AuthorGroupSearchParams extends SearchParams {
 }
+
+export interface AuthorGroupListItem {
+    userId: string;
+    userNm: string;
+    mberTyCode: string;
+    authorCode: string;
+    uniqId: string;
+    authorNm: string;
+    regYn: string;
+}
+
+export interface AuthorGroupExtraData {
+    authorManageList: AuthorListItem[];
+}
+
+export interface UpdateAuthorGroupRequest {
+    uniqId: string;
+    authorCode: string;
+}
(No newline at end of file)
src/admin/feature/role/authorRole/components/AuthorRoleListTable.tsx
--- src/admin/feature/role/authorRole/components/AuthorRoleListTable.tsx
+++ src/admin/feature/role/authorRole/components/AuthorRoleListTable.tsx
@@ -37,7 +37,6 @@
                                 searchParams={params}
                                 totalItems={pagination.totalItems}
                                 currentPage={pagination.currentPage}
-                                totalPages={pagination.totalPages}
                                 onDetail={rowActions.onDetail}
                                 onChange={rowActions.onSelectChange}
                             />
src/admin/feature/role/authorRole/components/AuthorRoleListTableHeader.tsx
--- src/admin/feature/role/authorRole/components/AuthorRoleListTableHeader.tsx
+++ src/admin/feature/role/authorRole/components/AuthorRoleListTableHeader.tsx
@@ -1,4 +1,6 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
 
 interface AuthorRoleListTableHeaderProps<T extends SearchParams = SearchParams> {
     params: T;
@@ -9,32 +11,7 @@
                                               params,
                                               onChange,
                                           }: AuthorRoleListTableHeaderProps<T>) => {
-    const handleSort = (field: string) => {
-
-        const nextOrder =
-            params.searchSortCnd === field &&
-            params.searchSortOrd === 'ASC'
-                ? 'DESC'
-                : 'ASC';
-
-        onChange({
-            ...params,
-            searchSortCnd: field,
-            searchSortOrd: nextOrder,
-            pageIndex: 1,
-        });
-    };
-
-    const getSortIcon = (field: string) => {
-
-        if (params.searchSortCnd !== field) {
-            return '-';
-        }
-
-        return params.searchSortOrd === 'ASC'
-            ? '▲'
-            : '▼';
-    };
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
 
     return (
         <>
@@ -48,51 +25,48 @@
             </colgroup>
             <thead>
             <tr>
-                <th scope="col">번호
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'tempSortNum' ? 'active' : ''}`}
-                        onClick={() => handleSort('tempSortNum')}
-                    >
-                        {getSortIcon('tempSortNum')}
-                    </button>
-                </th>
-                <th scope="col">롤명
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'ROLE_NM' ? 'active' : ''}`}
-                        onClick={() => handleSort('ROLE_NM')}
-                    >
-                        {getSortIcon('ROLE_NM')}
-                    </button>
-                </th>
-                <th scope="col">롤패턴
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'ROLE_PTTRN' ? 'active' : ''}`}
-                        onClick={() => handleSort('ROLE_PTTRN')}
-                    >
-                        {getSortIcon('ROLE_PTTRN')}
-                    </button>
-                </th>
-                <th scope="col">롤설명
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'ROLE_SORT' ? 'active' : ''}`}
-                        onClick={() => handleSort('ROLE_SORT')}
-                    >
-                        {getSortIcon('ROLE_SORT')}
-                    </button>
-                </th>
-                <th scope="col">등록일자
-                    <button
-                        className={`sort sortBtn ${params.searchSortCnd === 'ROLE_DC' ? 'active' : ''}`}
-                        onClick={() => handleSort('ROLE_DC')}
-                    >
-                        {getSortIcon('ROLE_DC')}
-                    </button>
-                </th>
-                <th scope="col">등록여부
-                    <button className={`sort sortBtn ${params.searchSortCnd === 'REG_YN' ? 'active' : ''}`}
-                            onClick={() => handleSort('REG_YN')}
-                    >{getSortIcon('REG_YN')}</button>
-                </th>
+                <SortableHeaderCell
+                    field="tempSortNum"
+                    active={isSorted('tempSortNum')}
+                    icon={getSortIcon('tempSortNum')}
+                    onSort={handleSort}
+                >번호
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="ROLE_NM"
+                    active={isSorted('ROLE_NM')}
+                    icon={getSortIcon('ROLE_NM')}
+                    onSort={handleSort}
+                >롤명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="ROLE_PTTRN"
+                    active={isSorted('ROLE_PTTRN')}
+                    icon={getSortIcon('ROLE_PTTRN')}
+                    onSort={handleSort}
+                >롤패턴
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="ROLE_SORT"
+                    active={isSorted('ROLE_SORT')}
+                    icon={getSortIcon('ROLE_SORT')}
+                    onSort={handleSort}
+                >롤설명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="ROLE_DC"
+                    active={isSorted('ROLE_DC')}
+                    icon={getSortIcon('ROLE_DC')}
+                    onSort={handleSort}
+                >등록일자
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="REG_YN"
+                    active={isSorted('REG_YN')}
+                    icon={getSortIcon('REG_YN')}
+                    onSort={handleSort}
+                >등록여부
+                </SortableHeaderCell>
             </tr>
             </thead>
         </>
src/admin/feature/role/authorRole/components/AuthorRoleListTableRow.tsx
--- src/admin/feature/role/authorRole/components/AuthorRoleListTableRow.tsx
+++ src/admin/feature/role/authorRole/components/AuthorRoleListTableRow.tsx
@@ -1,5 +1,6 @@
 import type {SearchParams} from "../../../../../type/searchParams.ts";
 import type {AuthorRoleListItem} from "../type/authorRole.types.ts";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
 
 
 interface AuthorRoleListTableRowProps {
@@ -8,7 +9,6 @@
     searchParams: SearchParams
     totalItems: number
     currentPage: number
-    totalPages: number
     onDetail: (roleCode: string) => void
     onChange: (roleCode: string,value: string) => void
 }
@@ -20,13 +20,15 @@
                                            searchParams,
                                            totalItems,
                                            currentPage,
-                                           totalPages,
                                            onDetail,
                                            onChange,
                                        }: AuthorRoleListTableRowProps) => {
-    const rowNumber = searchParams.searchSortOrd === 'DESC'
-        ? totalItems - (currentPage - 1) * totalPages - index
-        : (currentPage - 1) * totalPages + (index + 1)
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index,
+    });
 
     const roleCode = item.roleCode;
 
@@ -58,4 +60,4 @@
             </td>
         </tr>
     );
-};
(No newline at end of file)
+};
src/admin/feature/role/authorRoleMenu/api/authorRoleMenuApi.ts
--- src/admin/feature/role/authorRoleMenu/api/authorRoleMenuApi.ts
+++ src/admin/feature/role/authorRoleMenu/api/authorRoleMenuApi.ts
@@ -1,10 +1,19 @@
 import {apiClient} from "../../../../../api/apiClient.ts";
-import type {AuthorRoleMenuSearchParams} from "../type/authorRoleMenu.types.ts";
+import type {
+    AuthorRoleMenuFormItem,
+    AuthorRoleMenuListItem,
+    AuthorRoleMenuSearchParams, UpdateAuthorRoleMenuItemRequest
+} from "../type/authorRoleMenu.types.ts";
+import type {PageResponse} from "../../../../../type/pageResponse.ts";
 
 export async function fetchAuthorRoleMenuList(params: AuthorRoleMenuSearchParams) {
-    return apiClient.get(`/sym/mnu/mcm/list.do`, params);
+    return apiClient.get<PageResponse<AuthorRoleMenuListItem>>(`/sym/mnu/mcm/list.do`, params);
 }
 
 export async function fetchAuthorRoleMenuDetail(authorCode: string) {
-    return apiClient.get(`/sym/mnu/mcm/detail?authorCode=${authorCode}`);
+    return apiClient.get<AuthorRoleMenuFormItem[]>(`/sym/mnu/mcm/detail.do?authorCode=${authorCode}`);
+}
+
+export async function updateAuthorRoleMenu(params: UpdateAuthorRoleMenuItemRequest[]) {
+    return apiClient.post(`/sym/mnu/mcm/EgovMenuCreatInsert.do`, params);
 }
 
src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuDetailTable.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuDetailTable.tsx
@@ -0,0 +1,41 @@
+import {buildMenuTree} from "./buildMenuTree.ts";
+import type {AuthorRoleMenuFormItem} from "../type/authorRoleMenu.types.ts";
+import {AuthorRoleMenuTreeItem} from "./AuthorRoleMenuTreeItem.tsx";
+
+type Props = {
+    items: AuthorRoleMenuFormItem[];
+    isChecked: (menuNo: AuthorRoleMenuFormItem["menuNo"]) => boolean;
+    onCheck: (menuNo: AuthorRoleMenuFormItem["menuNo"], checked: boolean) => void;
+};
+
+export const AuthorRoleMenuDetailTable = ({
+                                              items,
+                                              isChecked,
+                                              onCheck,
+                                          }: Props) => {
+
+    const tree = buildMenuTree(items);
+
+    return (
+        <div className={"tree"}>
+
+            <div className={"tree_title"}>
+                메뉴목록
+            </div>
+
+            <div className={"tree_body"}>
+
+                {tree.map((node) => (
+                    <AuthorRoleMenuTreeItem
+                        key={node.menuNo}
+                        node={node}
+                        isChecked={isChecked}
+                        onCheck={onCheck}
+                    />
+                ))}
+
+            </div>
+
+        </div>
+    );
+};
 
src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTable.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTable.tsx
@@ -0,0 +1,52 @@
+import {EmptyRow} from "../../../../component/EmptyRow.tsx";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import type {ListTableModel, RowActionsModel} from "../../../../../type/viewModel.ts";
+import type {AuthorRoleMenuListItem} from "../type/authorRoleMenu.types.ts";
+import {AuthorRoleMenuListTableHeader} from "./AuthorRoleMenuListTableHeader.tsx";
+import {AuthorRoleMenuListTableRow} from "./AuthorRoleMenuListTableRow.tsx";
+
+
+type AuthorRoleMenuListTableProps<T extends SearchParams = SearchParams> =
+    ListTableModel<AuthorRoleMenuListItem, T> &
+    RowActionsModel<{
+        onPopup: (authorCode:string) => void;
+    }>;
+
+
+export const AuthorRoleMenuListTable = (
+    {
+        items,
+        params,
+        onChange,
+        pagination,
+        rowActions
+    }: AuthorRoleMenuListTableProps) => {
+
+    return (
+        <div className="table table_type_cols">
+            <table>
+                <AuthorRoleMenuListTableHeader
+                    params={params}
+                    onChange={onChange}
+                />
+                <tbody>
+                {items.length > 0 ?
+                    items.map((item, index) => (
+                            <AuthorRoleMenuListTableRow
+                                key={index}
+                                item={item}
+                                index={index}
+                                searchParams={params}
+                                {...pagination}
+                                {...rowActions}
+                            />
+                        )
+                    ) : (
+                        <EmptyRow colSpan={6} />
+                    )
+                }
+                </tbody>
+            </table>
+        </div>
+    )
+}(No newline at end of file)
 
src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTableHeader.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTableHeader.tsx
@@ -0,0 +1,67 @@
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
+
+interface AuthorRoleMenuListTableHeaderProps<T extends SearchParams = SearchParams>{
+    params: T;
+    onChange: (params: T) => void;
+}
+
+export const AuthorRoleMenuListTableHeader = ({params, onChange}: AuthorRoleMenuListTableHeaderProps) => {
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
+
+    return (
+        <>
+            <colgroup>
+                <col style={{width: "6%"}}/>
+                <col style={{width: "25%"}}/>
+                <col style={{width: "30%"}}/>
+                <col style={{width: "30%"}}/>
+                <col style={{width: "15%"}}/>
+                <col style={{width: "120px"}}/>
+            </colgroup>
+            <thead>
+            <tr>
+                <SortableHeaderCell
+                    field="tempSortNum"
+                    active={isSorted('tempSortNum')}
+                    icon={getSortIcon('tempSortNum')}
+                    onSort={handleSort}
+                >번호
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="authorCode"
+                    active={isSorted('authorCode')}
+                    icon={getSortIcon('authorCode')}
+                    onSort={handleSort}
+                >권한코드
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="authorNm"
+                    active={isSorted('authorNm')}
+                    icon={getSortIcon('authorNm')}
+                    onSort={handleSort}
+                >권한명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="authorDc"
+                    active={isSorted('authorDc')}
+                    icon={getSortIcon('authorDc')}
+                    onSort={handleSort}
+                >권한설명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field="chkYeoBu"
+                    active={isSorted('chkYeoBu')}
+                    icon={getSortIcon('chkYeoBu')}
+                    onSort={handleSort}
+                >관리메뉴 갯수
+                </SortableHeaderCell>
+                <th scope="col">
+                    롤 메뉴생성
+                </th>
+            </tr>
+            </thead>
+        </>
+    )
+}(No newline at end of file)
 
src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTableRow.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuListTableRow.tsx
@@ -0,0 +1,56 @@
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import type {AuthorRoleMenuListItem} from "../type/authorRoleMenu.types.ts";
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
+
+interface AuthorRoleMenuListTableRowProps {
+    item: AuthorRoleMenuListItem
+    index: number
+    searchParams: SearchParams
+    totalItems: number
+    currentPage: number
+    onPopup: (authorCode:string) => void
+}
+
+export const AuthorRoleMenuListTableRow = ({
+                                               item,
+                                               index,
+                                               searchParams,
+                                               totalItems,
+                                               currentPage,
+                                               onPopup
+                                           }: AuthorRoleMenuListTableRowProps) => {
+
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index,
+    });
+
+    const authorCode = item.authorCode;
+
+    return (
+        <tr>
+            <td>
+                {rowNumber}
+            </td>
+            <td>
+                {authorCode}
+            </td>
+            <td>
+                {item.authorNm}
+            </td>
+            <td>
+                {item.authorDc}
+            </td>
+            <td>
+                {item.chkYeoBu}개
+            </td>
+            <td>
+                <button type="button" className={"btn line secondary medium"} onClick={() => onPopup(authorCode)}>
+                    메뉴생성
+                </button>
+            </td>
+        </tr>
+    )
+}
 
src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuTreeItem.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/component/AuthorRoleMenuTreeItem.tsx
@@ -0,0 +1,60 @@
+import {useState} from "react";
+import type {MenuTreeItem} from "../type/authorRoleMenu.types.ts";
+
+type TreeItemProps = {
+    node: MenuTreeItem;
+    isChecked: (menuNo: MenuTreeItem["menuNo"]) => boolean;
+    onCheck: (menuNo: MenuTreeItem["menuNo"], checked: boolean) => void;
+};
+
+export const AuthorRoleMenuTreeItem = ({
+                                           node,
+                                           isChecked,
+                                           onCheck,
+                                       }: TreeItemProps) => {
+
+    const [open, setOpen] = useState(true);
+
+    const hasChildren =
+        node.children.length > 0;
+
+    return (
+        <div className={`tree-node ${hasChildren ? 'folder' : 'file'}`}>
+
+            <label className={"tree-label"}>
+
+                {hasChildren && (
+                    <button
+                        type={"button"}
+                        onClick={() => setOpen(prev => !prev)}
+                    >
+                        {open ? "-" : "+"}
+                    </button>
+                )}
+
+                <input
+                    type={"checkbox"}
+                    checked={isChecked(node.menuNo)}
+                    onChange={(event) => onCheck(node.menuNo, event.target.checked)}
+                />
+
+                <span>
+                    {node.menuNm}
+                </span>
+            </label>
+
+            {open && hasChildren && (
+                <div className={"tree-children"}>
+                    {node.children.map((child) => (
+                        <AuthorRoleMenuTreeItem
+                            key={child.menuNo}
+                            node={child}
+                            isChecked={isChecked}
+                            onCheck={onCheck}
+                        />
+                    ))}
+                </div>
+            )}
+        </div>
+    );
+};
 
src/admin/feature/role/authorRoleMenu/component/buildMenuTree.ts (added)
+++ src/admin/feature/role/authorRoleMenu/component/buildMenuTree.ts
@@ -0,0 +1,48 @@
+import type {AuthorRoleMenuFormItem, MenuTreeItem} from "../type/authorRoleMenu.types.ts";
+
+export const buildMenuTree = (
+    items: AuthorRoleMenuFormItem[]
+): MenuTreeItem[] => {
+
+    const map = new Map<string, MenuTreeItem>();
+    const roots: MenuTreeItem[] = [];
+    const normalizeId = (value: number | string | null | undefined) =>
+        value == null ? '' : String(value);
+
+    items.forEach((item) => {
+        map.set(normalizeId(item.menuNo), {
+            ...item,
+            children: [],
+        });
+    });
+
+    map.forEach((node) => {
+        const upperMenuId = normalizeId(node.upperMenuId);
+
+        if (upperMenuId === '0' || upperMenuId === '') {
+            roots.push(node);
+            return;
+        }
+
+        const parent = map.get(upperMenuId);
+
+        if (parent) {
+            parent.children.push(node);
+            return;
+        }
+
+        roots.push(node);
+    });
+
+    const sortByMenuOrder = (left: MenuTreeItem, right: MenuTreeItem) =>
+        Number(left.menuOrdr) - Number(right.menuOrdr);
+
+    const sortTree = (nodes: MenuTreeItem[]) => {
+        nodes.sort(sortByMenuOrder);
+        nodes.forEach((node) => sortTree(node.children));
+    };
+
+    sortTree(roots);
+
+    return roots;
+};
 
src/admin/feature/role/authorRoleMenu/constant/authorRoleMenuMessage.ts (added)
+++ src/admin/feature/role/authorRoleMenu/constant/authorRoleMenuMessage.ts
@@ -0,0 +1,1 @@
+export const AUTHOR_ROLE_MENU_UPDATED_MESSAGE = 'AUTHOR_ROLE_MENU_UPDATED';
 
src/admin/feature/role/authorRoleMenu/hook/mutation/useUpdateAuthorRoleMenu.ts (added)
+++ src/admin/feature/role/authorRoleMenu/hook/mutation/useUpdateAuthorRoleMenu.ts
@@ -0,0 +1,27 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {updateAuthorRoleMenu} from "../../api/authorRoleMenuApi.ts";
+
+export const useUpdateAuthorRoleMenu = () => {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: updateAuthorRoleMenu,
+        onSuccess: (_, variables) => {
+            const authorCode = variables[0]?.authorCode;
+
+            queryClient.invalidateQueries({
+                queryKey: ['authorRoleMenuList']
+            });
+
+            if (authorCode) {
+                queryClient.invalidateQueries({
+                    queryKey: ['authorRoleMenuDetail', authorCode],
+                });
+            }
+
+            queryClient.invalidateQueries({
+                queryKey: ['menuList'],
+            })
+        }
+    });
+}
 
src/admin/feature/role/authorRoleMenu/hook/page/useAuthorRoleMenuListPage.ts (added)
+++ src/admin/feature/role/authorRoleMenu/hook/page/useAuthorRoleMenuListPage.ts
@@ -0,0 +1,148 @@
+import type {
+    HeaderModel,
+    ListTableModel, PaginationModel,
+    RowActionsModel,
+    SearchModel,
+    StatusModel
+} from "../../../../../../type/viewModel.ts";
+import type {AuthorRoleMenuListItem, AuthorRoleMenuSearchParams} from "../../type/authorRoleMenu.types.ts";
+import {useEffect, useState} from "react";
+import {useQueryClient} from "@tanstack/react-query";
+import {useAuthorRoleMenuList} from "../query/useAuthorRoleMenuList.ts";
+import {ADMIN_MENU_POPUP_ROUTE} from "../../../../../route/adminRouteMap.ts";
+import {AUTHOR_ROLE_MENU_UPDATED_MESSAGE} from "../../constant/authorRoleMenuMessage.ts";
+
+type AuthorRoleMenuRowActions = {
+    onPopup: (authorCode: string) => void
+}
+
+
+type AuthorRoleMenuListPageModel = {
+    header: HeaderModel;
+    status: StatusModel;
+    search: SearchModel<AuthorRoleMenuSearchParams>;
+    table: ListTableModel<AuthorRoleMenuListItem, AuthorRoleMenuSearchParams> & RowActionsModel<AuthorRoleMenuRowActions>;
+    pagination: PaginationModel;
+}
+
+const initSearchParam = {
+    pageIndex: 1,
+    pageUnit: 10,
+    searchCnd: "0",
+    searchKeyword: "",
+    searchSortCnd: "",
+    searchSortOrd: ""
+}
+
+const title = "권한별메뉴관리"
+const breadcrumb = [
+    {label: '권한관리',},
+    {label: '권한별메뉴관리'}
+]
+const homeUrl = "#"
+
+const pageSizeOptions = [
+    {value: '10', label: '10줄'},
+    {value: '20', label: '20줄'},
+    {value: '30', label: '30줄'},
+]
+
+const successMessage = "메뉴를 조회하였습니다.";
+
+export const useAuthorRoleMenuListPage = (): AuthorRoleMenuListPageModel => {
+    const [searchParams, setSearchParams] = useState(initSearchParam);
+    const queryClient = useQueryClient();
+    const {list, totalPages, totalItems, currentPage, size, isLoading, error} = useAuthorRoleMenuList(searchParams);
+
+    useEffect(() => {
+        const handleMessage = (event: MessageEvent) => {
+            if (event.origin !== window.location.origin) {
+                return;
+            }
+
+            if (typeof event.data !== 'object' || event.data === null) {
+                return;
+            }
+
+            const message = event.data as {
+                type?: unknown;
+                authorCode?: unknown;
+            };
+
+            if (message.type !== AUTHOR_ROLE_MENU_UPDATED_MESSAGE) {
+                return;
+            }
+
+            queryClient.invalidateQueries({
+                queryKey: ['authorRoleMenuList'],
+            });
+
+            if (typeof message.authorCode === 'string') {
+                queryClient.invalidateQueries({
+                    queryKey: ['authorRoleMenuDetail', message.authorCode],
+                });
+            }
+
+            queryClient.invalidateQueries({
+                queryKey: ['menuList'],
+            });
+        };
+
+        window.addEventListener('message', handleMessage);
+
+        return () => window.removeEventListener('message', handleMessage);
+    }, [queryClient]);
+
+    const handleOpenPopup = (authorCode: string) => {
+        window.open(`${ADMIN_MENU_POPUP_ROUTE}/${authorCode}`, "_blank"
+            ,"width=600,height=600"
+        );
+    }
+
+    const handlePageChange = (pageIndex: number) => {
+        setSearchParams((prev) => ({
+            ...prev,
+            pageIndex
+        }));
+    }
+
+    return {
+        header: {
+            title,
+            breadcrumb,
+            homeUrl
+        },
+        status: {
+            isLoading,
+            error,
+            successMessage
+        },
+        search: {
+            totalItems,
+            searchParams,
+            onChange: setSearchParams,
+            pageSizeOptions
+        },
+        table : {
+            items: list,
+            params: searchParams,
+            onChange: setSearchParams,
+            pagination: {
+                totalItems,
+                currentPage,
+                totalPages
+            },
+            rowActions: {
+                onPopup: handleOpenPopup
+            }
+        },
+        pagination: {
+            totalItems,
+            totalPages,
+            currentPage,
+            size,
+            onPageChange: handlePageChange
+        }
+    }
+
+}
 
src/admin/feature/role/authorRoleMenu/hook/page/useAuthorRoleMenuPopupPage.ts (added)
+++ src/admin/feature/role/authorRoleMenu/hook/page/useAuthorRoleMenuPopupPage.ts
@@ -0,0 +1,119 @@
+import {useMemo, useState} from "react";
+import {useParams} from "react-router-dom";
+import {useAuthorRoleMenuDetail} from "../query/useAuthorRoleMenuDetail.ts";
+import type {AuthorRoleMenuFormItem, UpdateAuthorRoleMenuItemRequest} from "../../type/authorRoleMenu.types.ts";
+import {useUpdateAuthorRoleMenu} from "../mutation/useUpdateAuthorRoleMenu.ts";
+import {toast} from "react-toastify";
+import {AUTHOR_ROLE_MENU_UPDATED_MESSAGE} from "../../constant/authorRoleMenuMessage.ts";
+
+export const useAuthorRoleMenuPopupPage = () => {
+    const {authorCode = ''} = useParams();
+    const {data = [], isLoading, error} = useAuthorRoleMenuDetail(authorCode);
+    const [selectedMenuNos, setSelectedMenuNos] = useState<Set<string> | null>(null);
+    const {mutateAsync: updateRoleMenu} = useUpdateAuthorRoleMenu();
+
+    const initialSelectedMenuNos = useMemo(
+        () => new Set(
+            data
+                .filter((item) => String(item.chkYeoBu) === '1')
+                .map((item) => String(item.menuNo))
+        ),
+        [data],
+    );
+
+    const currentSelectedMenuNos = selectedMenuNos ?? initialSelectedMenuNos;
+
+    const childrenByParent = useMemo(() => {
+        const map = new Map<string, AuthorRoleMenuFormItem[]>();
+
+        data.forEach((item) => {
+            const parentKey = item.upperMenuId == null ? '' : String(item.upperMenuId);
+            const children = map.get(parentKey) ?? [];
+
+            children.push(item);
+            map.set(parentKey, children);
+        });
+
+        return map;
+    }, [data]);
+
+    const getDescendantMenuNos = (menuNo: AuthorRoleMenuFormItem["menuNo"]) => {
+        const descendantMenuNos: string[] = [];
+        const appendDescendants = (parentMenuNo: string) => {
+            const children = childrenByParent.get(parentMenuNo) ?? [];
+
+            children.forEach((child) => {
+                const childMenuNo = String(child.menuNo);
+
+                descendantMenuNos.push(childMenuNo);
+                appendDescendants(childMenuNo);
+            });
+        };
+
+        appendDescendants(String(menuNo));
+
+        return descendantMenuNos;
+    };
+
+    const isChecked = (menuNo: AuthorRoleMenuFormItem["menuNo"]) =>
+        currentSelectedMenuNos.has(String(menuNo));
+
+    const handleCheck = (
+        menuNo: AuthorRoleMenuFormItem["menuNo"],
+        checked: boolean,
+    ) => {
+        setSelectedMenuNos((prev) => {
+            const next = new Set(prev ?? currentSelectedMenuNos);
+            const menuNosToUpdate = [
+                String(menuNo),
+                ...getDescendantMenuNos(menuNo),
+            ];
+
+            if (checked) {
+                menuNosToUpdate.forEach((targetMenuNo) => next.add(targetMenuNo));
+            } else {
+                menuNosToUpdate.forEach((targetMenuNo) => next.delete(targetMenuNo));
+            }
+
+            return next;
+        });
+    };
+
+    const handleUpdateAuthorRoleMenu = async (menu: UpdateAuthorRoleMenuItemRequest[]) => {
+        await toast.promise(
+            updateRoleMenu(menu), {
+                pending: '수정 중...',
+                success: '수정 완료',
+                error: '수정 실패'
+            }
+        );
+
+        window.opener?.postMessage(
+            {
+                type: AUTHOR_ROLE_MENU_UPDATED_MESSAGE,
+                authorCode,
+            },
+            window.location.origin,
+        );
+    }
+
+    const handleSave = () => {
+        const payload = [...currentSelectedMenuNos].map((menuNo) => ({
+            authorCode,
+            menuNo,
+        }))
+
+        return handleUpdateAuthorRoleMenu(payload);
+    }
+
+    return {
+        authorCode,
+        items: data,
+        isLoading,
+        error,
+        selectedMenuNos: currentSelectedMenuNos,
+        isChecked,
+        onCheck: handleCheck,
+        onSave: handleSave,
+    };
+};
src/admin/feature/role/authorRoleMenu/hook/query/useAuthorRoleMenuList.ts
--- src/admin/feature/role/authorRoleMenu/hook/query/useAuthorRoleMenuList.ts
+++ src/admin/feature/role/authorRoleMenu/hook/query/useAuthorRoleMenuList.ts
@@ -1,11 +1,14 @@
 import type {AuthorRoleMenuSearchParams} from "../../type/authorRoleMenu.types.ts";
 import {keepPreviousData, useQuery} from "@tanstack/react-query";
 import {fetchAuthorRoleMenuList} from "../../api/authorRoleMenuApi.ts";
+import {createPageQueryResult} from "../../../../../../type/pageResponse.ts";
 
 export function useAuthorRoleMenuList(searchParams: AuthorRoleMenuSearchParams) {
-    return useQuery({
+    const query = useQuery({
         queryKey: ['authorRoleMenuList', searchParams],
         queryFn: () => fetchAuthorRoleMenuList(searchParams),
         placeholderData: keepPreviousData
     });
+
+    return createPageQueryResult(query);
 }
 
src/admin/feature/role/authorRoleMenu/page/AuthorRoleMenuListPage.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/page/AuthorRoleMenuListPage.tsx
@@ -0,0 +1,27 @@
+import {PageHeader} from "../../../../component/PageHeader.tsx";
+import {useAuthorRoleMenuListPage} from "../hook/page/useAuthorRoleMenuListPage.ts";
+import {ListSearchForm} from "../../../../component/ListSearchForm.tsx";
+import {Pagination} from "../../../../component/pagination/Pagination.tsx";
+import {AuthorRoleMenuListTable} from "../component/AuthorRoleMenuListTable.tsx";
+import {useLoadingToast} from "../../../../hook/useLoadingToast.ts";
+
+export const AuthorRoleMenuListPage = () => {
+    const {
+        header,
+        status,
+        search,
+        table,
+        pagination
+    } = useAuthorRoleMenuListPage();
+
+    useLoadingToast(status);
+
+    return (
+        <>
+            <PageHeader {...header}/>
+            <ListSearchForm {...search}/>
+            <AuthorRoleMenuListTable {...table}/>
+            <Pagination {...pagination}/>
+        </>
+    );
+}(No newline at end of file)
 
src/admin/feature/role/authorRoleMenu/page/AuthorRoleMenuPopupPage.tsx (added)
+++ src/admin/feature/role/authorRoleMenu/page/AuthorRoleMenuPopupPage.tsx
@@ -0,0 +1,53 @@
+import {AuthorRoleMenuDetailTable} from "../component/AuthorRoleMenuDetailTable.tsx";
+import {useAuthorRoleMenuPopupPage} from "../hook/page/useAuthorRoleMenuPopupPage.ts";
+
+export const AuthorRoleMenuPopupPage = () => {
+    const {
+        authorCode,
+        items,
+        isLoading,
+        error,
+        isChecked,
+        onCheck,
+        onSave
+    } = useAuthorRoleMenuPopupPage();
+
+    if (isLoading) {
+        return <p>Loading...</p>;
+    }
+
+    if (error) {
+        return <p>{error.message}</p>;
+    }
+
+    return (
+        <div>
+            <div className={"popup_window popup"}>
+                <h2 className={"title"}>메뉴관리</h2>
+
+                <div className={"popup_content"}>
+                    <div className="content_title mt0">
+                        <h3 className="title">조건정보 영역</h3>
+                    </div>
+                    <ul className="search_area box">
+                        <li className="search_item">
+                            <strong className="search_title">권한코드</strong>
+                            <div className="form_wrap">
+                                <input id="authorCode" className="input" name="authorCode" type="text" value={authorCode} readOnly={true} />
+                            </div>
+                        </li>
+                    </ul>
+                    <AuthorRoleMenuDetailTable
+                        items={items}
+                        isChecked={isChecked}
+                        onCheck={onCheck}
+                    />
+                </div>
+                <div className={"btn_wrap center mt20"}>
+                    <button onClick={onSave} className={"btn fill primary large w20per"}>설정</button>
+                    <button onClick={() => {window.close()}} className={"btn fill gray large w20per"}>닫기</button>
+                </div>
+            </div>
+        </div>
+    );
+}
src/admin/feature/role/authorRoleMenu/type/authorRoleMenu.types.ts
--- src/admin/feature/role/authorRoleMenu/type/authorRoleMenu.types.ts
+++ src/admin/feature/role/authorRoleMenu/type/authorRoleMenu.types.ts
@@ -2,3 +2,29 @@
 
 export interface AuthorRoleMenuSearchParams extends SearchParams {
 }
+
+
+export interface AuthorRoleMenuListItem {
+    tempSortNum: string;
+    authorCode: string;
+    authorNm: string;
+    authorDc: string;
+    chkYeoBu: string;
+}
+
+export interface AuthorRoleMenuFormItem {
+    chkYeoBu: number | string;
+    menuNm: string;
+    menuNo: number | string;
+    menuOrdr: number | string;
+    upperMenuId?: number | string | null;
+}
+
+export interface MenuTreeItem extends AuthorRoleMenuFormItem {
+    children: MenuTreeItem[];
+}
+
+export interface UpdateAuthorRoleMenuItemRequest {
+    authorCode: string;
+    menuNo: string;
+}
 
src/admin/hook/useTableSort.ts (added)
+++ src/admin/hook/useTableSort.ts
@@ -0,0 +1,40 @@
+import type {SearchParams} from "../../type/searchParams.ts";
+
+export const useTableSort = <T extends SearchParams>(
+    params: T,
+    onChange: (params: T) => void
+) => {
+    const handleSort = (field: string) => {
+        const nextOrder =
+            params.searchSortCnd === field &&
+            params.searchSortOrd === 'ASC'
+                ? 'DESC'
+                : 'ASC';
+
+        onChange({
+            ...params,
+            searchSortCnd: field,
+            searchSortOrd: nextOrder,
+            pageIndex: 1,
+        });
+    };
+
+    const getSortIcon = (field: string) => {
+        if (params.searchSortCnd !== field) {
+            return '-';
+        }
+
+        return params.searchSortOrd === 'ASC'
+            ? '▲'
+            : '▼';
+    };
+
+    const isSorted = (field: string) =>
+        params.searchSortCnd === field;
+
+    return {
+        handleSort,
+        getSortIcon,
+        isSorted,
+    };
+};
src/admin/layout/AdminLayout.tsx
--- src/admin/layout/AdminLayout.tsx
+++ src/admin/layout/AdminLayout.tsx
@@ -1,12 +1,8 @@
-import type {ReactNode} from "react";
+import {Outlet} from "react-router-dom";
 import {AdminSideBar} from "./AdminSideBar.tsx";
 import {AdminTopBar} from "./AdminTopBar.tsx";
 
-type AdminLayoutProps = {
-    children: ReactNode;
-}
-
-export function AdminLayout({children}: AdminLayoutProps) {
+export function AdminLayout() {
     return (
         <div className="wrap">
             <AdminSideBar/>
@@ -14,9 +10,9 @@
             <div className="container sub">
                 <AdminTopBar/>
                 <div className="content_wrap">
-                    {children}
+                    <Outlet/>
                 </div>
             </div>
         </div>
     );
-}
(No newline at end of file)
+}
src/admin/route/AdminRoute.tsx
--- src/admin/route/AdminRoute.tsx
+++ src/admin/route/AdminRoute.tsx
@@ -1,16 +1,20 @@
 import {Navigate, Route, Routes} from "react-router-dom";
 import {BoardListPage} from "../feature/board/master/page/BoardListPage.tsx";
 import {
-    ADMIN_AUTHOR_DETAIL_ROUTE,
+    ADMIN_AUTHOR_DETAIL_ROUTE, ADMIN_AUTHOR_GROUP_LIST_ROUTE,
     ADMIN_AUTHOR_LIST_ROUTE,
     ADMIN_AUTHOR_ROLE_LIST_ROUTE, ADMIN_BBS_ARTICLE_FORM_ROUTE,
-    ADMIN_BBS_MASTER_ROUTE, ADMIN_ROLE_FORM_ROUTE
+    ADMIN_BBS_MASTER_ROUTE, ADMIN_MENU_CREATE_MANAGE_ROUTE, ADMIN_MENU_POPUP_ROUTE, ADMIN_ROLE_FORM_ROUTE
 } from "./adminRouteMap.ts";
 import {BoardArticleListPage} from "../feature/board/article/page/BoardArticleListPage.tsx";
 import {BoardFormPage} from "../feature/board/master/page/BoardFormPage.tsx";
 import {AuthorListPage} from "../feature/role/author/page/AuthorListPage.tsx";
 import {AuthorRoleListPage} from "../feature/role/authorRole/page/AuthorRoleListPage.tsx";
 import {AuthorRoleFormPage} from "../feature/role/role/page/AuthorRoleFormPage.tsx";
+import {AuthorRoleMenuListPage} from "../feature/role/authorRoleMenu/page/AuthorRoleMenuListPage.tsx";
+import {AuthorRoleMenuPopupPage} from "../feature/role/authorRoleMenu/page/AuthorRoleMenuPopupPage.tsx";
+import {AdminLayout} from "../layout/AdminLayout.tsx";
+import {AuthorGroupListPage} from "../feature/role/authorGroup/page/AuthorGroupListPage.tsx";
 
 const ReadyPage = () => {
     return <div>Preparing menu.</div>;
@@ -19,17 +23,23 @@
 export const AdminRoute = () => {
     return (
         <Routes>
-            <Route path="/" element={<Navigate to={ADMIN_BBS_MASTER_ROUTE} replace/>}/>
-            <Route path={ADMIN_BBS_MASTER_ROUTE} element={<BoardListPage/>}/>
-            <Route path={`/admin/cop/bbs/article/:bbsId`} element={<BoardArticleListPage/>}/>
-            <Route path={`${ADMIN_BBS_ARTICLE_FORM_ROUTE}:bbsId`} element={<BoardFormPage/>}/>
-            <Route path={ADMIN_BBS_ARTICLE_FORM_ROUTE} element={<BoardFormPage/>}/>
+            <Route path={`${ADMIN_MENU_POPUP_ROUTE}/:authorCode`} element={<AuthorRoleMenuPopupPage/>}/>
 
-            <Route path={ADMIN_AUTHOR_LIST_ROUTE} element={<AuthorListPage/>}/>
-            <Route path={ADMIN_AUTHOR_DETAIL_ROUTE} element={<ReadyPage/>}/>
-            <Route path={ADMIN_AUTHOR_ROLE_LIST_ROUTE} element={<AuthorRoleListPage/>}/>
-            <Route path={ADMIN_ROLE_FORM_ROUTE} element={<AuthorRoleFormPage/>}/>
-            <Route path="*" element={<ReadyPage/>}/>
+            <Route element={<AdminLayout/>}>
+                <Route path="/" element={<Navigate to={ADMIN_BBS_MASTER_ROUTE} replace/>}/>
+                <Route path={ADMIN_BBS_MASTER_ROUTE} element={<BoardListPage/>}/>
+                <Route path={`/admin/cop/bbs/article/:bbsId`} element={<BoardArticleListPage/>}/>
+                <Route path={`${ADMIN_BBS_ARTICLE_FORM_ROUTE}:bbsId`} element={<BoardFormPage/>}/>
+                <Route path={ADMIN_BBS_ARTICLE_FORM_ROUTE} element={<BoardFormPage/>}/>
+
+                <Route path={ADMIN_AUTHOR_LIST_ROUTE} element={<AuthorListPage/>}/>
+                <Route path={ADMIN_AUTHOR_DETAIL_ROUTE} element={<ReadyPage/>}/>
+                <Route path={ADMIN_AUTHOR_ROLE_LIST_ROUTE} element={<AuthorRoleListPage/>}/>
+                <Route path={ADMIN_ROLE_FORM_ROUTE} element={<AuthorRoleFormPage/>}/>
+                <Route path={ADMIN_MENU_CREATE_MANAGE_ROUTE} element={<AuthorRoleMenuListPage/>}/>
+                <Route path={ADMIN_AUTHOR_GROUP_LIST_ROUTE} element={<AuthorGroupListPage/>}/>
+                <Route path="*" element={<ReadyPage/>}/>
+            </Route>
         </Routes>
     );
 };
src/admin/route/adminRouteMap.ts
--- src/admin/route/adminRouteMap.ts
+++ src/admin/route/adminRouteMap.ts
@@ -18,10 +18,16 @@
 export const ADMIN_LOGIN_GROUP_POLICY_ROUTE = `${ADMIN_ROUTE_PREFIX}/uat/uap/selectLoginGroupPolicyList.do`;
 export const ADMIN_MEMBER_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/umt/EgovMberManage.do`;
 export const ADMIN_POPUP_ZONE_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/popupZoneList.do`;
+
+
 export const ADMIN_MENU_CREATE_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/mnu/mcm/EgovMenuCreatManageSelect.do`;
+export const ADMIN_MENU_POPUP_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/mnu/mcm/detail.do`;
+
+export const ADMIN_AUTHOR_GROUP_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/rgm/EgovAuthorGroupList.do`;
+
+
 export const ADMIN_USER_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/umt/user/EgovUserManage.do`;
 export const ADMIN_COMMON_CODE_TREE_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/ccm/ccc/EgovCcmCmmnCodeTree.do`;
-export const ADMIN_AUTHOR_GROUP_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/rgm/EgovAuthorGroupList.do`;
 export const ADMIN_POPUP_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/egovPopupList.do`;
 export const ADMIN_WEB_LOG_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/log/clg/SelectWebLogList.do`;
 
src/styles/adm/popup.css
--- src/styles/adm/popup.css
+++ src/styles/adm/popup.css
@@ -57,7 +57,11 @@
 .popup_window .tree .tree-node{display:flex;height:auto;font-size:14px;flex-direction:column;align-items:flex-start;padding:0 12px;}
 .popup_window .tree .tree-node label{display:flex;min-height:35px;align-items:center;gap:4px;}
 .popup_window .tree-node.folder .tree-label{padding:0 0 0 26px;}
-.popup_window .tree-node.folder .tree-label::before{width:24px;height:24px;top:50%;transform:translateY(-50%);}
+.popup_window .tree-node.folder .tree-label{position:relative;}
+.popup_window .tree-node.folder .tree-label::before{position:absolute;left:0;width:24px;height:24px;top:50%;content:"";background:url(/publish/adm/images/component/icon_folder.png) no-repeat center;transform:translateY(-50%);}
+.popup_window .tree-node.folder:has(.tree-children)>.tree-label::before{background-image:url(/publish/adm/images/component/icon_folder_open.png);}
 .popup_window .tree-node.file label{min-height:25px;}
+.popup_window .tree-node.file .tree-label{position:relative;padding:0 0 0 26px;}
+.popup_window .tree-node.file .tree-label::before{position:absolute;left:0;width:24px;height:24px;top:50%;content:"";background:url(/publish/adm/images/component/icon_file.png) no-repeat center;transform:translateY(-50%);}
 .popup_window .tree .tree-children{display:flex;width:calc(100% - 24px);font-size:14px;background:#f2f4f5;flex-direction:column;gap:4px;border-radius:4px;padding:8px 12px;margin:0 auto;}
 .popup_window .tree .tree-children .tree-node{height:auto;}
src/type/pageResponse.ts
--- src/type/pageResponse.ts
+++ src/type/pageResponse.ts
@@ -9,14 +9,15 @@
     size: number;
 }
 
-export const createPageQueryResult = <T>(
-    query: UseQueryResult<PageResponse<T>>
+export const createPageQueryResult = <T, E = Record<string, unknown>>(
+    query: UseQueryResult<PageResponse<T, E>>
 ) => ({
     list: query.data?.list ?? [],
+    extraData: query.data?.extraData ?? null,
     totalItems: query.data?.totalItems ?? 0,
     totalPages: query.data?.totalPages ?? 0,
     currentPage: query.data?.currentPage ?? 0,
     size: query.data?.size ?? 0,
     isLoading: query.isLoading,
     error: query.error,
-});
(No newline at end of file)
+});
src/type/viewModel.ts
--- src/type/viewModel.ts
+++ src/type/viewModel.ts
@@ -51,8 +51,13 @@
     "totalItems" | "currentPage" | "totalPages"
 >;
 
-export type ListTableModel<TItem, TSearchParams extends SearchParams> = {
+export type ListTableModel<
+    TItem,
+    TSearchParams extends SearchParams,
+    TExtraData = unknown
+> = {
     items: TItem[];
+    extraData?: TExtraData | null;
     params: TSearchParams;
     onChange: (params: TSearchParams) => void;
     pagination: TablePaginationModel;
@@ -61,8 +66,9 @@
 export type CheckableTableModel<
     TItem,
     TSearchParams extends SearchParams,
-    TId extends string | number = string
-> = ListTableModel<TItem, TSearchParams> & {
+    TId extends string | number = string,
+    TExtraData = unknown
+> = ListTableModel<TItem, TSearchParams, TExtraData> & {
     check: TableCheckModel<TId>;
 };
 
Add a comment
List