feat: 그룹 관리 작업

This commit is contained in:
2025-06-17 11:06:33 +09:00
parent e390026bfe
commit 318bae3a1e
30 changed files with 952 additions and 96 deletions
@@ -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>;
}
@@ -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,
});
+8
View File
@@ -0,0 +1,8 @@
export { RoleApi } from "./api/role-service";
export {
roleFormSchema,
type Role,
type RoleParams,
type RoleFormData,
type RoleList,
} from "./model/role-interface";
@@ -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>;
+4 -1
View File
@@ -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();
+11 -10
View File
@@ -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();
-2
View File
@@ -1,2 +0,0 @@
export * from "./groups-service";
export * from "./group-users-service";
+1
View File
@@ -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>
),
},
];
-2
View File
@@ -1,2 +0,0 @@
export * from "./groups-interface";
export * from "./group-users-interface";
+204
View File
@@ -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 };
+1
View File
@@ -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 };
+1 -2
View File
@@ -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();
+1 -2
View File
@@ -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();
+1 -2
View File
@@ -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;
+1 -2
View File
@@ -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();