조민수 조민수 05-20
권한관리 > 롤관리
@a6c24b3341aeb6530cedab35a231ba414d5e8064
src/admin/component/button/ActionButtonFormGroup.tsx
--- src/admin/component/button/ActionButtonFormGroup.tsx
+++ src/admin/component/button/ActionButtonFormGroup.tsx
@@ -5,7 +5,6 @@
 type ActionButtonGroupProps = {
     mode: ActionMode;
     disabled?: boolean;
-
     onCreate?: () => void;
     onUpdate?: () => void;
     onDelete?: () => void;
src/admin/feature/board/master/hook/page/useBoardFormPage.ts
--- src/admin/feature/board/master/hook/page/useBoardFormPage.ts
+++ src/admin/feature/board/master/hook/page/useBoardFormPage.ts
@@ -8,9 +8,7 @@
 import {useCreateBoard} from "../mutation/useCreateBoard.ts";
 import {useDeleteBoard} from "../mutation/useDeleteBoard.ts";
 import {useUpdateBoard} from "../mutation/useUpdateBoard.ts";
-import type {FormActionsModel, HeaderModel, StatusModel} from "../../../../../../type/viewModel.ts";
-
-export type BoardFormMode = 'create' | 'update';
+import type {FormActionsModel, FormMode, HeaderModel, StatusModel} from "../../../../../../type/viewModel.ts";
 
 type BoardFormPageModel = {
     header: HeaderModel;
@@ -19,7 +17,7 @@
         form: BoardFormItem;
         onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
     };
-    actions: FormActionsModel<BoardFormMode>;
+    actions: FormActionsModel<FormMode>;
 };
 
 const initBoardFormData: BoardFormItem = {
@@ -46,7 +44,7 @@
 
 export const useBoardFormPage = (bbsId: string): BoardFormPageModel => {
     const navigate = useNavigate();
-    const mode: BoardFormMode = bbsId ? 'update' : 'create';
+    const mode: FormMode = bbsId ? 'update' : 'create';
     const [formDraft, setFormDraft] = useState<Partial<BoardFormItem>>({});
 
     const {data, isLoading, error} = useBoardDetail(bbsId, {enabled: !!bbsId});
@@ -106,7 +104,7 @@
             }
         );
 
-        navigate(ADMIN_BBS_MASTER_ROUTE);
+        handleList();
     };
 
     const handleUpdate = async () => {
@@ -123,7 +121,7 @@
             }
         );
 
-        navigate(ADMIN_BBS_MASTER_ROUTE);
+        handleList();
     };
 
     const handleDelete = async () => {
@@ -140,7 +138,7 @@
             }
         );
 
