feat: 그룹 관리 작업
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
import { GroupsPage } from "@/pages/groups";
|
||||
export default GroupsPage;
|
||||
@@ -35,6 +35,16 @@ export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getGroupMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList> {
|
||||
const response = await client.get<MenuAuthorityList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/group`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
export { MenuAuthorityApi } from "./api/menu-authority-service";
|
||||
export {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthority,
|
||||
type MenuAuthorityPayload,
|
||||
type MenuAuthorityFormData,
|
||||
} from "./model/menu-authority-interface";
|
||||
export { AuthorityButton } from "./ui/authority-button";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
|
||||
@@ -43,6 +43,9 @@ export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
getGroupMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList>;
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import type {
|
||||
} from "../model/role-interface";
|
||||
|
||||
export class RoleApiService implements RoleService {
|
||||
public async getRoles(params: RoleParams): Promise<RoleList> {
|
||||
public async getRoles(params?: RoleParams): Promise<RoleList> {
|
||||
const response = await client.get<RoleList>(ENDPOINT.USER_SERVICE.ROLES, {
|
||||
params,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export { RoleApi } from "./api/role-service";
|
||||
export {
|
||||
roleFormSchema,
|
||||
type Role,
|
||||
type RoleParams,
|
||||
type RoleFormData,
|
||||
type RoleList,
|
||||
} from "./model/role-interface";
|
||||
+1
-1
@@ -28,7 +28,7 @@ export type RoleList = PageableList<Role>;
|
||||
export type RoleFormData = z.infer<typeof roleFormSchema>;
|
||||
|
||||
export interface RoleService {
|
||||
getRoles: (params: RoleParams) => Promise<RoleList>;
|
||||
getRoles: (params?: RoleParams) => Promise<RoleList>;
|
||||
getRoleInfo: (roleId: string) => Promise<Role>;
|
||||
createRole: (data: RoleFormData) => Promise<void>;
|
||||
updateRole: (roleId: string, data: RoleFormData) => Promise<void>;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
cn,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
@@ -12,12 +13,14 @@ import { SiteApi } from "../api/site-service";
|
||||
type SiteSelectProps = {
|
||||
placeholder?: string;
|
||||
enabledAllSite?: boolean;
|
||||
className?: string;
|
||||
} & React.ComponentPropsWithoutRef<typeof Select>;
|
||||
|
||||
function SiteSelect({
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
enabledAllSite,
|
||||
className,
|
||||
...props
|
||||
}: SiteSelectProps) {
|
||||
const { data: sites } = useQuery({
|
||||
@@ -36,7 +39,7 @@ function SiteSelect({
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -58,12 +58,14 @@ export class GroupUsersApiService implements GroupUserService {
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
public async deleteGroupUser(
|
||||
grpSn: number,
|
||||
groupId: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${groupId}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupUsersApi = new GroupUsersApiService();
|
||||
|
||||
@@ -5,8 +5,7 @@ import type {
|
||||
GroupSearchParams,
|
||||
GroupList,
|
||||
GroupDetailResponse,
|
||||
GroupSaveData,
|
||||
GroupUpdateData,
|
||||
GroupFormData,
|
||||
} from "../model/groups-interface";
|
||||
|
||||
export class GroupsApiService implements GroupService {
|
||||
@@ -20,15 +19,15 @@ export class GroupsApiService implements GroupService {
|
||||
}
|
||||
|
||||
// 그룹 단건 조회
|
||||
public async findById(id: number): Promise<GroupDetailResponse> {
|
||||
public async findById(groupId: string): Promise<GroupDetailResponse> {
|
||||
const response = await client.get<GroupDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${id}`,
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 저장
|
||||
public async saveGroup(data: GroupSaveData): Promise<GroupDetailResponse> {
|
||||
public async saveGroup(data: GroupFormData): Promise<GroupDetailResponse> {
|
||||
const response = await client.post<GroupDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}`,
|
||||
data,
|
||||
@@ -38,21 +37,23 @@ export class GroupsApiService implements GroupService {
|
||||
|
||||
// 그룹 수정
|
||||
public async updateGroup(
|
||||
id: number,
|
||||
data: GroupUpdateData,
|
||||
groupId: string,
|
||||
data: GroupFormData,
|
||||
): Promise<GroupDetailResponse> {
|
||||
const response = await client.put<GroupDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${id}`,
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 삭제
|
||||
public async deleteGroup(id: number): Promise<boolean> {
|
||||
public async deleteGroup(groupId: string): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${id}`,
|
||||
`${ENDPOINT.CMS_SERVICE.GROUPS}/${groupId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupsApi = new GroupsApiService();
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./groups-service";
|
||||
export * from "./group-users-service";
|
||||
@@ -0,0 +1 @@
|
||||
export { GroupsPage } from "./ui/groups-page";
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityPayload,
|
||||
MenuAuthorityFormData,
|
||||
} from "@/entities/menu-authority";
|
||||
|
||||
export const createColumns = (
|
||||
form: UseFormReturn<MenuAuthorityFormData>,
|
||||
): ColumnDef<MenuAuthority>[] => {
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "menuNm",
|
||||
header: "메뉴명",
|
||||
},
|
||||
{
|
||||
accessorKey: "mngAuthrtYn",
|
||||
header: "관리권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"regAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"mdfcnAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"delAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regAuthrtYn",
|
||||
header: "등록권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnAuthrtYn",
|
||||
header: "수정권한 ",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delAuthrtYn",
|
||||
header: "삭제권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqAuthrtYn",
|
||||
header: "조회권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 사용자 검색 파라미터 인터페이스
|
||||
export interface GroupUserSearchParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
grpSn?: number;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
export interface GroupUserSearchParams extends PageableParams {
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 목록 응답 인터페이스
|
||||
@@ -103,5 +100,5 @@ export interface GroupUserService {
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
deleteGroupUser: (grpSn: number, userUnqId: string) => Promise<boolean>;
|
||||
deleteGroupUser: (groupId: string, userUnqId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Checkbox } from "@common/components";
|
||||
import { type Table, type Row, type ColumnDef } from "@tanstack/react-table";
|
||||
import { type GroupUserListResponseDto } from "./group-users-interface";
|
||||
|
||||
export const columns: ColumnDef<GroupUserListResponseDto>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }: { table: Table<GroupUserListResponseDto> }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: Row<GroupUserListResponseDto> }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "사용자ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "userName",
|
||||
header: "이름",
|
||||
},
|
||||
{
|
||||
accessorKey: "ogdpSeNm",
|
||||
header: "소속",
|
||||
},
|
||||
{
|
||||
accessorKey: "mphn",
|
||||
header: "연락처",
|
||||
cell: ({ row }) => {
|
||||
const phone = row.getValue("mphn") as string;
|
||||
if (!phone) return "";
|
||||
|
||||
const numbers = phone.replace(/\D/g, "");
|
||||
if (numbers.length === 10) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`;
|
||||
} else if (numbers.length === 11) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
|
||||
} else {
|
||||
return phone;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,56 +1,38 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 검색 파라미터 인터페이스
|
||||
export interface GroupSearchParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
grpNm?: string;
|
||||
useYn?: string;
|
||||
export interface GroupSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
groupId?: string;
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
delYn?: string;
|
||||
authrtId?: string;
|
||||
siteId?: string;
|
||||
groupExpln?: string;
|
||||
roleId?: string;
|
||||
mngrYn?: string;
|
||||
}
|
||||
|
||||
// 그룹 목록 응답 인터페이스
|
||||
export interface GroupListResponseDto {
|
||||
grpSn: number;
|
||||
grpNm: string;
|
||||
grpExpln: string;
|
||||
useYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupSeNm: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
groupExpln: string;
|
||||
siteId: string;
|
||||
mngrYn: string;
|
||||
roleId: string;
|
||||
roleNm: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
rgtrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 페이지 응답 인터페이스
|
||||
export interface GroupList {
|
||||
content: GroupListResponseDto[];
|
||||
pageable: {
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
offset: number;
|
||||
pageSize: number;
|
||||
pageNumber: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
};
|
||||
last: boolean;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
size: number;
|
||||
number: number;
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
first: boolean;
|
||||
numberOfElements: number;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
// 그룹 상세 응답 인터페이스
|
||||
export interface GroupDetailResponse {
|
||||
grpSn: number;
|
||||
@@ -65,19 +47,20 @@ export interface GroupDetailResponse {
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 저장 데이터 인터페이스
|
||||
export interface GroupSaveData {
|
||||
grpNm: string;
|
||||
grpExpln: string;
|
||||
useYn: string;
|
||||
}
|
||||
// 그룹 저장 데이터 스키마
|
||||
export const groupFormSchema = z.object({
|
||||
groupId: z.string().min(1, { message: "그룹 ID를 입력해주세요." }),
|
||||
groupNm: z.string().min(1, { message: "그룹명을 입력해주세요." }),
|
||||
groupSeCd: z.string().min(1, { message: "그룹구분을 선택해주세요." }),
|
||||
groupExpln: z.string().optional(),
|
||||
delYn: z.string().default("N"),
|
||||
authrtId: z.string().min(1, { message: "권한ID를 선택해주세요." }),
|
||||
siteId: z.string().optional(),
|
||||
mngrYn: z.string().default("N"),
|
||||
});
|
||||
|
||||
// 그룹 수정 데이터 인터페이스
|
||||
export interface GroupUpdateData {
|
||||
grpNm: string;
|
||||
grpExpln: string;
|
||||
useYn: string;
|
||||
}
|
||||
export type GroupList = PageableList<GroupListResponseDto>;
|
||||
export type GroupFormData = z.infer<typeof groupFormSchema>;
|
||||
|
||||
// 그룹 서비스 인터페이스
|
||||
export interface GroupService {
|
||||
@@ -85,17 +68,17 @@ export interface GroupService {
|
||||
findPageGroup: (params: GroupSearchParams) => Promise<GroupList>;
|
||||
|
||||
// 그룹 단건 조회
|
||||
findById: (id: number) => Promise<GroupDetailResponse>;
|
||||
findById: (groupId: string) => Promise<GroupDetailResponse>;
|
||||
|
||||
// 그룹 저장
|
||||
saveGroup: (data: GroupSaveData) => Promise<GroupDetailResponse>;
|
||||
saveGroup: (data: GroupFormData) => Promise<GroupDetailResponse>;
|
||||
|
||||
// 그룹 수정
|
||||
updateGroup: (
|
||||
id: number,
|
||||
data: GroupUpdateData,
|
||||
groupId: string,
|
||||
data: GroupFormData,
|
||||
) => Promise<GroupDetailResponse>;
|
||||
|
||||
// 그룹 삭제
|
||||
deleteGroup: (id: number) => Promise<boolean>;
|
||||
deleteGroup: (groupId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { type GroupListResponseDto } from "./groups-interface";
|
||||
import { MenuAuthorityDialog } from "../ui/menu-authority-dialog";
|
||||
|
||||
export const columns: ColumnDef<GroupListResponseDto>[] = [
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "그룹코드",
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-center gap-2">
|
||||
<MenuAuthorityDialog groupId={row.original.groupId} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./groups-interface";
|
||||
export * from "./group-users-interface";
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import { RoleSelect } from "./role-select";
|
||||
import type { GroupFormData } from "../model/groups-interface";
|
||||
|
||||
type GroupFormProps = {
|
||||
form: UseFormReturn<GroupFormData>;
|
||||
};
|
||||
|
||||
function GroupForm({ form }: GroupFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>그룹코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[24rem] !text-[1.3rem]"
|
||||
placeholder="그룹코드를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[24rem] !text-[1.3rem]"
|
||||
placeholder="그룹명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
upCd="GROUPSECD"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="h-[10rem] resize-none !text-[1.3rem]"
|
||||
placeholder="그룹설명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SiteSelect
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>관리권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mngrYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="mngrYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="mngrY" />
|
||||
<Label htmlFor="mngrY">Y</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="mngrN" />
|
||||
<Label htmlFor="mngrN">N</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authrtId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RoleSelect
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
mngrYn={form.watch("mngrYn")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupForm };
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/group-users-table-columns";
|
||||
import { GroupUsersApi } from "../api/group-users-service";
|
||||
|
||||
type GroupUsersSectionProps = {
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: groupUsersList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId, pagination],
|
||||
queryFn: () =>
|
||||
GroupUsersApi.findPageGroupUser({
|
||||
keyword: groupId,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: !!groupId,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: groupUsersList?.content ?? [],
|
||||
columns,
|
||||
rowCount: groupUsersList?.totalElements ?? 0,
|
||||
state: { pagination, rowSelection },
|
||||
manualPagination: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row) => row.userUnqId,
|
||||
});
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const selectedRows = Object.keys(rowSelection);
|
||||
if (selectedRows.length === 0 || !groupId) return;
|
||||
|
||||
try {
|
||||
for (const userUnqId of selectedRows) {
|
||||
await GroupUsersApi.deleteGroupUser(groupId, userUnqId);
|
||||
}
|
||||
refetch();
|
||||
setRowSelection({});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 사용자 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button>등록</Button>
|
||||
<Button
|
||||
color="red"
|
||||
disabled={table.getSelectedRowModel().rows.length === 0}
|
||||
onClick={handleDeleteSelected}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-25.5rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupUsersSection };
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
} from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { GroupForm } from "./group-form";
|
||||
import { GroupsApi } from "../api/groups-service";
|
||||
import { groupFormSchema, type GroupFormData } from "../model/groups-interface";
|
||||
|
||||
type GroupsDialogProps = {
|
||||
groupId?: string;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
const GroupsDialog = memo(
|
||||
({ groupId, onSuccess, ...props }: GroupsDialogProps) => {
|
||||
const isRegistration = !groupId;
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
defaultValues: {
|
||||
mngrYn: "N",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||
mutationFn: (data: GroupFormData) => GroupsApi.saveGroup(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", () => {
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateGroup } = useMutationWrapper({
|
||||
mutationFn: (data: GroupFormData) =>
|
||||
GroupsApi.updateGroup(groupId ?? "", data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit =
|
||||
(callback: (data: GroupFormData) => void) =>
|
||||
async (data: GroupFormData) => {
|
||||
const confirmMessage = isRegistration
|
||||
? "등록하시겠습니까?"
|
||||
: "수정하시겠습니까?";
|
||||
|
||||
if (await confirm(confirmMessage)) callback(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>등록</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isRegistration ? "그룹 등록" : "그룹 수정"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(
|
||||
onSubmit(isRegistration ? createGroup : updateGroup),
|
||||
)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<GroupForm form={form} />
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
{isRegistration ? "등록" : "수정"}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="white" size="lg">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
GroupsDialog.displayName = "GroupsDialog";
|
||||
|
||||
export { GroupsDialog };
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { GroupsSection } from "./groups-section";
|
||||
import { GroupUsersSection } from "./group-users-section";
|
||||
|
||||
function GroupsPage() {
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<GroupsSection handleSelectedGroup={setSelectedGroupId} />
|
||||
<GroupUsersSection groupId={selectedGroupId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupsPage };
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { DataTable } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/groups-table-columns";
|
||||
import { GroupsApi } from "../api/groups-service";
|
||||
import { GroupsDialog } from "./groups-dialog";
|
||||
|
||||
type GroupsSectionProps = {
|
||||
handleSelectedGroup: (groupId: string) => void;
|
||||
};
|
||||
|
||||
function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: groupsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, pagination],
|
||||
queryFn: () =>
|
||||
GroupsApi.findPageGroup({
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: groupsList?.content ?? [],
|
||||
columns,
|
||||
rowCount: groupsList?.totalElements ?? 0,
|
||||
state: { pagination },
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
getRowId: (row) => row.groupId,
|
||||
});
|
||||
|
||||
const handleRowClick = (row: { groupId: string }) => {
|
||||
table.setRowSelection({ [row.groupId]: true });
|
||||
handleSelectedGroup(row.groupId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 목록</h3>
|
||||
<GroupsDialog onSuccess={refetch} />
|
||||
</div>
|
||||
<div className="card !m-4 h-[calc(100vh-25.5rem)] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupsSection };
|
||||
@@ -0,0 +1 @@
|
||||
export { GroupsPage } from "./groups-page";
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import {
|
||||
MenuAuthorityApi,
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityPayload,
|
||||
type MenuAuthorityFormData,
|
||||
} from "@/entities/menu-authority";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { createColumns } from "../model/group-menu-authority-table-columns";
|
||||
|
||||
type MenuAuthorityDialogProps = {
|
||||
groupId: string;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, groupId],
|
||||
queryFn: () => MenuAuthorityApi.getGroupMenuAuthorityList({ groupId }),
|
||||
enabled: open && !!groupId,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
|
||||
mutationFn: (data: MenuAuthorityPayload[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: menuAuthorityList?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: menuAuthorityList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleSave = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("menuAuthority");
|
||||
const filteredData = data.filter((item) => item.mode !== "R");
|
||||
const menuAuthorityData = filteredData.map((item) => ({
|
||||
...item,
|
||||
groupId,
|
||||
}));
|
||||
await saveMenuAuthority(menuAuthorityData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" onClick={(e) => e.stopPropagation()}>
|
||||
메뉴권한
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuAuthorityDialog };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { RoleApi } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type RoleSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
mngrYn?: string;
|
||||
};
|
||||
|
||||
function RoleSelect({ mngrYn = "N", ...props }: RoleSelectProps) {
|
||||
const { data: roles } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES],
|
||||
queryFn: () => RoleApi.getRoles(),
|
||||
select: (data) => {
|
||||
return data.content.filter((role) => role.mngrYn === mngrYn);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[24rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles?.map((role) => (
|
||||
<SelectItem key={role.roleId} value={role.roleId}>
|
||||
{role.roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export { RoleSelect };
|
||||
@@ -3,10 +3,9 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RoleApi, roleFormSchema, type RoleFormData } from "@/entities/role";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { RoleForm } from "./role-form";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import { roleFormSchema, type RoleFormData } from "../model/role-interface";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -4,11 +4,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { RoleApi, roleFormSchema, type RoleFormData } from "@/entities/role";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { RoleForm } from "./role-form";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import { roleFormSchema, type RoleFormData } from "../model/role-interface";
|
||||
|
||||
function ModifyPage({ roleId }: { roleId: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -13,10 +13,9 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { RoleApi, type RoleFormData } from "@/entities/role";
|
||||
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import type { RoleFormData } from "../model/role-interface";
|
||||
|
||||
type RoleFormProps = {
|
||||
roleId?: string;
|
||||
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { RoleApi } from "../api/role-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import type { Role, RoleParams } from "../model/role-interface";
|
||||
|
||||
function RolePage() {
|
||||
const router = useRouter();
|
||||
|
||||
Reference in New Issue
Block a user