feat: 관리자 접속 IP 관리 기능 작업
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ManagerIp,
|
||||
ManagerIpInfo,
|
||||
ManagerIpList,
|
||||
ManagerIpParams,
|
||||
ManagerIpService,
|
||||
} from "../model/manager-ip-interface";
|
||||
|
||||
export class ManagerIpApiService implements ManagerIpService {
|
||||
public async getManagerIpList(
|
||||
params: ManagerIpParams,
|
||||
): Promise<ManagerIpList> {
|
||||
const response = await client.get<ManagerIpList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getManagerIpInfo(mngNo: string): Promise<ManagerIpInfo> {
|
||||
const response = await client.get<ManagerIpInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveManagerIp(data: ManagerIp[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
ENDPOINT.CMS_SERVICE.MANAGER_IP,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateManagerIp(mngNo: string, data: ManagerIp): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteManagerIp(mngNo: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.MANAGER_IP}/${mngNo}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ManagerIpApi = new ManagerIpApiService();
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ManagerIpParams extends PageableParams {
|
||||
ipAddr?: string;
|
||||
userUnqId?: string;
|
||||
}
|
||||
|
||||
export const managerIpSchema = z.object({
|
||||
mngNo: z.number().optional(),
|
||||
mode: z.string(),
|
||||
ipAddr: z.string().max(15),
|
||||
userUnqId: z.string(),
|
||||
regDt: z.string().optional(),
|
||||
rgtr: z.string().optional(),
|
||||
mdfcnDt: z.string().optional(),
|
||||
mdfr: z.string().optional(),
|
||||
});
|
||||
|
||||
export const managerIpFormSchema = z.object({
|
||||
ips: z.array(
|
||||
managerIpSchema.omit({
|
||||
regDt: true,
|
||||
rgtr: true,
|
||||
mdfcnDt: true,
|
||||
mdfr: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ManagerIp = z.infer<typeof managerIpSchema>;
|
||||
export type ManagerIpFormData = z.infer<typeof managerIpFormSchema>;
|
||||
export type ManagerIpList = PageableList<ManagerIp>;
|
||||
export type ManagerIpInfo = ManagerIp;
|
||||
|
||||
export interface ManagerIpService {
|
||||
getManagerIpList: (params: ManagerIpParams) => Promise<ManagerIpList>;
|
||||
getManagerIpInfo: (mngNo: string) => Promise<ManagerIpInfo>;
|
||||
saveManagerIp: (data: ManagerIp[]) => Promise<void>;
|
||||
updateManagerIp: (mngNo: string, data: ManagerIp) => Promise<void>;
|
||||
deleteManagerIp: (mngNo: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { AdminUserList } from "@/entities/user";
|
||||
import { ManagerIpDialog } from "../ui/manager-ip-dialog";
|
||||
|
||||
export const columns: ColumnDef<AdminUserList["content"][number]>[] = [
|
||||
{
|
||||
@@ -38,4 +39,11 @@ export const columns: ColumnDef<AdminUserList["content"][number]>[] = [
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => {
|
||||
return <ManagerIpDialog userUnqId={row.original.userUnqId} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
@@ -26,12 +27,11 @@ import {
|
||||
type UserSearchParams,
|
||||
type AdminUserListResponse,
|
||||
} from "@/entities/user";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useAlertStore } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { AddAdminDialog } from "./add-admin-dialog";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
function AdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { ManagerIpApi } from "../api/manager-ip-service";
|
||||
import {
|
||||
managerIpFormSchema,
|
||||
type ManagerIp,
|
||||
type ManagerIpFormData,
|
||||
} from "../model/manager-ip-interface";
|
||||
|
||||
type ManagerIpDialogProps = {
|
||||
userUnqId: string;
|
||||
};
|
||||
|
||||
function ManagerIpDialog({ userUnqId }: ManagerIpDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const { data: managerIpList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MANAGER_IP, open],
|
||||
queryFn: () => ManagerIpApi.getManagerIpList({ userUnqId }),
|
||||
select: (data) => {
|
||||
const ips = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
initialForm();
|
||||
return ips;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { mutateAsync } = useMutationWrapper({
|
||||
mutationFn: (data: ManagerIp[]) => ManagerIpApi.saveManagerIp(data),
|
||||
});
|
||||
|
||||
const form = useForm<ManagerIpFormData>({
|
||||
resolver: zodResolver(managerIpFormSchema),
|
||||
values: {
|
||||
ips: managerIpList ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ips",
|
||||
});
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ManagerIpFormData) => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
await mutateAsync(data.ips);
|
||||
alert("저장되었습니다.", () => {
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddIP = () => {
|
||||
append({
|
||||
mngNo: 0,
|
||||
mode: "C",
|
||||
ipAddr: "",
|
||||
userUnqId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === fields.length) {
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
setSelectedRows(fields.map((_, index) => index));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
setSelectedRows((prev) =>
|
||||
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteIP = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
if (await confirm("선택한 IP를 삭제하시겠습니까?")) {
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
|
||||
for (const i of sortedIndices) {
|
||||
const field = fields[i];
|
||||
if (field?.mngNo) {
|
||||
if (field.mode === "C") {
|
||||
remove(i);
|
||||
} else {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedRows([]);
|
||||
}
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">접속 IP</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>접속 IP 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button type="button" size="sm" onClick={handleAddIP}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteIP}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border max-h-[28.5rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="90%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>IP 주소</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields.map((field, i) => {
|
||||
if (field.mode === "D") return null;
|
||||
return (
|
||||
<TableRow key={field.id}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ips.${i}.ipAddr`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagerIpDialog };
|
||||
@@ -47,6 +47,7 @@ export const ENDPOINT = {
|
||||
ZIPS: "/cms-service/api/v1/zips",
|
||||
GROUPS: "/cms-service/api/v1/groups",
|
||||
GROUP_USERS: "/cms-service/api/v1/group-users",
|
||||
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
Reference in New Issue
Block a user