-        navigate(ADMIN_BBS_MASTER_ROUTE);
+        handleList();
     };
 
     const handleList = () => {
src/admin/feature/role/authorGroup/component/AuthorGrouptListTable.tsx
--- src/admin/feature/role/authorGroup/component/AuthorGrouptListTable.tsx
+++ src/admin/feature/role/authorGroup/component/AuthorGrouptListTable.tsx
@@ -8,7 +8,7 @@
 
 type AuthorGroupListTableProps =
     ListTableModel<AuthorGroupListItem, SearchParams, AuthorListItem[]>
-    & RowActionsModel<{ onEdit: (authorCode: string) => void }>;
+    & RowActionsModel<{ onEdit: (uniqId: string, authorCode: string) => void }>;
 
 export const AuthorGroupListTable = ({
                                          items,
src/admin/feature/role/role/api/roleApi.ts
--- src/admin/feature/role/role/api/roleApi.ts
+++ src/admin/feature/role/role/api/roleApi.ts
@@ -1,11 +1,33 @@
 import {apiClient} from "../../../../../api/apiClient.ts";
 import type {PageResponse} from "../../../../../type/pageResponse.ts";
-import type {RoleDetailResponse, RoleSearchParams} from "../type/role.types.ts";
+import type {
+    DeleteRoleListItem,
+    RoleDetailResponse,
+    RoleFormItem,
+    RoleListItem,
+    RoleSearchParams
+} from "../type/role.types.ts";
 
 export async function fetchRoleList(params: RoleSearchParams) {
-    return apiClient.get<PageResponse<unknown>>(`/sec/rmt/list.do`, params);
+    return apiClient.get<PageResponse<RoleListItem>>(`/sec/rmt/list.do`, params);
 }
 
 export async function fetchRoleDetail(roleCode: string) {
     return apiClient.get<RoleDetailResponse>(`/sec/rmt/detail.do?roleCode=${roleCode}`);
 }
+
+export async function fetchRoleDeleteBatch(params: DeleteRoleListItem[]) {
+    return apiClient.post(`/sec/rmt/EgovRoleDeleteBatch.do`, params);
+}
+
+export async function fetchCreateRole(params: RoleFormItem) {
+    return apiClient.post(`/sec/rmt/EgovRoleInsert.do`, params);
+}
+
+export async function fetchUpdateRole(params: RoleFormItem) {
+    return apiClient.post(`/sec/rmt/EgovRoleUpdate.do`, params)
+}
+
+export async function fetchDeleteRole(roleCode: string) {
+    return apiClient.post(`/sec/rmt/EgovRoleDelete.do?roleCode=${roleCode}`);
+}
 
src/admin/feature/role/role/component/RoleFormTable.tsx (added)
+++ src/admin/feature/role/role/component/RoleFormTable.tsx
@@ -0,0 +1,87 @@
+import type {RoleFormItem} from "../type/role.types.ts";
+import type {ChangeEvent} from "react";
+import type {FormMode} from "../../../../../type/viewModel.ts";
+
+type RoleFormTableProps = {
+    form: RoleFormItem;
+    onChange: (event: ChangeEvent<HTMLInputElement>) => void;
+    mode: FormMode;
+}
+
+export const RoleFormTable = ({form, onChange, mode}: RoleFormTableProps) => {
+    return (
+        <div className="table table_type_rows">
+            <table>
+                <colgroup>
+                    <col style={{width: "200px"}}/>
+                    <col style={{width: "auto"}}/>
+                </colgroup>
+
+                <tbody>
+                {mode === "update" ?
+                    <tr>
+                        <th>
+                            <span className="required">*</span>롤코드
+                        </th>
+                        <td>
+                            <input
+                                name="roleCode"
+                                id="roleCode"
+                                className="input"
+                                type="text"
+                                value={form.roleCode}
+                                readOnly={true}
+                                title="롤 코드"
+                            />
+                        </td>
+                    </tr> : null
+                }
+
+                <tr>
+                    <th>
+                        <span className="required">*</span>롤명
+                    </th>
+                    <td>
+                        <input name="roleNm" id="roleNm" className="input" type="text" value={form.roleNm} title="롤명"
+                               onChange={onChange}/>
+                    </td>
+                </tr>
+                <tr>
+                    <th><span className="required">*</span>롤패턴</th>
+                    <td>
+                        <input name="rolePtn" id="rolePtn" className="input" type="text" value={form.rolePtn}
+                               title="롤패턴"
+                               onChange={onChange}/>
+                    </td>
+                </tr>
+                <tr>
+                    <th><span className="required">*</span>설명</th>
+                    <td>
+                        <input name="roleDc" id="roleDc" className="input" type="text" value={form.roleDc} title="설명"
+                               onChange={onChange}/>
+                    </td>
+                </tr>
+                <tr>
+                    <th>롤순서</th>
+                    <td>
+                        <input name="roleSort" id="roleSort" className="input" type="text" value={form.roleSort}
+                               onChange={onChange}
+                               title="롤sort"/>
+                    </td>
+                </tr>
+
+                {mode === "update" ?
+                    <tr>
+                        <th>등록일자</th>
+                        <td>
+                            <input name="roleCreatDe" id="roleCreatDe" className="input" type="text"
+                                   value={form.roleCreatDe} title="등록일자" readOnly={true}/>
+                        </td>
+                    </tr>
+                    : null
+                }
+                </tbody>
+            </table>
+        </div>
+    );
+}(No newline at end of file)
 
src/admin/feature/role/role/component/RoleListTable.tsx (added)
+++ src/admin/feature/role/role/component/RoleListTable.tsx
@@ -0,0 +1,45 @@
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import type {RoleListItem} from "../type/role.types.ts";
+import type {CheckableTableModel, RowActionsModel} from "../../../../../type/viewModel.ts";
+import {EmptyRow} from "../../../../component/EmptyRow.tsx";
+import {RoleListTableHeader} from "./RoleListTableHeader.tsx";
+import {RoleListTableRow} from "./RoleListTableRow.tsx";
+
+type RoleListTableProps =
+    CheckableTableModel<RoleListItem, SearchParams> &
+    RowActionsModel<{
+        onDetail: (roleCode: string) => void;
+    }>
+
+export const RoleListTable = ({items, params, onChange, pagination, check, rowActions}: RoleListTableProps) => {
+
+    return (
+        <div className="table table_type_cols">
+            <table>
+                <RoleListTableHeader
+                    params={params}
+                    onChange={onChange}
+                    checked={check.isAllChecked}
+                    indeterminate={check.isPartiallyChecked}
+                    onCheckAll={check.onCheckAll}
+                />
+                <tbody>
+                {items.length > 0
+                    ? items.map((item, index) => (
+                        <RoleListTableRow
+                            key={index}
+                            item={item}
+                            index={index}
+                            searchParams={params}
+                            checked={check.isChecked(item.roleCode)}
+                            onCheck={check.onCheck}
+                            {...pagination}
+                            {...rowActions}
+                        />
+                    ))
+                    : (<EmptyRow colSpan={7}/>)}
+                </tbody>
+            </table>
+        </div>
+    )
+}(No newline at end of file)
 
src/admin/feature/role/role/component/RoleListTableHeader.tsx (added)
+++ src/admin/feature/role/role/component/RoleListTableHeader.tsx
@@ -0,0 +1,99 @@
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {useTableSort} from "../../../../hook/useTableSort.ts";
+import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+import {SortableHeaderCell} from "../../../../component/table/SortableHeaderCell.tsx";
+
+interface RoleListTableHeaderProps {
+    params: SearchParams;
+    onChange: (params: SearchParams) => void
+    checked: boolean
+    indeterminate: boolean
+    onCheckAll: (checked: boolean) => void
+}
+
+export const RoleListTableHeader = ({
+                                        params,
+                                        onChange,
+                                        checked,
+                                        indeterminate,
+                                        onCheckAll
+                                    }: RoleListTableHeaderProps) => {
+    const {handleSort, getSortIcon, isSorted} = useTableSort(params, onChange);
+
+    return (
+        <>
+            <colgroup>
+                <col style={{width: "40px"}}/>
+                <col style={{width: "8%"}}/>
+                <col style={{width: "19%"}}/>
+                <col style={{width: "34%"}}/>
+                <col style={{width: "10%"}}/>
+                <col style={{width: "25%"}}/>
+                <col style={{width: "10%"}}/>
+            </colgroup>
+
+            <thead>
+            <tr>
+                <th>
+                    <CheckBox
+                        id={"roleCheckAll"}
+                        name={"checkAll"}
+                        checked={checked}
+                        onChange={onCheckAll}
+                        indeterminate={indeterminate}
+                    />
+                </th>
+                <SortableHeaderCell
+                    field={"SORT_TEMP_NO"}
+                    active={isSorted("SORT_TEMP_NO")}
+                    icon={getSortIcon("SORT_TEMP_NO")}
+                    onSort={handleSort}
+                >
+                    번호
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field={"ROLE_DC"}
+                    active={isSorted("ROLE_DC")}
+                    icon={getSortIcon("ROLE_DC")}
+                    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_NM"}
+                    active={isSorted("ROLE_NM")}
+                    icon={getSortIcon("ROLE_NM")}
+                    onSort={handleSort}
+                >
+                    롤명
+                </SortableHeaderCell>
+                <SortableHeaderCell
+                    field={"ROLE_CREAT_DE"}
+                    active={isSorted("ROLE_CREAT_DE")}
+                    icon={getSortIcon("ROLE_CREAT_DE")}
+                    onSort={handleSort}
+                >
+                    등록일자
+                </SortableHeaderCell>
+            </tr>
+            </thead>
+        </>
+    )
+
+}(No newline at end of file)
 
src/admin/feature/role/role/component/RoleListTableRow.tsx (added)
+++ src/admin/feature/role/role/component/RoleListTableRow.tsx
@@ -0,0 +1,59 @@
+import {getTableRowNumber} from "../../../../component/table/getTableRowNumber.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import type {RoleListItem} from "../type/role.types.ts";
+import {CheckBox} from "../../../../component/checkbox/CheckBox.tsx";
+
+interface RoleListTableRowProps {
+    item: RoleListItem
+    index: number
+    searchParams: SearchParams
+    totalItems: number
+    currentPage: number
+    checked: boolean
+    onCheck: (id: string, checked: boolean) => void
+    onDetail: (roleCode: string) => void
+}
+
+export const RoleListTableRow = ({
+                                     item,
+                                     index,
+                                     searchParams,
+                                     totalItems,
+                                     currentPage,
+                                     checked,
+                                     onCheck,
+                                     onDetail
+                                 }: RoleListTableRowProps) => {
+    const rowNumber = getTableRowNumber({
+        searchParams,
+        totalItems,
+        currentPage,
+        index
+    });
+    const roleCode = item.roleCode;
+
+    return (
+        <tr>
+            <td>
+                <CheckBox id={`roleCheckList_${roleCode}`}
+                          name={`checkList`}
+                          value={roleCode}
+                          checked={checked}
+                          onChange={(nextChecked) => onCheck(roleCode, nextChecked)}
+                />
+            </td>
+            <td>
+                {rowNumber}
+            </td>
+            <td>
+                <button onClick={() => onDetail(roleCode)}>
+                    {item.roleDc}
+                </button>
+            </td>
+            <td>{item.roleSort}</td>
+            <td>{item.rolePtn}</td>
+            <td>{item.roleNm}</td>
+            <td>{item.roleCreatDe}</td>
+        </tr>
+    )
+}(No newline at end of file)
 
src/admin/feature/role/role/hook/mutation/useCreateRole.ts (added)
+++ src/admin/feature/role/role/hook/mutation/useCreateRole.ts
@@ -0,0 +1,15 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {fetchCreateRole} from "../../api/roleApi.ts";
+
+export const useCreateRole = () => {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: fetchCreateRole,
+        onSuccess: () => {
+            queryClient.invalidateQueries({
+                queryKey: ['roleDetail']
+            });
+        }
+    })
+}(No newline at end of file)
 
src/admin/feature/role/role/hook/mutation/useDeleteRole.ts (added)
+++ src/admin/feature/role/role/hook/mutation/useDeleteRole.ts
@@ -0,0 +1,15 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {fetchDeleteRole} from "../../api/roleApi.ts";
+
+export const useDeleteRole = () => {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: fetchDeleteRole,
+        onSuccess: () => {
+            queryClient.invalidateQueries({
+                queryKey: ['roleDetail']
+            });
+        }
+    })
+}(No newline at end of file)
 
