feat: 관리자 접속 IP 관리 기능 작업

This commit is contained in:
2025-06-20 11:39:47 +09:00
parent d5b3a2e5f1
commit 3f9951c635
6 changed files with 344 additions and 2 deletions
@@ -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} />;
},
},
];
+2 -2
View File
@@ -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 };
+1
View File
@@ -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",