feat: 그룹 사용자 선택 삭제 기능

This commit is contained in:
2025-06-17 16:18:09 +09:00
parent 706a1db187
commit de67befb6c
6 changed files with 239 additions and 56 deletions
@@ -20,6 +20,7 @@ export interface UserSearchParams extends PageableParams {
grpId?: string;
frDateYmd?: string;
toDateYmd?: string;
userName?: string;
}
// 사용자 목록 응답 인터페이스
@@ -3,8 +3,9 @@ import { ENDPOINT } from "@/shared/config";
import type {
GroupUserService,
GroupUserSearchParams,
GroupUserList,
GroupUserListResponseDto,
GroupUserDetailResponse,
PageGroupUserList,
GroupUserSaveData,
GroupUserUpdateData,
} from "../model/group-users-interface";
@@ -13,8 +14,8 @@ export class GroupUsersApiService implements GroupUserService {
// 그룹 사용자 목록 조회
public async findPageGroupUser(
params: GroupUserSearchParams,
): Promise<GroupUserList> {
const response = await client.get<GroupUserList>(
): Promise<PageGroupUserList> {
const response = await client.get<PageGroupUserList>(
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
{ params },
);
@@ -34,9 +35,9 @@ export class GroupUsersApiService implements GroupUserService {
// 그룹 사용자 저장
public async saveGroupUser(
data: GroupUserSaveData,
): Promise<GroupUserDetailResponse> {
const response = await client.post<GroupUserDetailResponse>(
data: GroupUserSaveData[],
): Promise<GroupUserListResponseDto[]> {
const response = await client.post<GroupUserListResponseDto[]>(
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
data,
);
@@ -1,4 +1,4 @@
import type { PageableParams } from "@/shared/model";
import type { PageableList, PageableParams } from "@/shared/model";
// 그룹 사용자 검색 파라미터 인터페이스
export interface GroupUserSearchParams extends PageableParams {
@@ -7,49 +7,17 @@ export interface GroupUserSearchParams extends PageableParams {
// 그룹 사용자 목록 응답 인터페이스
export interface GroupUserListResponseDto {
grpSn: number;
mode: string;
groupId: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
ogdpSeCd: string;
ogdpSeNm: string;
mphn: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
rgtrNm: string;
mdfrNm: string;
}
// 그룹 사용자 페이지 응답 인터페이스
export interface GroupUserList {
content: GroupUserListResponseDto[];
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 type PageGroupUserList = PageableList<GroupUserListResponseDto>;
// 그룹 사용자 상세 응답 인터페이스
export interface GroupUserDetailResponse {
@@ -69,7 +37,8 @@ export interface GroupUserDetailResponse {
// 그룹 사용자 저장 데이터 인터페이스
export interface GroupUserSaveData {
grpSn: number;
mode: string;
groupId: string;
userUnqId: string;
}
@@ -81,7 +50,9 @@ export interface GroupUserUpdateData {
// 그룹 사용자 서비스 인터페이스
export interface GroupUserService {
// 그룹 사용자 목록 조회
findPageGroupUser: (params: GroupUserSearchParams) => Promise<GroupUserList>;
findPageGroupUser: (
params: GroupUserSearchParams,
) => Promise<PageGroupUserList>;
// 그룹 사용자 단건 조회
findByGroupAndUserId: (
@@ -90,7 +61,9 @@ export interface GroupUserService {
) => Promise<GroupUserDetailResponse>;
// 그룹 사용자 저장
saveGroupUser: (data: GroupUserSaveData) => Promise<GroupUserDetailResponse>;
saveGroupUser: (
data: GroupUserSaveData[],
) => Promise<GroupUserListResponseDto[]>;
// 그룹 사용자 수정
updateGroupUser: (
@@ -0,0 +1,192 @@
"use client";
import { useState } from "react";
import {
keepPreviousData,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
type PaginationState,
type RowSelectionState,
type ColumnDef,
} from "@tanstack/react-table";
import {
Button,
DataTable,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
Input,
Checkbox,
} from "@common/components";
import {
UserApi,
type UserListResponse,
type UserSearchParams,
} from "@/entities/user";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { GroupUsersApi } from "../api/group-users-service";
type AdditionalUserDialogProps = {
groupId?: string;
};
function AdditionalUserDialog({ groupId }: AdditionalUserDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
userName: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [userRowSelection, setUserRowSelection] = useState<RowSelectionState>(
{},
);
const { data: usersList } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
queryFn: () =>
UserApi.findPageUser({
...userSearchQuery,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: open,
placeholderData: keepPreviousData,
});
const userColumns: ColumnDef<UserListResponse>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="전체 선택"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="행 선택"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "userId",
header: "사용자ID",
},
{
accessorKey: "userName",
header: "이름",
},
{
accessorKey: "ogdpSeNm",
header: "소속",
},
{
accessorKey: "mphn",
header: "연락처",
},
];
const userTable = useReactTable({
data: usersList?.content ?? [],
columns: userColumns,
rowCount: usersList?.totalElements ?? 0,
state: { pagination, rowSelection: userRowSelection },
manualPagination: true,
enableRowSelection: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setUserRowSelection,
getRowId: (row) => row.userUnqId,
});
const handleAddUsers = async () => {
const selectedRows = Object.keys(userRowSelection);
if (selectedRows.length === 0 || !groupId) return;
try {
const usersToAdd = selectedRows.map((userUnqId) => ({
mode: "C",
groupId,
userUnqId,
}));
await GroupUsersApi.saveGroupUser(usersToAdd);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId],
});
alert("사용자 추가를 완료했습니다.", () => {
setOpen(false);
setUserRowSelection({});
});
} catch (error) {
console.error(error);
}
};
const handleSearch = () => {
setPagination({ pageIndex: 0, pageSize: 10 });
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button disabled={!groupId}>사용자 추가</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>사용자 추가</DialogTitle>
</DialogHeader>
<div className="mb-4 flex items-end gap-2">
<Input
value={userSearchQuery.userName}
onChange={(e) =>
setUserSearchQuery({
...userSearchQuery,
userName: e.target.value,
})
}
className="!text-[1.3rem]"
placeholder="사용자 이름으로 검색"
/>
<Button onClick={handleSearch}>검색</Button>
</div>
<DataTable table={userTable} viewPagination />
<DialogFooter>
<Button color="white" onClick={() => setOpen(false)}>
취소
</Button>
<Button
onClick={handleAddUsers}
disabled={userTable.getSelectedRowModel().rows.length === 0}
>
추가
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { AdditionalUserDialog };
@@ -1,7 +1,11 @@
"use client";
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
keepPreviousData,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
@@ -10,21 +14,26 @@ import {
} from "@tanstack/react-table";
import { Button, DataTable } from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { columns } from "../model/group-users-table-columns";
import { GroupUsersApi } from "../api/group-users-service";
import { AdditionalUserDialog } from "./additional-user-dialog";
type GroupUsersSectionProps = {
groupId?: string;
};
function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const { data: groupUsersList, refetch } = useQuery({
const { data: groupUsersList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId, pagination],
queryFn: () =>
GroupUsersApi.findPageGroupUser({
@@ -55,11 +64,19 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
if (selectedRows.length === 0 || !groupId) return;
try {
for (const userUnqId of selectedRows) {
await GroupUsersApi.deleteGroupUser(groupId, userUnqId);
}
refetch();
setRowSelection({});
const usersToDelete = selectedRows.map((userUnqId) => ({
mode: "D",
groupId,
userUnqId,
}));
await GroupUsersApi.saveGroupUser(usersToDelete);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId],
});
alert("사용자 삭제를 완료했습니다.", () => {
setRowSelection({});
});
} catch (error) {
console.error(error);
}
@@ -71,7 +88,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">그룹 사용자 목록</h3>
<div className="flex gap-2">
<Button>등록</Button>
<AdditionalUserDialog groupId={groupId} />
<Button
color="red"
disabled={table.getSelectedRowModel().rows.length === 0}
-1
View File
@@ -1 +0,0 @@
export { GroupsPage } from "./groups-page";