src/admin/feature/role/role/hook/mutation/useDeleteRoleBatch.ts (added)
+++ src/admin/feature/role/role/hook/mutation/useDeleteRoleBatch.ts
@@ -0,0 +1,15 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {fetchRoleDeleteBatch} from "../../api/roleApi.ts";
+
+export const useDeleteRoleBatch = () => {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: fetchRoleDeleteBatch,
+        onSuccess: () => {
+            queryClient.invalidateQueries({
+                queryKey: ['roleList']
+            })
+        }
+    })
+}(No newline at end of file)
 
src/admin/feature/role/role/hook/mutation/useUpdateRole.ts (added)
+++ src/admin/feature/role/role/hook/mutation/useUpdateRole.ts
@@ -0,0 +1,15 @@
+import {useMutation, useQueryClient} from "@tanstack/react-query";
+import {fetchUpdateRole} from "../../api/roleApi.ts";
+
+export const useUpdateRole = () => {
+    const queryClient = useQueryClient();
+
+    return useMutation({
+        mutationFn: fetchUpdateRole,
+        onSuccess: () => {
+            queryClient.invalidateQueries({
+                queryKey: ['roleDetail']
+            });
+        }
+    })
+}(No newline at end of file)
src/admin/feature/role/role/hook/page/useRoleFormPage.ts
--- src/admin/feature/role/role/hook/page/useRoleFormPage.ts
+++ src/admin/feature/role/role/hook/page/useRoleFormPage.ts
@@ -1,8 +1,158 @@
 import {useRoleDetail} from "../query/useRoleDetail.ts";
