feat: ip 가드 로직 공통 적용

This commit is contained in:
2025-06-25 13:37:01 +09:00
parent 4696e2b710
commit 5aadc38441
12 changed files with 241 additions and 48 deletions
@@ -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>
);
};
+9 -6
View File
@@ -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 (
+1
View File
@@ -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

+62
View File
@@ -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;
}
}
}