feat: 클라이언트 IP 가드 로직 작성

This commit is contained in:
2025-06-24 11:48:05 +09:00
parent bd9f9468f2
commit 2c6295a467
8 changed files with 102 additions and 25 deletions
+16
View File
@@ -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,
});
}
+25 -22
View File
@@ -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 (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<PromptProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>
<DndProvider
backend={MultiBackend}
options={getBackendOptions()}
>
{children}
</DndProvider>
</MenuWrapper>
<AlertDialog />
<PromptDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
<ClientIpGuard>
<StoreProvider>
<PromptProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>
<DndProvider
backend={MultiBackend}
options={getBackendOptions()}
>
{children}
</DndProvider>
</MenuWrapper>
<AlertDialog />
<PromptDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
</ClientIpGuard>
</QueryClientProvider>
);
};
@@ -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<IpList> {
+10
View File
@@ -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";
@@ -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 };
+1
View File
@@ -0,0 +1 @@
export { SiteApi } from "../api/site-service";
@@ -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;