+import {ADMIN_ROLE_LIST_ROUTE} from "../../../../../route/adminRouteMap.ts";
+import {type ChangeEvent, useMemo, useState} from "react";
+import {toast} from "react-toastify";
+import type {RoleDetailResponse, RoleFormItem} from "../../type/role.types.ts";
+import type {FormActionsModel, FormMode, HeaderModel, StatusModel} from "../../../../../../type/viewModel.ts";
+import {useCreateRole} from "../mutation/useCreateRole.ts";
+import {useUpdateRole} from "../mutation/useUpdateRole.ts";
+import {useDeleteRole} from "../mutation/useDeleteRole.ts";
+import {useNavigate} from "react-router-dom";
 
-export const useRoleFormPage = (roleCode: string) => {
+type RoleFormPageModel = {
+    header: HeaderModel;
+    status: StatusModel;
+    form: {
+        form: RoleFormItem;
+        onChange: (event: ChangeEvent<HTMLInputElement>) => void;
+    };
+    actions: FormActionsModel<FormMode>
+}
+
+const initRoleForm = {
+    roleCode: '',
+    roleNm: '',
+    rolePtn: '',
+    roleDc: '',
+    roleSort: '',
+    roleCreatDe: ''
+}
+
+const createInitialForm = (item?: RoleDetailResponse) => ({
+    ...initRoleForm,
+    ...item,
+});
+
+export const useRoleFormPage = (roleCode: string):RoleFormPageModel => {
     const mode = roleCode ? "update" : "create";
     const {data, isLoading, error} = useRoleDetail(roleCode);
+    const [formDraft, setFormDraft] = useState<Partial<RoleDetailResponse>>({});
+    const baseForm = useMemo(
+        () => createInitialForm(data),
+        [data],
+    );
+    const navigate = useNavigate();
 
-    return {mode, data, isLoading, error}
-}
(No newline at end of file)
+    const {mutateAsync: createRole, isPending: isCreating} = useCreateRole();
+    const {mutateAsync: updateRole, isPending: isUpdating} = useUpdateRole();
+    const {mutateAsync: deleteRole, isPending: isDeleting} = useDeleteRole();
+    const isPending = isCreating || isUpdating || isDeleting;
+
+
+    const form = {
+        ...baseForm,
+        ...formDraft,
+    };
+
+    const title = `롤 ${mode === "create" ? '생성' : '수정'}`;
+    const breadcrumb = [
+        {label: '권한 관리'},
+        {label: '롤 관리', url: ADMIN_ROLE_LIST_ROUTE},
+        {label: title}
+    ]
+
+    const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
+        const {name, value} = event.target;
+
+        setFormDraft((prev) => ({...prev, [name]: value}));
+    }
+
+    const validateForm = () => {
+        if(!form.roleNm.trim()) {
+            toast.warning('롤명을 입력해주세요.');
+            return false;
+        }
+
+        if(!form.rolePtn.trim()) {
+            toast.warning('롤 패턴을 입력해주세요.');
+            return false;
+        }
+
+        if(!form.roleDc.trim()) {
+            toast.warning('롤 설명을 적어주세요.');
+            return false;
+        }
+        return true;
+    }
+    const handleCreate = async () => {
+        if(!validateForm()) {
+            return;
+        }
+        await toast.promise(
+            createRole(form),
+            {
+                pending: '등록 중...',
+                success: '등록 완료',
+                error: '등록 실패'
+            }
+        )
+        handleList();
+    }
+
+    const handleUpdate = async () => {
+        if(!validateForm()) {
+            return;
+        }
+
+        await toast.promise(
+            updateRole(form),
+            {
+                pending: '변경 중...',
+                success: '변경 완료',
+                error: '변경 실패'
+            }
+        )
+        handleList();
+    }
+
+    const handleDelete = async () => {
+        await toast.promise(
+            deleteRole(form.roleCode),
+            {
+                pending: '삭제 중...',
+                success: '삭제 완료',
+                error: '삭제 실패'
+            }
+        )
+        handleList();
+    }
+    const handleList = () => {
+        navigate(ADMIN_ROLE_LIST_ROUTE);
+    }
+
+
+    return {
+        header: {
+            title,
+            breadcrumb,
+            homeUrl: '#',
+        },
+        status: {
+            isLoading,
+            error,
+            successMessage: '데이터 조회가 완료되었습니다.'
+        },
+        form : {
+            form,
+            onChange: handleChange
+        },
+        actions: {
+            mode,
+            disabled: isPending,
+            onCreate: handleCreate,
+            onUpdate: handleUpdate,
+            onDelete: handleDelete,
+            onList: handleList
+        },
+    }
+}
 
