Compare commits

..
22 changed files with 214 additions and 233 deletions
+3 -9
View File
@@ -26,14 +26,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
}
+5 -17
View File
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,23 +19,15 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
}
+4 -9
View File
@@ -3,11 +3,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -27,16 +23,15 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"exclude": [
"node_modules",
+8 -8
View File
@@ -1,17 +1,17 @@
import "@/app/styles/base.css";
export const metadata = {
title: "유역하수도 통합 정보플랫폼",
title: "하수도플랫폼 통합관리시스템",
};
export default function AuthLayout({
children,
children,
}: {
children: React.ReactNode;
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
import { App } from "@/app";
export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼",
title: "하수도플랫폼 통합관리시스템",
};
export default App;
+10
View File
@@ -0,0 +1,10 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config";
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN });
return NextResponse.json({ message: "Logged out" }, { status: 200 });
}
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/base.css";
import "@/app/styles/components.css";
export const metadata: Metadata = {
title: "하수도플랫폼 통합관리시스템",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">하수도플랫폼 통합관리시스템</span>
에 오신 것을 환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+1
View File
@@ -7,6 +7,7 @@ const nextConfig: NextConfig = {
output: "standalone",
experimental: {
proxyTimeout: 600000,
globalNotFound: true,
},
eslint: {
ignoreDuringBuilds: true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

@@ -92,6 +92,7 @@ async function silentRefresh(): Promise<void> {
}
async function logout(): Promise<void> {
await fetch("/api/auth/logout", { method: "POST" });
tokenManager.clearAuthToken();
clearDeviceId();
@@ -1,10 +1,12 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface AuthorizationsParams extends PageableParams {
keywordType?: string;
keyword?: string;
authorizationName?: string;
urlPatternValue?: string;
httpMethodCode?: string;
useYn?: string;
}
+19 -2
View File
@@ -23,6 +23,7 @@ import { columns } from "../model/authorizations-table-columns";
const DEFAULT_PARAMS = {
authorizationName: "",
urlPatternValue: "",
httpMethodCode: "",
sort: "regDt,desc",
page: 0,
@@ -36,6 +37,9 @@ function ModulePage() {
const [authorizationName, setAuthorizationName] = useState(
params.authorizationName,
);
const [urlPatternValue, setUrlPatternValue] = useState(
params.urlPatternValue,
);
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
const query = useQuery({
@@ -43,6 +47,7 @@ function ModulePage() {
queryFn: () =>
AuthorizationsApi.getAuthorizationsList({
authorizationName: params.authorizationName,
urlPatternValue: params.urlPatternValue,
httpMethodCode: params.httpMethodCode,
sort: params.sort,
page: params.page,
@@ -63,11 +68,12 @@ function ModulePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setParams({ authorizationName, httpMethodCode, page: 0 });
setParams({ authorizationName, urlPatternValue, httpMethodCode, page: 0 });
};
const handleReset = () => {
setAuthorizationName("");
setUrlPatternValue("");
setHttpMethodCode("");
resetParams();
};
@@ -113,12 +119,23 @@ function ModulePage() {
API명
</Label>
<Input
id="keyword"
id="authorizationName"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={authorizationName}
onChange={(e) => setAuthorizationName(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="urlPatternValue" className="text-label">
API URL
</Label>
<Input
id="urlPatternValue"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={urlPatternValue}
onChange={(e) => setUrlPatternValue(e.target.value)}
/>
</div>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
+15 -9
View File
@@ -1,10 +1,17 @@
"use client";
import dayjs from "dayjs";
import Image from "next/image";
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMenuStore, useMutationWrapper } from "@/shared/lib";
import { AttachmentApi } from "@/entities/attachment";
import { BoardApi } from "@/entities/board";
import { PostApi } from "@/entities/post";
import {
Button,
Label,
@@ -12,14 +19,11 @@ import {
RadioGroupItem,
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi } from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
import { PostEvaluationApi } from "../api/post-evaluation-service";
import { PostAction } from "./post-action";
import { PostComment } from "./post-comment";
import { PostEvaluationApi } from "../api/post-evaluation-service";
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
@@ -255,7 +259,9 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)}
</div>
</PostAction>
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
{bbsInfo?.cmntUseYn === "Y" && (
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
)}
</>
);
}
+2
View File
@@ -1,10 +1,12 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import presetConfig from "../../tailwind.config.preset";
export default {
presets: [presetConfig],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
],
+4 -9
View File
@@ -3,11 +3,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -27,16 +23,15 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"exclude": [
"node_modules",
+42 -46
View File
@@ -1,49 +1,45 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/common/types/**/*.d.ts"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
@@ -1,59 +0,0 @@
import { useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getSiteInfo } from "../api/site";
import { getIpList } from "../api/ip";
import { ENDPOINT } from "../config";
import { getCookie } from "../lib";
const siteId = getCookie("site-id-key") ?? "";
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 (!siteId || !clientIp) return;
const siteInfo = await queryClient.ensureQueryData({
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
queryFn: () => getSiteInfo(siteId),
});
if (siteInfo.siteKndCd === "O") return;
const { content: ipList } = await getIpList({
siteId,
prmYn: "Y",
});
const allowedClientIp = ipList.some((ip) => ip.ipAddr === clientIp);
setIsAllowed(allowedClientIp);
})();
}, [queryClient, clientIp, currentPath]);
if (!isAllowed) {
alert("허용되지 않은 IP로 접근하였습니다.");
return <></>;
}
return <>{children}</>;
};
export { ClientIpGuard };
@@ -3,7 +3,6 @@ import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import { ClientIpGuard } from "../../lib/client-ip-guard";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
@@ -29,30 +28,28 @@ export function BaseLayout({
}) {
return (
<QueryProvider queryClient={queryClient}>
<ClientIpGuard currentPath={currentPath}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header
authService={authService}
tokenManager={authService ? undefined : tokenManager}
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header
authService={authService}
tokenManager={authService ? undefined : tokenManager}
/>
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
<div className="flex-1">{children}</div>
<Footer />
</div>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
<div className="flex-1">{children}</div>
<Footer />
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</ClientIpGuard>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</QueryProvider>
);
}
@@ -2,7 +2,6 @@ import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import { ClientIpGuard } from "../../lib/client-ip-guard";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { LNB } from "../lnb";
@@ -25,26 +24,24 @@ export function NoFooterLayout({
}) {
return (
<QueryProvider queryClient={queryClient}>
<ClientIpGuard currentPath={currentPath}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
</div>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<LNB
currentPath={currentPath}
handleMenuClick={handleMenuClick}
/>
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</ClientIpGuard>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</QueryProvider>
);
}
@@ -3,7 +3,6 @@ import type { QueryClient } from "@tanstack/react-query";
import { QueryProvider } from "../../provider/query-provider";
import { PopupProvider } from "../../provider/popup-provider";
import { HelpDialogProvider } from "../../provider/help-dialog-context";
import { ClientIpGuard } from "../../lib/client-ip-guard";
import type { MenuItem } from "../../api";
import { SidebarProvider } from "../sidebar";
import { Header } from "../header";
@@ -14,7 +13,6 @@ export function NoLNBLayout({
children,
queryClient,
authService,
currentPath,
}: {
children: React.ReactNode;
queryClient?: QueryClient;
@@ -24,23 +22,21 @@ export function NoLNBLayout({
}) {
return (
<QueryProvider queryClient={queryClient}>
<ClientIpGuard currentPath={currentPath}>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
<Footer />
</div>
<PopupProvider>
<HelpDialogProvider>
<SidebarProvider>
<div className="flex-1">
<Header authService={authService} />
<div className="flex">
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
<div className="flex-1">{children}</div>
<Footer />
</div>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</ClientIpGuard>
</div>
</SidebarProvider>
</HelpDialogProvider>
</PopupProvider>
</QueryProvider>
);
}
+2 -6
View File
@@ -3,13 +3,9 @@
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["node"]
},
"include": ["."],
"exclude": [
"node_modules",
"dist",
"coverage",
"storybook-static"
]
"exclude": ["node_modules", "dist", "coverage", "storybook-static"]
}
+1
View File
@@ -0,0 +1 @@
declare module "*.css";