feat: ip 관리 서비스 작업
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { IP, IpList, IpService } from "../model/ip-interface";
|
||||||
|
|
||||||
|
export class IpApiService implements IpService {
|
||||||
|
public async getIpList(): Promise<IpList> {
|
||||||
|
const response = await client.get<IpList>(`${ENDPOINT.CMS_SERVICE.IPS}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveIpList(body: IP[]): Promise<void> {
|
||||||
|
const response = await client.post<void>(
|
||||||
|
`${ENDPOINT.CMS_SERVICE.IPS}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IpApi = new IpApiService();
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ipSchema = z.object({
|
||||||
|
mngNo: z.number().nullish(),
|
||||||
|
siteId: z.string(),
|
||||||
|
ipAddr: z.string().nonempty(),
|
||||||
|
prmYn: z.string(),
|
||||||
|
mode: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ipFormSchema = z.object({
|
||||||
|
ips: z.array(ipSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IP = z.infer<typeof ipSchema>;
|
||||||
|
export type IPFormData = z.infer<typeof ipFormSchema>;
|
||||||
|
|
||||||
|
export interface IpList {
|
||||||
|
content: IP[];
|
||||||
|
pageable: {
|
||||||
|
sort: {
|
||||||
|
empty: boolean;
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
offset: number;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
paged: boolean;
|
||||||
|
unpaged: boolean;
|
||||||
|
};
|
||||||
|
last: boolean;
|
||||||
|
totalPages: number;
|
||||||
|
totalElements: number;
|
||||||
|
number: number;
|
||||||
|
sort: {
|
||||||
|
empty: boolean;
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
size: number;
|
||||||
|
first: boolean;
|
||||||
|
numberOfElements: number;
|
||||||
|
empty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IpService {
|
||||||
|
getIpList(): Promise<IpList>;
|
||||||
|
saveIpList(body: IP[]): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { useForm, useFieldArray, type FieldArrayWithId } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -15,162 +19,241 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
Checkbox,
|
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
|
Checkbox,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
import {
|
||||||
type IP = {
|
Form,
|
||||||
id: number;
|
FormControl,
|
||||||
ipAddress: string;
|
FormField,
|
||||||
allowed: boolean;
|
FormItem,
|
||||||
};
|
FormMessage,
|
||||||
|
} from "@/shared/ui/form";
|
||||||
|
import { ENDPOINT, SITE_ID } from "@/shared/config";
|
||||||
|
import { IpApi } from "../api/ip-service";
|
||||||
|
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
|
||||||
type IPManagementDialogProps = {
|
type IPManagementDialogProps = {
|
||||||
triggerDisabled?: boolean;
|
triggerDisabled?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
|
function IPManagementDialog({
|
||||||
const [rows, setRows] = useState<IP[]>([]);
|
triggerDisabled,
|
||||||
const [rowSelection, setRowSelection] = useState<IP["id"][]>([]);
|
...props
|
||||||
|
}: IPManagementDialogProps) {
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const { setAlertMessage } = useAlertStore((state) => state);
|
||||||
|
|
||||||
const onSave = () => {};
|
useQuery({
|
||||||
|
queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open],
|
||||||
|
queryFn: () => IpApi.getIpList(),
|
||||||
|
select: (data) => {
|
||||||
|
const ips = data.content.map((item) => ({
|
||||||
|
...item,
|
||||||
|
mode: "U",
|
||||||
|
}));
|
||||||
|
replace(ips);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync } = useMutation({
|
||||||
|
mutationFn: (body: IP[]) => IpApi.saveIpList(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<IPFormData>({
|
||||||
|
resolver: zodResolver(ipFormSchema),
|
||||||
|
});
|
||||||
|
const { fields, append, remove, update, replace } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "ips",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: IPFormData) => {
|
||||||
|
setIsOpen(true);
|
||||||
|
if (await confirm("저장하시겠습니까?")) {
|
||||||
|
setIsOpen(false);
|
||||||
|
try {
|
||||||
|
await mutateAsync(data.ips);
|
||||||
|
setAlertMessage("저장되었습니다.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setAlertMessage("저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddIP = () => {
|
const handleAddIP = () => {
|
||||||
const newIP: IP = {
|
append({
|
||||||
id: Date.now(),
|
mngNo: null,
|
||||||
ipAddress: "",
|
siteId: SITE_ID,
|
||||||
allowed: true,
|
ipAddr: "",
|
||||||
};
|
prmYn: "Y",
|
||||||
setRows([...rows, newIP]);
|
mode: "C",
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteIP = () => {
|
|
||||||
const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
|
|
||||||
setRows(filteredRows);
|
|
||||||
setRowSelection([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIPAddressChange = (id: number, value: string) => {
|
|
||||||
setRows(
|
|
||||||
rows.map((row) => (row.id === id ? { ...row, ipAddress: value } : row)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAllowedChange = (id: number, value: boolean) => {
|
|
||||||
setRows(
|
|
||||||
rows.map((row) => (row.id === id ? { ...row, allowed: value } : row)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleRowSelection = (id: number) => {
|
|
||||||
setRowSelection((prev) => {
|
|
||||||
if (prev.includes(id)) {
|
|
||||||
return prev.filter((item) => item !== id);
|
|
||||||
} else {
|
|
||||||
return [...prev, id];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectRow = (i: number) => {
|
||||||
|
setSelectedRows((prev) =>
|
||||||
|
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteIP = () => {
|
||||||
|
selectedRows.forEach((i) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { id, ...field } = fields[i] as FieldArrayWithId<IPFormData>;
|
||||||
|
if (field.mngNo) {
|
||||||
|
update(i, {
|
||||||
|
...field,
|
||||||
|
mode: "D",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
remove(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setSelectedRows([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<>
|
||||||
<DialogTrigger asChild>
|
<Dialog {...props}>
|
||||||
<Button disabled={triggerDisabled}>IP 관리</Button>
|
<DialogTrigger asChild>
|
||||||
</DialogTrigger>
|
<Button disabled={triggerDisabled}>IP 관리</Button>
|
||||||
<DialogContent className="sm:max-w-[60rem]">
|
</DialogTrigger>
|
||||||
<DialogHeader>
|
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||||
<DialogTitle>IP 관리</DialogTitle>
|
<DialogHeader>
|
||||||
</DialogHeader>
|
<DialogTitle>IP 관리</DialogTitle>
|
||||||
<div className="mb-4 flex justify-end gap-2">
|
</DialogHeader>
|
||||||
<Button size="sm" color="green">
|
<Form {...form}>
|
||||||
엑셀 업로드
|
<form
|
||||||
</Button>
|
onSubmit={(e) => {
|
||||||
<Button size="sm" color="white" onClick={handleAddIP}>
|
e.preventDefault();
|
||||||
항목 추가
|
e.stopPropagation();
|
||||||
</Button>
|
form.handleSubmit(onSubmit)(e);
|
||||||
<Button
|
}}
|
||||||
size="sm"
|
>
|
||||||
color="red"
|
<div className="mb-4 flex justify-end gap-2">
|
||||||
onClick={handleDeleteIP}
|
<Button type="button" size="sm" onClick={handleAddIP}>
|
||||||
disabled={rowSelection.length === 0}
|
항목 추가
|
||||||
>
|
</Button>
|
||||||
항목 삭제
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
</div>
|
size="sm"
|
||||||
<div className="card card-border">
|
color="red"
|
||||||
<Table variant="secondary">
|
onClick={handleDeleteIP}
|
||||||
<colgroup>
|
disabled={selectedRows.length === 0}
|
||||||
<col width="10%" />
|
>
|
||||||
<col width="10%" />
|
항목 삭제
|
||||||
<col width="55%" />
|
</Button>
|
||||||
<col width="25%" />
|
</div>
|
||||||
</colgroup>
|
<div className="card card-border max-h-[28.5rem] overflow-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<colgroup>
|
||||||
<TableHead>NO</TableHead>
|
<col width="10%" />
|
||||||
<TableHead>선택</TableHead>
|
<col width="65%" />
|
||||||
<TableHead>IP 주소</TableHead>
|
<col width="25%" />
|
||||||
<TableHead>허용여부</TableHead>
|
</colgroup>
|
||||||
</TableRow>
|
<TableHeader className="sticky -top-[0.6rem]">
|
||||||
</TableHeader>
|
<TableRow>
|
||||||
<TableBody>
|
<TableHead>선택</TableHead>
|
||||||
{rows.map((row, idx) => (
|
<TableHead>IP 주소</TableHead>
|
||||||
<TableRow key={row.id}>
|
<TableHead>허용여부</TableHead>
|
||||||
<TableCell align="center">{idx + 1}</TableCell>
|
</TableRow>
|
||||||
<TableCell align="center">
|
</TableHeader>
|
||||||
<Checkbox
|
<TableBody>
|
||||||
checked={rowSelection.includes(row.id)}
|
{fields.map((field, i) => {
|
||||||
onCheckedChange={() => toggleRowSelection(row.id)}
|
if (field.mode === "D") return null;
|
||||||
className="cursor-pointer"
|
return (
|
||||||
/>
|
<TableRow key={field.id}>
|
||||||
</TableCell>
|
<TableCell align="center" className="!px-0">
|
||||||
<TableCell align="center">
|
<Checkbox
|
||||||
<Input
|
checked={selectedRows.includes(i)}
|
||||||
value={row.ipAddress}
|
onCheckedChange={() => handleSelectRow(i)}
|
||||||
onChange={(e) =>
|
/>
|
||||||
handleIPAddressChange(row.id, e.target.value)
|
</TableCell>
|
||||||
}
|
<TableCell align="center">
|
||||||
className="!text-[1.3rem]"
|
<FormField
|
||||||
/>
|
control={form.control}
|
||||||
</TableCell>
|
name={`ips.${i}.ipAddr`}
|
||||||
<TableCell align="center">
|
render={({ field }) => (
|
||||||
<RadioGroup
|
<FormItem>
|
||||||
value={row.allowed ? "true" : "false"}
|
<FormControl>
|
||||||
onValueChange={(value) =>
|
<Input
|
||||||
handleAllowedChange(row.id, value === "true")
|
{...field}
|
||||||
}
|
className="!text-[1.3rem]"
|
||||||
className="flex justify-center"
|
/>
|
||||||
>
|
</FormControl>
|
||||||
<div className="flex items-center space-x-2">
|
<FormMessage className="text-left" />
|
||||||
<RadioGroupItem value="true" id={`allow-${row.id}`} />
|
</FormItem>
|
||||||
<label htmlFor={`allow-${row.id}`}>예</label>
|
)}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center space-x-2">
|
</TableCell>
|
||||||
<RadioGroupItem value="false" id={`deny-${row.id}`} />
|
<TableCell align="center">
|
||||||
<label htmlFor={`deny-${row.id}`}>아니오</label>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
</RadioGroup>
|
name={`ips.${i}.prmYn`}
|
||||||
</TableCell>
|
render={({ field }) => (
|
||||||
</TableRow>
|
<FormItem>
|
||||||
))}
|
<FormControl>
|
||||||
</TableBody>
|
<RadioGroup
|
||||||
</Table>
|
value={field.value}
|
||||||
</div>
|
onValueChange={field.onChange}
|
||||||
<DialogFooter className="!justify-center">
|
className="flex justify-center"
|
||||||
<DialogClose asChild>
|
>
|
||||||
<Button size="lg" onClick={onSave}>
|
<div className="flex items-center space-x-2">
|
||||||
저장
|
<RadioGroupItem
|
||||||
</Button>
|
value="Y"
|
||||||
</DialogClose>
|
id={`prmY-${i}`}
|
||||||
<DialogClose asChild>
|
/>
|
||||||
<Button size="lg" color="white">
|
<label htmlFor={`prmY-${i}`}>예</label>
|
||||||
취소
|
</div>
|
||||||
</Button>
|
<div className="flex items-center space-x-2">
|
||||||
</DialogClose>
|
<RadioGroupItem
|
||||||
</DialogFooter>
|
value="N"
|
||||||
</DialogContent>
|
id={`prmN-${i}`}
|
||||||
</Dialog>
|
/>
|
||||||
|
<label htmlFor={`prmN-${i}`}>
|
||||||
|
아니오
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</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>
|
||||||
|
<ConfirmDialog open={isOpen} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const ENDPOINT = {
|
|||||||
CMS_SERVICE: {
|
CMS_SERVICE: {
|
||||||
SITES: "/cms-service/api/v1/sites",
|
SITES: "/cms-service/api/v1/sites",
|
||||||
CODE: "/cms-service/api/v1/code/cache",
|
CODE: "/cms-service/api/v1/code/cache",
|
||||||
|
IPS: "/cms-service/api/v1/site-ips",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user