src/admin/feature/role/role/hook/page/useRoleListPage.ts (added)
+++ src/admin/feature/role/role/hook/page/useRoleListPage.ts
@@ -0,0 +1,155 @@
+import type {DeleteRoleListItem, RoleListItem, RoleSearchParams} from "../../type/role.types.ts";
+import type {
+    CheckableTableModel,
+    HeaderModel, ListActionsModel, PaginationModel,
+    RowActionsModel,
+    SearchModel,
+    StatusModel
+} from "../../../../../../type/viewModel.ts";
+import {useMemo, useState} from "react";
+import {useRoleList} from "../query/useRoleList.ts";
+import {useCheckedList} from "../../../../../hook/useCheckedList.ts";
+import {toast} from "react-toastify";
+import {useDeleteRoleBatch} from "../mutation/useDeleteRoleBatch.ts";
+import {useNavigate} from "react-router-dom";
+import {ADMIN_ROLE_FORM_ROUTE} from "../../../../../route/adminRouteMap.ts";
+
+type RoleRowActions = {
+    onDetail: (roleCode: string) => void;
+}
+
+type RoleListPageModel = {
+    header: HeaderModel;
+    status: StatusModel;
+    search: SearchModel<RoleSearchParams>
+    table: CheckableTableModel<RoleListItem, RoleSearchParams> & RowActionsModel<RoleRowActions>;
+    actions: ListActionsModel;
+    pagination: PaginationModel;
+}
+
+const initSearchParam: RoleSearchParams = {
+    pageIndex: 1,
+    pageUnit: 10,
+    searchCnd: "0",
+    searchKeyword: "",
+    searchSortCnd: "",
+    searchSortOrd: "",
+};
+
+const pageSizeOptions = [
+    {value: '10', label: '10줄'},
+    {value: '20', label: '20줄'},
+    {value: '30', label: '30줄'},
+]
+
+const searchOptions = [
+    {value: '', label: '전체'},
+    {value: '0', label: '롤명'},
+    {value: '1', label: '롤설명'},
+    {value: '2', label: '롤패턴'},
+]
+
+const title = '롤관리';
+const breadcrumb = [
+    {label: '권한관리'},
+    {label: title}
+];
+const successMessage = '롤관리를 조회하였습니다.'
+
+export const useRoleListPage = ():RoleListPageModel => {
+    const [searchParams, setSearchParams] = useState({...initSearchParam});
+    const {currentPage, totalPages, list, totalItems, isLoading, error, size} = useRoleList(searchParams);
+    const {mutateAsync: deleteRoleBatch} = useDeleteRoleBatch();
+    const navigate = useNavigate();
+
+    const handlePageChange = (pageIndex: number) => {
+        setSearchParams({...initSearchParam, pageIndex});
+    }
+    const roleCodes = useMemo(
+        () => list.map((item) => item.roleCode),
+        [list]
+    );
+    const {
+        checkedIds,
+        isAllChecked,
+        isPartiallyChecked,
+        isChecked,
+        handleCheck,
+        handleCheckAll,
+    } = useCheckedList<string>(roleCodes);
+
+    const handleDetail = (roleCode: string) => {
+        navigate(`${ADMIN_ROLE_FORM_ROUTE}/${roleCode}`);
+    }
+    const handleDeleteBatch = async () => {
+        if(checkedIds.length === 0) {
+            toast.warning('삭제할 롤을 선택해주세요.');
+            return;
+        }
+        const roleList: DeleteRoleListItem[] = checkedIds.map((roleCode) => ({
+            roleCode
+        }));
+
+        await toast.promise(
+            deleteRoleBatch(roleList),
+            {
+                pending: '삭제 처리중...',
+                success: '삭제 완료',
+                error: '삭제 실패'
+            }
+        )
+    }
+    const handleForm = () => {
+        navigate(ADMIN_ROLE_FORM_ROUTE);
+    }
+
+    return {
+        header: {
+            title,
+            breadcrumb,
+        },
+        status: {
+            isLoading,
+            error,
+            successMessage
+        },
+        search: {
+            totalItems,
+            searchParams,
+            onChange: setSearchParams,
+            pageSizeOptions,
+            searchOptions
+        },
+        table: {
+            items: list,
+            params: searchParams,
+            onChange: setSearchParams,
+            pagination: {
+                totalItems,
+                currentPage,
+                totalPages
+            },
+            check: {
+                isAllChecked,
+                isPartiallyChecked,
+                isChecked,
+                onCheck: handleCheck,
+                onCheckAll: handleCheckAll,
+            },
+            rowActions: {
+                onDetail: handleDetail,
+            }
+        },
+        actions: {
+            onDelete: handleDeleteBatch,
+            onCreate: handleForm
+        },
+        pagination: {
+            totalItems,
+            totalPages,
+            currentPage,
+            size,
+            onPageChange: handlePageChange
+        }
+    }
+}(No newline at end of file)
src/admin/feature/role/role/hook/query/useRoleDetail.ts
--- src/admin/feature/role/role/hook/query/useRoleDetail.ts
+++ src/admin/feature/role/role/hook/query/useRoleDetail.ts
@@ -2,10 +2,13 @@
 import {fetchRoleDetail} from "../../api/roleApi.ts";
 
 export function useRoleDetail(roleCode: string) {
-    return useQuery({
+    const query = useQuery({
         queryKey: ['roleDetail', roleCode],
         queryFn: () => fetchRoleDetail(roleCode),
         placeholderData: keepPreviousData,
         enabled: !!roleCode,
-    })
+    });
+    console.log(query)
+
+    return query
 }
