feat: ip 가드 로직 공통 적용
This commit is contained in:
@@ -5,7 +5,6 @@ import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { ClientIpGuard } from "@/entities/ip";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
@@ -17,7 +16,6 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ClientIpGuard>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
@@ -40,7 +38,6 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</ClientIpGuard>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,12 +18,8 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => SiteApi.siteDetail(siteId),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(siteFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return {
|
||||
...filteredData,
|
||||
...data,
|
||||
faxNo: data.faxNumber,
|
||||
bscSiteYn: data.basicSiteYn,
|
||||
};
|
||||
@@ -50,7 +46,14 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
|
||||
const form = useForm<SiteFormData>({
|
||||
resolver: zodResolver(siteFormSchema),
|
||||
values: siteDetail as SiteFormData,
|
||||
values: {
|
||||
...siteDetail,
|
||||
siteId: siteDetail?.siteId ?? "",
|
||||
siteNm: siteDetail?.siteNm ?? "",
|
||||
siteKndCd: siteDetail?.siteKndCd ?? "",
|
||||
bscSiteYn: siteDetail?.bscSiteYn ?? "",
|
||||
useYn: siteDetail?.useYn ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -82,6 +82,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const images = {
|
||||
logo: require("./logo.png"),
|
||||
logoWhite: require("./logo_white.png"),
|
||||
menuBtn: require("./mob_menuBtn.svg"),
|
||||
menuBtnClose: require("./mob_menuBtn_close.svg"),
|
||||
iconFolderOff: require("./icon_folder_off.png"),
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,62 @@
|
||||
import { getApiClient } from "./client";
|
||||
import { ENDPOINT } from "../config";
|
||||
|
||||
interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
|
||||
export interface IpParams extends PageableParams {
|
||||
siteId: string;
|
||||
ipAddr?: string;
|
||||
prmYn?: string;
|
||||
}
|
||||
|
||||
export interface IP {
|
||||
mngNo: number;
|
||||
siteId: string;
|
||||
ipAddr: string;
|
||||
prmYn: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
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 type IpList = PageableList<IP>;
|
||||
|
||||
const client = getApiClient();
|
||||
|
||||
export const getIpList = async (params: IpParams) => {
|
||||
const response = await client.get<IpList>(`${ENDPOINT.CMS_SERVICE.IPS}`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export const ENDPOINT = {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
POPUPS: "/cms-service/api/v1/popups",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
USERS: "/user-service/api/v1/users",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getConfig } from "@common/config";
|
||||
import { getSiteInfo } from "../api/site";
|
||||
import { getIpList } from "../api/ip";
|
||||
import { ENDPOINT } from "../config";
|
||||
|
||||
const { SITE_ID } = getConfig();
|
||||
|
||||
const ClientIpGuard = ({
|
||||
currentPath,
|
||||
children,
|
||||
}: {
|
||||
currentPath?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [clientIp, setClientIp] = useState("");
|
||||
const [isAllowed, setIsAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("https://api.ipify.org?format=json");
|
||||
const data = await response.json();
|
||||
if (!data?.ip) return;
|
||||
setClientIp(data.ip);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!SITE_ID || !clientIp) return;
|
||||
|
||||
const siteInfo = await queryClient.ensureQueryData({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
|
||||
queryFn: () => getSiteInfo(SITE_ID),
|
||||
});
|
||||
|
||||
if (siteInfo.siteKndCd === "O") return;
|
||||
|
||||
const { content: ipList } = await getIpList({
|
||||
siteId: SITE_ID,
|
||||
prmYn: "Y",
|
||||
});
|
||||
|
||||
const allowedClientIp = ipList.some((ip) => ip.ipAddr === clientIp);
|
||||
setIsAllowed(allowedClientIp);
|
||||
})();
|
||||
}, [queryClient, clientIp, currentPath]);
|
||||
|
||||
if (!isAllowed) {
|
||||
alert("허용되지 않은 IP로 접근하였습니다.");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { ClientIpGuard };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const initHistoryPatch = (() => {
|
||||
let initialized = false;
|
||||
return () => {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const patch = (method: "pushState" | "replaceState") => {
|
||||
const original = history[method];
|
||||
history[method] = function (...args) {
|
||||
const result = original.apply(this, args);
|
||||
window.dispatchEvent(new Event("locationchange"));
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
||||
patch("pushState");
|
||||
patch("replaceState");
|
||||
};
|
||||
})();
|
||||
|
||||
export const usePathname = () => {
|
||||
const [pathname, setPathname] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initHistoryPatch();
|
||||
|
||||
const update = () => {
|
||||
queueMicrotask(() => setPathname(window.location.pathname));
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", update);
|
||||
window.addEventListener("locationchange", update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("popstate", update);
|
||||
window.removeEventListener("locationchange", update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return pathname;
|
||||
};
|
||||
@@ -78,6 +78,13 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getLogoImage = () => {
|
||||
const siteThemeKey = getCookie("site-theme-key");
|
||||
if (siteThemeKey === "blue" || siteThemeKey === "green")
|
||||
return images.logoWhite;
|
||||
return images.logo;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!userPopupList?.length) return;
|
||||
|
||||
@@ -98,7 +105,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
<img src={images.menuBtn} />
|
||||
</div>
|
||||
<h1 className="flex items-center gap-4">
|
||||
<img src={images.logo} className="h-[3rem]" alt="logo" />
|
||||
<img src={getLogoImage()} className="h-[3rem]" alt="logo" />
|
||||
<span className="text-[1.8rem] font-semibold text-foreground">
|
||||
{siteInfo?.siteNm}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import { ClientIpGuard } from "../../lib/client-ip-guard";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
@@ -21,6 +22,7 @@ export function BaseLayout({
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<ClientIpGuard currentPath={currentPath}>
|
||||
<PopupProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
@@ -40,6 +42,7 @@ export function BaseLayout({
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</PopupProvider>
|
||||
</ClientIpGuard>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
[data-sidebar="trigger"] > svg {
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-radix-popper-content-wrapper] > * {
|
||||
color: #0a0a0a !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
[data-radix-popper-content-wrapper] > *:hover {
|
||||
color: #0a0a0a !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
@@ -78,5 +86,13 @@
|
||||
[data-sidebar="trigger"] > svg {
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-radix-popper-content-wrapper] > * {
|
||||
color: #0a0a0a !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
[data-radix-popper-content-wrapper] > *:hover {
|
||||
color: #0a0a0a !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user