feat: 도메인 관리 및 IP 관리 작업
This commit is contained in:
@@ -5,10 +5,13 @@ const serverApiUrl =
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
return NextResponse.rewrite(
|
||||
`${serverApiUrl}${pathname.replace("/server", "")}`,
|
||||
const newUrl = new URL(
|
||||
`${serverApiUrl}${pathname.replace("/server", "")}?${searchParams.toString() ?? ""}`,
|
||||
);
|
||||
return NextResponse.rewrite(newUrl);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Domain,
|
||||
DomainService,
|
||||
DomainList,
|
||||
IDomainParams,
|
||||
} from "../model/domain-interface";
|
||||
|
||||
export class DomainApiService implements DomainService {
|
||||
public async getDomainList(params: IDomainParams): Promise<DomainList> {
|
||||
const response = await client.get<DomainList>(
|
||||
`${ENDPOINT.CMS_SERVICE.DOMAINS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveDomainList(body: Domain[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.DOMAINS}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const DomainApi = new DomainApiService();
|
||||
@@ -1,10 +1,12 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { IP, IpList, IpService } from "../model/ip-interface";
|
||||
import type { IIpParams, 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}`);
|
||||
public async getIpList(params: IIpParams): Promise<IpList> {
|
||||
const response = await client.get<IpList>(`${ENDPOINT.CMS_SERVICE.IPS}`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
siteFormSchema,
|
||||
type ISiteInfo,
|
||||
type SiteService,
|
||||
type SiteList,
|
||||
type ISiteDetail,
|
||||
type ISiteParams,
|
||||
type SiteFormData,
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async siteInfo(params: ISiteParams): Promise<ISiteInfo> {
|
||||
const response = await client.get<ISiteInfo>(
|
||||
public async getSiteList(params: ISiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IDomainParams extends IPageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
siteId?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export const domainSchema = z.object({
|
||||
siteDmnNo: z.number().nullish(),
|
||||
siteId: z.string(),
|
||||
dmnAddr: z.string().nonempty(),
|
||||
useYn: z.string(),
|
||||
rprsDmnYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const domainFormSchema = z.object({
|
||||
domains: z.array(domainSchema),
|
||||
});
|
||||
|
||||
export type Domain = z.infer<typeof domainSchema>;
|
||||
export type DomainFormData = z.infer<typeof domainFormSchema>;
|
||||
export type DomainList = PageableList<Domain[]>;
|
||||
|
||||
export interface DomainService {
|
||||
getDomainList(params: IDomainParams): Promise<DomainList>;
|
||||
saveDomainList(body: Domain[]): Promise<void>;
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IIpParams extends IPageableParams {
|
||||
siteId: string;
|
||||
ipAddr?: string;
|
||||
prmYn?: string;
|
||||
}
|
||||
|
||||
export const ipSchema = z.object({
|
||||
mngNo: z.number().nullish(),
|
||||
@@ -14,37 +21,9 @@ export const ipFormSchema = z.object({
|
||||
|
||||
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 type IpList = PageableList<IP[]>;
|
||||
|
||||
export interface IpService {
|
||||
getIpList(): Promise<IpList>;
|
||||
getIpList(params: IIpParams): Promise<IpList>;
|
||||
saveIpList(body: IP[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface ISiteParams {
|
||||
siteNm: string;
|
||||
siteKndCd: string;
|
||||
useYn: string;
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
export interface ISiteParams extends IPageableParams {
|
||||
siteNm?: string;
|
||||
siteKndCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ISite {
|
||||
@@ -20,35 +18,6 @@ export interface ISite {
|
||||
rgTrNm: string;
|
||||
}
|
||||
|
||||
export interface ISiteInfo {
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
size: number;
|
||||
content: ISite[];
|
||||
pageable: {
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
};
|
||||
numberOfElements: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface ISiteDetail extends SiteFormData {
|
||||
siteId: string;
|
||||
siteKindNm: string;
|
||||
@@ -65,22 +34,23 @@ export interface ISiteDetail extends SiteFormData {
|
||||
export const siteFormSchema = z.object({
|
||||
siteId: z.string().max(2).nonempty(),
|
||||
siteNm: z.string().max(100).nonempty(),
|
||||
siteExpln: z.string().max(1000).optional(),
|
||||
siteExpln: z.string().max(1000).nullable(),
|
||||
siteKndCd: z.string().max(10).nonempty(),
|
||||
siteAddr: z.string().max(2000).optional(),
|
||||
faxNo: z.string().max(100).optional(),
|
||||
telNo: z.string().max(1000).optional(),
|
||||
lwndCn: z.string().max(2000).optional(),
|
||||
siteAddr: z.string().max(2000).nullable(),
|
||||
faxNo: z.string().max(100).nullable(),
|
||||
telNo: z.string().max(1000).nullable(),
|
||||
lwndCn: z.string().max(2000).nullable(),
|
||||
bscSiteYn: z.string(),
|
||||
bkmkIcon: z.string().max(100).optional(),
|
||||
bkmkIcon: z.string().max(100).nullable(),
|
||||
useYn: z.string(),
|
||||
siteSkn: z.string().max(10).optional(),
|
||||
siteSkn: z.string().max(10).nullable(),
|
||||
});
|
||||
|
||||
export type SiteFormData = z.infer<typeof siteFormSchema>;
|
||||
export type SiteList = PageableList<ISite[]>;
|
||||
|
||||
export interface SiteService {
|
||||
siteInfo(query: ISiteParams): Promise<ISiteInfo>;
|
||||
getSiteList(params: ISiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<ISiteDetail>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
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 } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -13,197 +17,304 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
Checkbox,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
|
||||
export type Domain = {
|
||||
id: number;
|
||||
domainUrl: string;
|
||||
isRepresentative: boolean;
|
||||
isActive: "true" | "false";
|
||||
};
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { DomainApi } from "../api/domain-service";
|
||||
import {
|
||||
domainFormSchema,
|
||||
type Domain,
|
||||
type DomainFormData,
|
||||
} from "../model/domain-interface";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type DomainManagementDialogProps = {
|
||||
triggerDisabled?: boolean;
|
||||
handleSave: (rows: Domain[]) => void;
|
||||
siteId: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
handleSaveDomain?: (rows: Domain[]) => void;
|
||||
};
|
||||
|
||||
function DomainManagementDialog({
|
||||
triggerDisabled,
|
||||
handleSave,
|
||||
siteId,
|
||||
handleSaveDomain,
|
||||
...props
|
||||
}: DomainManagementDialogProps) {
|
||||
const [rows, setRows] = useState<Domain[]>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Domain["id"][]>([]);
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const onSave = () => {
|
||||
handleSave(rows);
|
||||
const { data: domainList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.DOMAINS, props.open],
|
||||
queryFn: () => DomainApi.getDomainList({ siteId }),
|
||||
select: (data) => {
|
||||
const domains = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
|
||||
initialForm();
|
||||
return domains;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: (body: Domain[]) => DomainApi.saveDomainList(body),
|
||||
});
|
||||
|
||||
const form = useForm<DomainFormData>({
|
||||
resolver: zodResolver(domainFormSchema),
|
||||
values: {
|
||||
domains: domainList ?? [],
|
||||
},
|
||||
});
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "domains",
|
||||
});
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: DomainFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await mutateAsync(data.domains);
|
||||
handleSaveDomain?.(data.domains);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddDomain = () => {
|
||||
const newDomain: Domain = {
|
||||
id: Date.now(),
|
||||
domainUrl: "",
|
||||
isRepresentative: rows.length === 0,
|
||||
isActive: "true",
|
||||
};
|
||||
setRows([...rows, newDomain]);
|
||||
};
|
||||
|
||||
const handleDeleteDomain = () => {
|
||||
const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
|
||||
const hasRepresentative = filteredRows.some((row) => row.isRepresentative);
|
||||
|
||||
let updatedRows = filteredRows;
|
||||
if (!hasRepresentative && filteredRows.length > 0) {
|
||||
updatedRows = filteredRows.map((row, index) =>
|
||||
index === 0 ? { ...row, isRepresentative: true } : row,
|
||||
);
|
||||
}
|
||||
|
||||
setRows(updatedRows);
|
||||
setRowSelection([]);
|
||||
};
|
||||
|
||||
const handleDomainUrlChange = (id: number, value: string) => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, domainUrl: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRepresentativeChange = (id: number) => {
|
||||
setRows(
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
isRepresentative: row.id === id,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const handleActiveChange = (id: number, value: "true" | "false") => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, isActive: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleRowSelection = (id: number) => {
|
||||
setRowSelection((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((item) => item !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
append({
|
||||
siteDmnNo: null,
|
||||
siteId,
|
||||
dmnAddr: "",
|
||||
useYn: "Y",
|
||||
rprsDmnYn: "N",
|
||||
mode: "C",
|
||||
});
|
||||
};
|
||||
|
||||
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 handleDeleteDomain = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
sortedIndices.forEach((i) => {
|
||||
const field = fields[i];
|
||||
if (field?.siteDmnNo) {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
});
|
||||
} else {
|
||||
remove(i);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const handleChangeRprsDmnYn = (i: number) => {
|
||||
fields.forEach((_, j) => {
|
||||
if (j === i) return;
|
||||
form.setValue(`domains.${j}.rprsDmnYn`, "N");
|
||||
});
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={triggerDisabled}>도메인 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[60rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>도메인 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button size="sm" color="white" onClick={handleAddDomain}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteDomain}
|
||||
disabled={rowSelection.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="12%" />
|
||||
<col width="40%" />
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NO</TableHead>
|
||||
<TableHead>선택</TableHead>
|
||||
<TableHead>도메인URL</TableHead>
|
||||
<TableHead>대표URL</TableHead>
|
||||
<TableHead>사용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, idx) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell align="center">{idx + 1}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={rowSelection.includes(row.id)}
|
||||
onCheckedChange={() => toggleRowSelection(row.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
value={row.domainUrl}
|
||||
onChange={(e) =>
|
||||
handleDomainUrlChange(row.id, e.target.value)
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={row.isRepresentative}
|
||||
onCheckedChange={() => handleRepresentativeChange(row.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Select
|
||||
value={row.isActive}
|
||||
onValueChange={(value: "true" | "false") =>
|
||||
handleActiveChange(row.id, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mx-auto w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">예</SelectItem>
|
||||
<SelectItem value="false">아니오</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" onClick={onSave}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>도메인 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>도메인 관리</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={handleAddDomain}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteDomain}
|
||||
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="auto" />
|
||||
<col width="15%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>도메인 주소</TableHead>
|
||||
<TableHead>대표 URL</TableHead>
|
||||
<TableHead>사용여부</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={`domains.${i}.dmnAddr`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`domains.${i}.rprsDmnYn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked ? "Y" : "N");
|
||||
handleChangeRprsDmnYn(i);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`domains.${i}.useYn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="Y"
|
||||
id={`useY-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useY-${i}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`useN-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useN-${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={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { useForm, useFieldArray } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -30,37 +30,38 @@ import {
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT, SITE_ID } from "@/shared/config";
|
||||
import { ENDPOINT } 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 = {
|
||||
siteId: string;
|
||||
triggerDisabled?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
function IPManagementDialog({
|
||||
siteId,
|
||||
triggerDisabled,
|
||||
...props
|
||||
}: IPManagementDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
useQuery({
|
||||
const { data: ipList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open],
|
||||
queryFn: () => IpApi.getIpList(),
|
||||
queryFn: () => IpApi.getIpList({ siteId, sort: [] }),
|
||||
select: (data) => {
|
||||
const ips = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
|
||||
form.reset();
|
||||
replace(ips);
|
||||
return data;
|
||||
initialForm();
|
||||
return ips;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,8 +71,11 @@ function IPManagementDialog({
|
||||
|
||||
const form = useForm<IPFormData>({
|
||||
resolver: zodResolver(ipFormSchema),
|
||||
values: {
|
||||
ips: ipList ?? [],
|
||||
},
|
||||
});
|
||||
const { fields, append, remove, update, replace } = useFieldArray({
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ips",
|
||||
});
|
||||
@@ -79,6 +83,11 @@ function IPManagementDialog({
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: IPFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
@@ -91,24 +100,34 @@ function IPManagementDialog({
|
||||
const handleAddIP = () => {
|
||||
append({
|
||||
mngNo: null,
|
||||
siteId: SITE_ID,
|
||||
siteId,
|
||||
ipAddr: "",
|
||||
prmYn: "Y",
|
||||
mode: "C",
|
||||
});
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
selectedRows.forEach((i) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, ...field } = fields[i] as FieldArrayWithId<IPFormData>;
|
||||
if (field.mngNo) {
|
||||
const handleDeleteIP = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
sortedIndices.forEach((i) => {
|
||||
const field = fields[i];
|
||||
if (field?.mngNo) {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
@@ -117,9 +136,13 @@ function IPManagementDialog({
|
||||
remove(i);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog {...props}>
|
||||
@@ -161,7 +184,12 @@ function IPManagementDialog({
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead>선택</TableHead>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>IP 주소</TableHead>
|
||||
<TableHead>허용여부</TableHead>
|
||||
</TableRow>
|
||||
|
||||
@@ -23,12 +23,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { type SiteFormData } from "../model/site-interface";
|
||||
import type { SiteFormData } from "../model/site-interface";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
import {
|
||||
DomainManagementDialog,
|
||||
type Domain,
|
||||
} from "./domain-management-dialog";
|
||||
import { DomainManagementDialog } from "./domain-management-dialog";
|
||||
|
||||
type SiteFormProps = {
|
||||
form: UseFormReturn<SiteFormData>;
|
||||
@@ -44,12 +42,11 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
|
||||
const [representativeDomain, setRepresentativeDomain] = useState("");
|
||||
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
|
||||
const [domainUrl, setDomainUrl] = useState("");
|
||||
|
||||
const handleDomainSave = (rows: Domain[]) => {
|
||||
setRepresentativeDomain(
|
||||
rows.find((row) => row.isRepresentative)?.domainUrl || "",
|
||||
);
|
||||
const handleSaveDomain = (rows: Domain[]) => {
|
||||
setDomainUrl(rows.find((row) => row.rprsDmnYn === "Y")?.dmnAddr || "");
|
||||
};
|
||||
|
||||
const onSubmit = async (data: SiteFormData) => {
|
||||
@@ -132,6 +129,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
@@ -151,6 +149,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
@@ -193,6 +192,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
@@ -209,10 +209,13 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{representativeDomain}
|
||||
{domainUrl}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
handleSave={handleDomainSave}
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -229,6 +232,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
@@ -250,6 +254,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
@@ -271,6 +276,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
@@ -292,6 +298,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
@@ -313,6 +320,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type ISite, type ISiteParams } from "../model/site-interface";
|
||||
@@ -15,23 +15,18 @@ import { SitePageLayout } from "./layout";
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
const { data: siteInfo, refetch } = useQuery({
|
||||
const { data: siteList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.siteInfo(siteQuery),
|
||||
queryFn: () => SiteApi.getSiteList(siteQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const [siteQuery, setSiteQuery] = useState<ISiteParams>({
|
||||
siteNm: "",
|
||||
siteKndCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 0,
|
||||
sort: [],
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: siteInfo?.content ?? [],
|
||||
data: siteList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
@@ -61,9 +56,7 @@ function SitePage() {
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={siteQuery.siteNm}
|
||||
onChange={(e) =>
|
||||
setSiteQuery({ ...siteQuery, siteNm: e.target.value })
|
||||
}
|
||||
onChange={(e) => setSiteQuery({ siteNm: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
|
||||
@@ -12,6 +12,7 @@ export const ENDPOINT = {
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
export interface PageableList<T> {
|
||||
content: T;
|
||||
pageable: PageableConfig;
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface IPageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./api-interface";
|
||||
Reference in New Issue
Block a user