(No newline at end of file)
src/admin/feature/role/role/hook/query/useRoleList.ts
--- src/admin/feature/role/role/hook/query/useRoleList.ts
+++ src/admin/feature/role/role/hook/query/useRoleList.ts
@@ -1,11 +1,14 @@
 import {keepPreviousData, useQuery} from "@tanstack/react-query";
 import type {RoleSearchParams} from "../../type/role.types.ts";
 import {fetchRoleList} from "../../api/roleApi.ts";
+import {createPageQueryResult} from "../../../../../../type/pageResponse.ts";
 
 export function useRoleList(searchParams: RoleSearchParams) {
-    return useQuery({
+    const query = useQuery({
         queryKey: ['roleList', searchParams],
         queryFn: () => fetchRoleList(searchParams),
         placeholderData: keepPreviousData
     });
+
+    return createPageQueryResult(query);
 }
(No newline at end of file)
 
src/admin/feature/role/role/page/AuthorRoleFormPage.tsx (deleted)
--- src/admin/feature/role/role/page/AuthorRoleFormPage.tsx
@@ -1,7 +0,0 @@
-export const AuthorRoleFormPage = () => {
-    return (
-
-        <>
-        </>
-    );
-}(No newline at end of file)
 
src/admin/feature/role/role/page/RoleFormPage.tsx (added)
+++ src/admin/feature/role/role/page/RoleFormPage.tsx
@@ -0,0 +1,20 @@
+import {useParams} from "react-router-dom";
+import {useRoleFormPage} from "../hook/page/useRoleFormPage.ts";
+import {PageHeader} from "../../../../component/PageHeader.tsx";
+import {ActionButtonFormGroup} from "../../../../component/button/ActionButtonFormGroup.tsx";
+import {RoleFormTable} from "../component/RoleFormTable.tsx";
+import {useLoadingToast} from "../../../../hook/useLoadingToast.ts";
+
+export const RoleFormPage = () => {
+    const {roleCode = ''} = useParams();
+    const {header, form, actions, status} = useRoleFormPage(roleCode);
+
+    useLoadingToast(status);
+    return (
+        <>
+            <PageHeader {...header}/>
+            <RoleFormTable {...form} mode={actions.mode}/>
+            <ActionButtonFormGroup {...actions}/>
+        </>
+    );
+}(No newline at end of file)
 
src/admin/feature/role/role/page/RoleListPage.tsx (added)
+++ src/admin/feature/role/role/page/RoleListPage.tsx
@@ -0,0 +1,22 @@
+import {useRoleListPage} from "../hook/page/useRoleListPage.ts";
+import {useLoadingToast} from "../../../../hook/useLoadingToast.ts";
+import { PageHeader } from "../../../../component/PageHeader.tsx";
+import {ActionButtonListGroup} from "../../../../component/button/ActionButtonListGroup.tsx";
+import {ListSearchForm} from "../../../../component/ListSearchForm.tsx";
+import {Pagination} from "../../../../component/pagination/Pagination.tsx";
+import {RoleListTable} from "../component/RoleListTable.tsx";
+
+export const RoleListPage = () => {
+    const {header, status, search, table, pagination, actions} = useRoleListPage();
+    useLoadingToast(status);
+
+    return (
+        <>
+            <PageHeader {...header} />
+            <ListSearchForm {...search} totalLabel={'총 게시물'}/>
+            <RoleListTable {...table}/>
+            <ActionButtonListGroup {...actions}/>
+            <Pagination {...pagination} />
+        </>
+    )
+}(No newline at end of file)
src/admin/feature/role/role/type/role.types.ts
--- src/admin/feature/role/role/type/role.types.ts
+++ src/admin/feature/role/role/type/role.types.ts
@@ -11,3 +11,25 @@
     roleSort: string,
     roleCreatDe: string,
 }
+
+export interface RoleListItem {
+    roleCode: string;
+    roleDc: string;
+    rolePtn: string;
+    roleSort: string;
+    roleNm: string;
+    roleCreatDe: string;
+}
+
+export interface DeleteRoleListItem {
+    roleCode: string;
+}
+
+export interface RoleFormItem {
+    roleCode: string;
+    roleDc: string;
+    rolePtn: string;
+    roleSort: string;
+    roleNm: string;
+    roleCreatDe: string;
+}
src/admin/route/AdminRoute.tsx
--- src/admin/route/AdminRoute.tsx
+++ src/admin/route/AdminRoute.tsx
@@ -4,17 +4,19 @@
     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_MENU_CREATE_MANAGE_ROUTE, ADMIN_MENU_POPUP_ROUTE, ADMIN_ROLE_FORM_ROUTE
+    ADMIN_BBS_MASTER_ROUTE, ADMIN_MENU_CREATE_MANAGE_ROUTE, ADMIN_MENU_POPUP_ROUTE, ADMIN_ROLE_FORM_ROUTE,
+    ADMIN_ROLE_LIST_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 {RoleFormPage} from "../feature/role/role/page/RoleFormPage.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";
+import {RoleListPage} from "../feature/role/role/page/RoleListPage.tsx";
 
 const ReadyPage = () => {
     return <div>Preparing menu.</div>;
@@ -35,9 +37,11 @@
                 <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={ADMIN_ROLE_LIST_ROUTE} element={<RoleListPage/>}/>
+                <Route path={ADMIN_ROLE_FORM_ROUTE} element={<RoleFormPage/>}/>
+                <Route path={`${ADMIN_ROLE_FORM_ROUTE}/:roleCode`} element={<RoleFormPage/>}/>
                 <Route path="*" element={<ReadyPage/>}/>
             </Route>
         </Routes>
src/type/viewModel.ts
--- src/type/viewModel.ts
+++ src/type/viewModel.ts
@@ -90,3 +90,5 @@
     onDelete?: () => void | Promise<void>;
     onList?: () => void;
 };
+
+export type FormMode = 'create' | 'update'
Add a comment
List