diff --git a/apps/cs/app/api/getClientIp/route.ts b/apps/cs/app/api/getClientIp/route.ts
new file mode 100644
index 000000000..ec9ff72d5
--- /dev/null
+++ b/apps/cs/app/api/getClientIp/route.ts
@@ -0,0 +1,16 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function GET(request: NextRequest) {
+ const ipAddress =
+ request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown";
+
+ const ipv4Regex = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
+ const match = ipAddress.match(ipv4Regex);
+
+ const normalizedIp = match ? match[1] : ipAddress;
+
+ return NextResponse.json({
+ originalIp: ipAddress,
+ normalizedIp: normalizedIp,
+ });
+}
diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx
index 1a2189b03..b8fabff3d 100644
--- a/apps/cs/src/app/providers/app-provider.tsx
+++ b/apps/cs/src/app/providers/app-provider.tsx
@@ -5,6 +5,7 @@ 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";
@@ -16,28 +17,30 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const queryClient = new QueryClient();
return (
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/apps/cs/src/pages/site/api/ip-service.ts b/apps/cs/src/entities/ip/api/ip-service.ts
similarity index 89%
rename from apps/cs/src/pages/site/api/ip-service.ts
rename to apps/cs/src/entities/ip/api/ip-service.ts
index 903f0407e..0a9e063e6 100644
--- a/apps/cs/src/pages/site/api/ip-service.ts
+++ b/apps/cs/src/entities/ip/api/ip-service.ts
@@ -1,6 +1,6 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
-import type { IpParams, IP, IpList, IpService } from "../model/ip-interface";
+import type { IpService, IpParams, IpList, IP } from "../model/ip-interface";
export class IpApiService implements IpService {
public async getIpList(params: IpParams): Promise {
diff --git a/apps/cs/src/entities/ip/index.ts b/apps/cs/src/entities/ip/index.ts
new file mode 100644
index 000000000..ef1c5be33
--- /dev/null
+++ b/apps/cs/src/entities/ip/index.ts
@@ -0,0 +1,10 @@
+export { IpApi } from "./api/ip-service";
+export {
+ ipFormSchema,
+ type IpService,
+ type IPFormData,
+ type IpParams,
+ type IpList,
+ type IP,
+} from "./model/ip-interface";
+export { ClientIpGuard } from "./lib/client-ip-guard";
diff --git a/apps/cs/src/entities/ip/lib/client-ip-guard.tsx b/apps/cs/src/entities/ip/lib/client-ip-guard.tsx
new file mode 100644
index 000000000..f3ae452ce
--- /dev/null
+++ b/apps/cs/src/entities/ip/lib/client-ip-guard.tsx
@@ -0,0 +1,48 @@
+import { useEffect, useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { SiteApi } from "@/entities/site/@x/ip";
+import { ENDPOINT, SITE_ID } from "@/shared/config";
+import { IpApi } from "../api/ip-service";
+
+type ClientIpResponse = {
+ originalIp: string;
+ normalizedIp: string;
+};
+
+const ClientIpGuard = ({ children }: { children: React.ReactNode }) => {
+ const queryClient = useQueryClient();
+ const [isAllowed, setIsAllowed] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ const response = await fetch("/api/getClientIp");
+ const { normalizedIp } = (await response.json()) as ClientIpResponse;
+
+ if (!normalizedIp || !SITE_ID) return;
+
+ const siteInfo = await queryClient.ensureQueryData({
+ queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
+ queryFn: () => SiteApi.siteDetail(SITE_ID),
+ });
+
+ if (siteInfo.siteKndCd === "O") return;
+
+ const { content: ipList } = await IpApi.getIpList({
+ siteId: SITE_ID,
+ prmYn: "Y",
+ });
+
+ const allowedClientIp = ipList.some((ip) => ip.ipAddr === normalizedIp);
+ setIsAllowed(allowedClientIp);
+ })();
+ }, []);
+
+ if (!isAllowed) {
+ alert("허용되지 않은 IP로 접근하였습니다.");
+ return <>>;
+ }
+
+ return <>{children}>;
+};
+
+export { ClientIpGuard };
diff --git a/apps/cs/src/pages/site/model/ip-interface.ts b/apps/cs/src/entities/ip/model/ip-interface.ts
similarity index 100%
rename from apps/cs/src/pages/site/model/ip-interface.ts
rename to apps/cs/src/entities/ip/model/ip-interface.ts
diff --git a/apps/cs/src/entities/site/@x/ip.ts b/apps/cs/src/entities/site/@x/ip.ts
new file mode 100644
index 000000000..9daf50de3
--- /dev/null
+++ b/apps/cs/src/entities/site/@x/ip.ts
@@ -0,0 +1 @@
+export { SiteApi } from "../api/site-service";
diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
index ec5c7a0dd..c2a2c2702 100644
--- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
+++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
@@ -24,11 +24,10 @@ import {
Checkbox,
Label,
} from "@common/components";
+import { IpApi, ipFormSchema, type IP, type IPFormData } from "@/entities/ip";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
-import { IpApi } from "../api/ip-service";
-import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
type IPManagementDialogProps = {
siteId: string;